001 /*
002 * Copyright 2007-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2008-2016 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021 package com.unboundid.ldif;
022
023
024
025 import java.io.Closeable;
026 import java.io.File;
027 import java.io.IOException;
028 import java.io.OutputStream;
029 import java.io.FileOutputStream;
030 import java.io.BufferedOutputStream;
031 import java.util.List;
032 import java.util.ArrayList;
033 import java.util.Arrays;
034
035 import com.unboundid.asn1.ASN1OctetString;
036 import com.unboundid.ldap.sdk.Entry;
037 import com.unboundid.util.Base64;
038 import com.unboundid.util.LDAPSDKThreadFactory;
039 import com.unboundid.util.ByteStringBuffer;
040 import com.unboundid.util.parallel.ParallelProcessor;
041 import com.unboundid.util.parallel.Result;
042 import com.unboundid.util.parallel.Processor;
043
044 import static com.unboundid.util.Debug.*;
045 import static com.unboundid.util.StaticUtils.*;
046 import static com.unboundid.util.Validator.*;
047
048
049
050 /**
051 * This class provides an LDIF writer, which can be used to write entries and
052 * change records in the LDAP Data Interchange Format as per
053 * <A HREF="http://www.ietf.org/rfc/rfc2849.txt">RFC 2849</A>.
054 * <BR><BR>
055 * <H2>Example</H2>
056 * The following example performs a search to find all users in the "Sales"
057 * department and then writes their entries to an LDIF file:
058 * <PRE>
059 * // Perform a search to find all users who are members of the sales
060 * // department.
061 * SearchRequest searchRequest = new SearchRequest("dc=example,dc=com",
062 * SearchScope.SUB, Filter.createEqualityFilter("ou", "Sales"));
063 * SearchResult searchResult;
064 * try
065 * {
066 * searchResult = connection.search(searchRequest);
067 * }
068 * catch (LDAPSearchException lse)
069 * {
070 * searchResult = lse.getSearchResult();
071 * }
072 * LDAPTestUtils.assertResultCodeEquals(searchResult, ResultCode.SUCCESS);
073 *
074 * // Write all of the matching entries to LDIF.
075 * int entriesWritten = 0;
076 * LDIFWriter ldifWriter = new LDIFWriter(pathToLDIF);
077 * for (SearchResultEntry entry : searchResult.getSearchEntries())
078 * {
079 * ldifWriter.writeEntry(entry);
080 * entriesWritten++;
081 * }
082 *
083 * ldifWriter.close();
084 * </PRE>
085 */
086 public final class LDIFWriter
087 implements Closeable
088 {
089 /**
090 * Indicates whether LDIF records should include a comment above each
091 * base64-encoded value that attempts to provide an unencoded representation
092 * of that value (with special characters escaped).
093 */
094 private static volatile boolean commentAboutBase64EncodedValues = false;
095
096
097
098 /**
099 * The bytes that comprise the LDIF version header.
100 */
101 private static final byte[] VERSION_1_HEADER_BYTES =
102 getBytes("version: 1" + EOL);
103
104
105
106 /**
107 * The default buffer size (128KB) that will be used when writing LDIF data
108 * to the appropriate destination.
109 */
110 private static final int DEFAULT_BUFFER_SIZE = 128 * 1024;
111
112
113
114 // The writer that will be used to actually write the data.
115 private final BufferedOutputStream writer;
116
117 // The byte string buffer that will be used to convert LDIF records to LDIF.
118 // It will only be used when operating synchronously.
119 private final ByteStringBuffer buffer;
120
121 // The translator to use for change records to be written, if any.
122 private final LDIFWriterChangeRecordTranslator changeRecordTranslator;
123
124 // The translator to use for entries to be written, if any.
125 private final LDIFWriterEntryTranslator entryTranslator;
126
127 // The column at which to wrap long lines.
128 private int wrapColumn = 0;
129
130 // A pre-computed value that is two less than the wrap column.
131 private int wrapColumnMinusTwo = -2;
132
133 // non-null if this writer was configured to use multiple threads when
134 // writing batches of entries.
135 private final ParallelProcessor<LDIFRecord,ByteStringBuffer>
136 toLdifBytesInvoker;
137
138
139
140 /**
141 * Creates a new LDIF writer that will write entries to the provided file.
142 *
143 * @param path The path to the LDIF file to be written. It must not be
144 * {@code null}.
145 *
146 * @throws IOException If a problem occurs while opening the provided file
147 * for writing.
148 */
149 public LDIFWriter(final String path)
150 throws IOException
151 {
152 this(new FileOutputStream(path));
153 }
154
155
156
157 /**
158 * Creates a new LDIF writer that will write entries to the provided file.
159 *
160 * @param file The LDIF file to be written. It must not be {@code null}.
161 *
162 * @throws IOException If a problem occurs while opening the provided file
163 * for writing.
164 */
165 public LDIFWriter(final File file)
166 throws IOException
167 {
168 this(new FileOutputStream(file));
169 }
170
171
172
173 /**
174 * Creates a new LDIF writer that will write entries to the provided output
175 * stream.
176 *
177 * @param outputStream The output stream to which the data is to be written.
178 * It must not be {@code null}.
179 */
180 public LDIFWriter(final OutputStream outputStream)
181 {
182 this(outputStream, 0);
183 }
184
185
186
187 /**
188 * Creates a new LDIF writer that will write entries to the provided output
189 * stream optionally using parallelThreads when writing batches of LDIF
190 * records.
191 *
192 * @param outputStream The output stream to which the data is to be
193 * written. It must not be {@code null}.
194 * @param parallelThreads If this value is greater than zero, then the
195 * specified number of threads will be used to
196 * encode entries before writing them to the output
197 * for the {@code writeLDIFRecords(List)} method.
198 * Note this is the only output method that will
199 * use multiple threads.
200 * This should only be set to greater than zero when
201 * performance analysis has demonstrated that writing
202 * the LDIF is a bottleneck. The default
203 * synchronous processing is normally fast enough.
204 * There is no benefit in passing in a value
205 * greater than the number of processors in the
206 * system. A value of zero implies the
207 * default behavior of reading and parsing LDIF
208 * records synchronously when one of the read
209 * methods is called.
210 */
211 public LDIFWriter(final OutputStream outputStream, final int parallelThreads)
212 {
213 this(outputStream, parallelThreads, null);
214 }
215
216
217
218 /**
219 * Creates a new LDIF writer that will write entries to the provided output
220 * stream optionally using parallelThreads when writing batches of LDIF
221 * records.
222 *
223 * @param outputStream The output stream to which the data is to be
224 * written. It must not be {@code null}.
225 * @param parallelThreads If this value is greater than zero, then the
226 * specified number of threads will be used to
227 * encode entries before writing them to the output
228 * for the {@code writeLDIFRecords(List)} method.
229 * Note this is the only output method that will
230 * use multiple threads.
231 * This should only be set to greater than zero when
232 * performance analysis has demonstrated that writing
233 * the LDIF is a bottleneck. The default
234 * synchronous processing is normally fast enough.
235 * There is no benefit in passing in a value
236 * greater than the number of processors in the
237 * system. A value of zero implies the
238 * default behavior of reading and parsing LDIF
239 * records synchronously when one of the read
240 * methods is called.
241 * @param entryTranslator An optional translator that will be used to alter
242 * entries before they are actually written. This
243 * may be {@code null} if no translator is needed.
244 */
245 public LDIFWriter(final OutputStream outputStream, final int parallelThreads,
246 final LDIFWriterEntryTranslator entryTranslator)
247 {
248 this(outputStream, parallelThreads, entryTranslator, null);
249 }
250
251
252
253 /**
254 * Creates a new LDIF writer that will write entries to the provided output
255 * stream optionally using parallelThreads when writing batches of LDIF
256 * records.
257 *
258 * @param outputStream The output stream to which the data is to
259 * be written. It must not be {@code null}.
260 * @param parallelThreads If this value is greater than zero, then
261 * the specified number of threads will be
262 * used to encode entries before writing them
263 * to the output for the
264 * {@code writeLDIFRecords(List)} method.
265 * Note this is the only output method that
266 * will use multiple threads. This should
267 * only be set to greater than zero when
268 * performance analysis has demonstrated that
269 * writing the LDIF is a bottleneck. The
270 * default synchronous processing is normally
271 * fast enough. There is no benefit in
272 * passing in a value greater than the number
273 * of processors in the system. A value of
274 * zero implies the default behavior of
275 * reading and parsing LDIF records
276 * synchronously when one of the read methods
277 * is called.
278 * @param entryTranslator An optional translator that will be used to
279 * alter entries before they are actually
280 * written. This may be {@code null} if no
281 * translator is needed.
282 * @param changeRecordTranslator An optional translator that will be used to
283 * alter change records before they are
284 * actually written. This may be {@code null}
285 * if no translator is needed.
286 */
287 public LDIFWriter(final OutputStream outputStream, final int parallelThreads,
288 final LDIFWriterEntryTranslator entryTranslator,
289 final LDIFWriterChangeRecordTranslator changeRecordTranslator)
290 {
291 ensureNotNull(outputStream);
292 ensureTrue(parallelThreads >= 0,
293 "LDIFWriter.parallelThreads must not be negative.");
294
295 this.entryTranslator = entryTranslator;
296 this.changeRecordTranslator = changeRecordTranslator;
297 buffer = new ByteStringBuffer();
298
299 if (outputStream instanceof BufferedOutputStream)
300 {
301 writer = (BufferedOutputStream) outputStream;
302 }
303 else
304 {
305 writer = new BufferedOutputStream(outputStream, DEFAULT_BUFFER_SIZE);
306 }
307
308 if (parallelThreads == 0)
309 {
310 toLdifBytesInvoker = null;
311 }
312 else
313 {
314 final LDAPSDKThreadFactory threadFactory =
315 new LDAPSDKThreadFactory("LDIFWriter Worker", true, null);
316 toLdifBytesInvoker = new ParallelProcessor<LDIFRecord,ByteStringBuffer>(
317 new Processor<LDIFRecord,ByteStringBuffer>() {
318 public ByteStringBuffer process(final LDIFRecord input)
319 throws IOException
320 {
321 final LDIFRecord r;
322 if ((entryTranslator != null) && (input instanceof Entry))
323 {
324 r = entryTranslator.translateEntryToWrite((Entry) input);
325 if (r == null)
326 {
327 return null;
328 }
329 }
330 else if ((changeRecordTranslator != null) &&
331 (input instanceof LDIFChangeRecord))
332 {
333 r = changeRecordTranslator.translateChangeRecordToWrite(
334 (LDIFChangeRecord) input);
335 if (r == null)
336 {
337 return null;
338 }
339 }
340 else
341 {
342 r = input;
343 }
344
345 final ByteStringBuffer b = new ByteStringBuffer(200);
346 r.toLDIF(b, wrapColumn);
347 return b;
348 }
349 }, threadFactory, parallelThreads, 5);
350 }
351 }
352
353
354
355 /**
356 * Flushes the output stream used by this LDIF writer to ensure any buffered
357 * data is written out.
358 *
359 * @throws IOException If a problem occurs while attempting to flush the
360 * output stream.
361 */
362 public void flush()
363 throws IOException
364 {
365 writer.flush();
366 }
367
368
369
370 /**
371 * Closes this LDIF writer and the underlying LDIF target.
372 *
373 * @throws IOException If a problem occurs while closing the underlying LDIF
374 * target.
375 */
376 public void close()
377 throws IOException
378 {
379 try
380 {
381 if (toLdifBytesInvoker != null)
382 {
383 try
384 {
385 toLdifBytesInvoker.shutdown();
386 }
387 catch (InterruptedException e)
388 {
389 debugException(e);
390 }
391 }
392 }
393 finally
394 {
395 writer.close();
396 }
397 }
398
399
400
401 /**
402 * Retrieves the column at which to wrap long lines.
403 *
404 * @return The column at which to wrap long lines, or zero to indicate that
405 * long lines should not be wrapped.
406 */
407 public int getWrapColumn()
408 {
409 return wrapColumn;
410 }
411
412
413
414 /**
415 * Specifies the column at which to wrap long lines. A value of zero
416 * indicates that long lines should not be wrapped.
417 *
418 * @param wrapColumn The column at which to wrap long lines.
419 */
420 public void setWrapColumn(final int wrapColumn)
421 {
422 this.wrapColumn = wrapColumn;
423
424 wrapColumnMinusTwo = wrapColumn - 2;
425 }
426
427
428
429 /**
430 * Indicates whether the LDIF writer should generate comments that attempt to
431 * provide unencoded representations (with special characters escaped) of any
432 * base64-encoded values in entries and change records that are written by
433 * this writer.
434 *
435 * @return {@code true} if the LDIF writer should generate comments that
436 * attempt to provide unencoded representations of any base64-encoded
437 * values, or {@code false} if not.
438 */
439 public static boolean commentAboutBase64EncodedValues()
440 {
441 return commentAboutBase64EncodedValues;
442 }
443
444
445
446 /**
447 * Specifies whether the LDIF writer should generate comments that attempt to
448 * provide unencoded representations (with special characters escaped) of any
449 * base64-encoded values in entries and change records that are written by
450 * this writer.
451 *
452 * @param commentAboutBase64EncodedValues Indicates whether the LDIF writer
453 * should generate comments that
454 * attempt to provide unencoded
455 * representations (with special
456 * characters escaped) of any
457 * base64-encoded values in entries
458 * and change records that are
459 * written by this writer.
460 */
461 public static void setCommentAboutBase64EncodedValues(
462 final boolean commentAboutBase64EncodedValues)
463 {
464 LDIFWriter.commentAboutBase64EncodedValues =
465 commentAboutBase64EncodedValues;
466 }
467
468
469
470 /**
471 * Writes the LDIF version header (i.e.,"version: 1"). If a version header
472 * is to be added to the LDIF content, it should be done before any entries or
473 * change records have been written.
474 *
475 * @throws IOException If a problem occurs while writing the version header.
476 */
477 public void writeVersionHeader()
478 throws IOException
479 {
480 writer.write(VERSION_1_HEADER_BYTES);
481 }
482
483
484
485 /**
486 * Writes the provided entry in LDIF form.
487 *
488 * @param entry The entry to be written. It must not be {@code null}.
489 *
490 * @throws IOException If a problem occurs while writing the LDIF data.
491 */
492 public void writeEntry(final Entry entry)
493 throws IOException
494 {
495 writeEntry(entry, null);
496 }
497
498
499
500 /**
501 * Writes the provided entry in LDIF form, preceded by the provided comment.
502 *
503 * @param entry The entry to be written in LDIF form. It must not be
504 * {@code null}.
505 * @param comment The comment to be written before the entry. It may be
506 * {@code null} if no comment is to be written.
507 *
508 * @throws IOException If a problem occurs while writing the LDIF data.
509 */
510 public void writeEntry(final Entry entry, final String comment)
511 throws IOException
512 {
513 ensureNotNull(entry);
514
515 final Entry e;
516 if (entryTranslator == null)
517 {
518 e = entry;
519 }
520 else
521 {
522 e = entryTranslator.translateEntryToWrite(entry);
523 if (e == null)
524 {
525 return;
526 }
527 }
528
529 if (comment != null)
530 {
531 writeComment(comment, false, false);
532 }
533
534 debugLDIFWrite(e);
535 writeLDIF(e);
536 }
537
538
539
540 /**
541 * Writes the provided change record in LDIF form.
542 *
543 * @param changeRecord The change record to be written. It must not be
544 * {@code null}.
545 *
546 * @throws IOException If a problem occurs while writing the LDIF data.
547 */
548 public void writeChangeRecord(final LDIFChangeRecord changeRecord)
549 throws IOException
550 {
551 writeChangeRecord(changeRecord, null);
552 }
553
554
555
556 /**
557 * Writes the provided change record in LDIF form, preceded by the provided
558 * comment.
559 *
560 * @param changeRecord The change record to be written. It must not be
561 * {@code null}.
562 * @param comment The comment to be written before the entry. It may
563 * be {@code null} if no comment is to be written.
564 *
565 * @throws IOException If a problem occurs while writing the LDIF data.
566 */
567 public void writeChangeRecord(final LDIFChangeRecord changeRecord,
568 final String comment)
569 throws IOException
570 {
571 ensureNotNull(changeRecord);
572
573 final LDIFChangeRecord r;
574 if (changeRecordTranslator == null)
575 {
576 r = changeRecord;
577 }
578 else
579 {
580 r = changeRecordTranslator.translateChangeRecordToWrite(changeRecord);
581 if (r == null)
582 {
583 return;
584 }
585 }
586
587 if (comment != null)
588 {
589 writeComment(comment, false, false);
590 }
591
592 debugLDIFWrite(r);
593 writeLDIF(r);
594 }
595
596
597
598 /**
599 * Writes the provided record in LDIF form.
600 *
601 * @param record The LDIF record to be written. It must not be
602 * {@code null}.
603 *
604 * @throws IOException If a problem occurs while writing the LDIF data.
605 */
606 public void writeLDIFRecord(final LDIFRecord record)
607 throws IOException
608 {
609 writeLDIFRecord(record, null);
610 }
611
612
613
614 /**
615 * Writes the provided record in LDIF form, preceded by the provided comment.
616 *
617 * @param record The LDIF record to be written. It must not be
618 * {@code null}.
619 * @param comment The comment to be written before the LDIF record. It may
620 * be {@code null} if no comment is to be written.
621 *
622 * @throws IOException If a problem occurs while writing the LDIF data.
623 */
624 public void writeLDIFRecord(final LDIFRecord record, final String comment)
625 throws IOException
626 {
627 ensureNotNull(record);
628
629 final LDIFRecord r;
630 if ((entryTranslator != null) && (record instanceof Entry))
631 {
632 r = entryTranslator.translateEntryToWrite((Entry) record);
633 if (r == null)
634 {
635 return;
636 }
637 }
638 else if ((changeRecordTranslator != null) &&
639 (record instanceof LDIFChangeRecord))
640 {
641 r = changeRecordTranslator.translateChangeRecordToWrite(
642 (LDIFChangeRecord) record);
643 if (r == null)
644 {
645 return;
646 }
647 }
648 else
649 {
650 r = record;
651 }
652
653 debugLDIFWrite(r);
654 if (comment != null)
655 {
656 writeComment(comment, false, false);
657 }
658
659 writeLDIF(r);
660 }
661
662
663
664 /**
665 * Writes the provided list of LDIF records (most likely Entries) to the
666 * output. If this LDIFWriter was constructed without any parallel
667 * output threads, then this behaves identically to calling
668 * {@code writeLDIFRecord()} sequentially for each item in the list.
669 * If this LDIFWriter was constructed to write records in parallel, then
670 * the configured number of threads are used to convert the records to raw
671 * bytes, which are sequentially written to the input file. This can speed up
672 * the total time to write a large set of records. Either way, the output
673 * records are guaranteed to be written in the order they appear in the list.
674 *
675 * @param ldifRecords The LDIF records (most likely entries) to write to the
676 * output.
677 *
678 * @throws IOException If a problem occurs while writing the LDIF data.
679 *
680 * @throws InterruptedException If this thread is interrupted while waiting
681 * for the records to be written to the output.
682 */
683 public void writeLDIFRecords(final List<? extends LDIFRecord> ldifRecords)
684 throws IOException, InterruptedException
685 {
686 if (toLdifBytesInvoker == null)
687 {
688 for (final LDIFRecord ldifRecord : ldifRecords)
689 {
690 writeLDIFRecord(ldifRecord);
691 }
692 }
693 else
694 {
695 final List<Result<LDIFRecord,ByteStringBuffer>> results =
696 toLdifBytesInvoker.processAll(ldifRecords);
697 for (final Result<LDIFRecord,ByteStringBuffer> result: results)
698 {
699 rethrow(result.getFailureCause());
700
701 final ByteStringBuffer encodedBytes = result.getOutput();
702 if (encodedBytes != null)
703 {
704 encodedBytes.write(writer);
705 writer.write(EOL_BYTES);
706 }
707 }
708 }
709 }
710
711
712
713
714 /**
715 * Writes the provided comment to the LDIF target, wrapping long lines as
716 * necessary.
717 *
718 * @param comment The comment to be written to the LDIF target. It must
719 * not be {@code null}.
720 * @param spaceBefore Indicates whether to insert a blank line before the
721 * comment.
722 * @param spaceAfter Indicates whether to insert a blank line after the
723 * comment.
724 *
725 * @throws IOException If a problem occurs while writing the LDIF data.
726 */
727 public void writeComment(final String comment, final boolean spaceBefore,
728 final boolean spaceAfter)
729 throws IOException
730 {
731 ensureNotNull(comment);
732 if (spaceBefore)
733 {
734 writer.write(EOL_BYTES);
735 }
736
737 //
738 // Check for a newline explicitly to avoid the overhead of the regex
739 // for the common case of a single-line comment.
740 //
741
742 if (comment.indexOf('\n') < 0)
743 {
744 writeSingleLineComment(comment);
745 }
746 else
747 {
748 //
749 // Split on blank lines and wrap each line individually.
750 //
751
752 final String[] lines = comment.split("\\r?\\n");
753 for (final String line: lines)
754 {
755 writeSingleLineComment(line);
756 }
757 }
758
759 if (spaceAfter)
760 {
761 writer.write(EOL_BYTES);
762 }
763 }
764
765
766
767 /**
768 * Writes the provided comment to the LDIF target, wrapping long lines as
769 * necessary.
770 *
771 * @param comment The comment to be written to the LDIF target. It must
772 * not be {@code null}, and it must not include any line
773 * breaks.
774 *
775 * @throws IOException If a problem occurs while writing the LDIF data.
776 */
777 private void writeSingleLineComment(final String comment)
778 throws IOException
779 {
780 // We will always wrap comments, even if we won't wrap LDIF entries. If
781 // there is a wrap column set, then use it. Otherwise use the terminal
782 // width and back off two characters for the "# " at the beginning.
783 final int commentWrapMinusTwo;
784 if (wrapColumn <= 0)
785 {
786 commentWrapMinusTwo = TERMINAL_WIDTH_COLUMNS - 3;
787 }
788 else
789 {
790 commentWrapMinusTwo = wrapColumnMinusTwo;
791 }
792
793 buffer.clear();
794 final int length = comment.length();
795 if (length <= commentWrapMinusTwo)
796 {
797 buffer.append("# ");
798 buffer.append(comment);
799 buffer.append(EOL_BYTES);
800 }
801 else
802 {
803 int minPos = 0;
804 while (minPos < length)
805 {
806 if ((length - minPos) <= commentWrapMinusTwo)
807 {
808 buffer.append("# ");
809 buffer.append(comment.substring(minPos));
810 buffer.append(EOL_BYTES);
811 break;
812 }
813
814 // First, adjust the position until we find a space. Go backwards if
815 // possible, but if we can't find one there then go forward.
816 boolean spaceFound = false;
817 final int pos = minPos + commentWrapMinusTwo;
818 int spacePos = pos;
819 while (spacePos > minPos)
820 {
821 if (comment.charAt(spacePos) == ' ')
822 {
823 spaceFound = true;
824 break;
825 }
826
827 spacePos--;
828 }
829
830 if (! spaceFound)
831 {
832 spacePos = pos + 1;
833 while (spacePos < length)
834 {
835 if (comment.charAt(spacePos) == ' ')
836 {
837 spaceFound = true;
838 break;
839 }
840
841 spacePos++;
842 }
843
844 if (! spaceFound)
845 {
846 // There are no spaces at all in the remainder of the comment, so
847 // we'll just write the remainder of it all at once.
848 buffer.append("# ");
849 buffer.append(comment.substring(minPos));
850 buffer.append(EOL_BYTES);
851 break;
852 }
853 }
854
855 // We have a space, so we'll write up to the space position and then
856 // start up after the next space.
857 buffer.append("# ");
858 buffer.append(comment.substring(minPos, spacePos));
859 buffer.append(EOL_BYTES);
860
861 minPos = spacePos + 1;
862 while ((minPos < length) && (comment.charAt(minPos) == ' '))
863 {
864 minPos++;
865 }
866 }
867 }
868
869 buffer.write(writer);
870 }
871
872
873
874 /**
875 * Writes the provided record to the LDIF target, wrapping long lines as
876 * necessary.
877 *
878 * @param record The LDIF record to be written.
879 *
880 * @throws IOException If a problem occurs while writing the LDIF data.
881 */
882 private void writeLDIF(final LDIFRecord record)
883 throws IOException
884 {
885 buffer.clear();
886 record.toLDIF(buffer, wrapColumn);
887 buffer.append(EOL_BYTES);
888 buffer.write(writer);
889 }
890
891
892
893 /**
894 * Performs any appropriate wrapping for the provided set of LDIF lines.
895 *
896 * @param wrapColumn The column at which to wrap long lines. A value that
897 * is less than or equal to two indicates that no
898 * wrapping should be performed.
899 * @param ldifLines The set of lines that make up the LDIF data to be
900 * wrapped.
901 *
902 * @return A new list of lines that have been wrapped as appropriate.
903 */
904 public static List<String> wrapLines(final int wrapColumn,
905 final String... ldifLines)
906 {
907 return wrapLines(wrapColumn, Arrays.asList(ldifLines));
908 }
909
910
911
912 /**
913 * Performs any appropriate wrapping for the provided set of LDIF lines.
914 *
915 * @param wrapColumn The column at which to wrap long lines. A value that
916 * is less than or equal to two indicates that no
917 * wrapping should be performed.
918 * @param ldifLines The set of lines that make up the LDIF data to be
919 * wrapped.
920 *
921 * @return A new list of lines that have been wrapped as appropriate.
922 */
923 public static List<String> wrapLines(final int wrapColumn,
924 final List<String> ldifLines)
925 {
926 if (wrapColumn <= 2)
927 {
928 return new ArrayList<String>(ldifLines);
929 }
930
931 final ArrayList<String> newLines = new ArrayList<String>(ldifLines.size());
932 for (final String s : ldifLines)
933 {
934 final int length = s.length();
935 if (length <= wrapColumn)
936 {
937 newLines.add(s);
938 continue;
939 }
940
941 newLines.add(s.substring(0, wrapColumn));
942
943 int pos = wrapColumn;
944 while (pos < length)
945 {
946 if ((length - pos + 1) <= wrapColumn)
947 {
948 newLines.add(' ' + s.substring(pos));
949 break;
950 }
951 else
952 {
953 newLines.add(' ' + s.substring(pos, (pos+wrapColumn-1)));
954 pos += wrapColumn - 1;
955 }
956 }
957 }
958
959 return newLines;
960 }
961
962
963
964 /**
965 * Creates a string consisting of the provided attribute name followed by
966 * either a single colon and the string representation of the provided value,
967 * or two colons and the base64-encoded representation of the provided value.
968 *
969 * @param name The name for the attribute.
970 * @param value The value for the attribute.
971 *
972 * @return A string consisting of the provided attribute name followed by
973 * either a single colon and the string representation of the
974 * provided value, or two colons and the base64-encoded
975 * representation of the provided value.
976 */
977 public static String encodeNameAndValue(final String name,
978 final ASN1OctetString value)
979 {
980 final StringBuilder buffer = new StringBuilder();
981 encodeNameAndValue(name, value, buffer);
982 return buffer.toString();
983 }
984
985
986
987 /**
988 * Appends a string to the provided buffer consisting of the provided
989 * attribute name followed by either a single colon and the string
990 * representation of the provided value, or two colons and the base64-encoded
991 * representation of the provided value.
992 *
993 * @param name The name for the attribute.
994 * @param value The value for the attribute.
995 * @param buffer The buffer to which the name and value are to be written.
996 */
997 public static void encodeNameAndValue(final String name,
998 final ASN1OctetString value,
999 final StringBuilder buffer)
1000 {
1001 encodeNameAndValue(name, value, buffer, 0);
1002 }
1003
1004
1005
1006 /**
1007 * Appends a string to the provided buffer consisting of the provided
1008 * attribute name followed by either a single colon and the string
1009 * representation of the provided value, or two colons and the base64-encoded
1010 * representation of the provided value.
1011 *
1012 * @param name The name for the attribute.
1013 * @param value The value for the attribute.
1014 * @param buffer The buffer to which the name and value are to be
1015 * written.
1016 * @param wrapColumn The column at which to wrap long lines. A value that
1017 * is less than or equal to two indicates that no
1018 * wrapping should be performed.
1019 */
1020 public static void encodeNameAndValue(final String name,
1021 final ASN1OctetString value,
1022 final StringBuilder buffer,
1023 final int wrapColumn)
1024 {
1025 final int bufferStartPos = buffer.length();
1026 final byte[] valueBytes = value.getValue();
1027 boolean base64Encoded = false;
1028
1029 try
1030 {
1031 buffer.append(name);
1032 buffer.append(':');
1033
1034 final int length = valueBytes.length;
1035 if (length == 0)
1036 {
1037 buffer.append(' ');
1038 return;
1039 }
1040
1041 // If the value starts with a space, colon, or less-than character, then
1042 // it must be base64-encoded.
1043 switch (valueBytes[0])
1044 {
1045 case ' ':
1046 case ':':
1047 case '<':
1048 buffer.append(": ");
1049 Base64.encode(valueBytes, buffer);
1050 base64Encoded = true;
1051 return;
1052 }
1053
1054 // If the value ends with a space, then it should be base64-encoded.
1055 if (valueBytes[length-1] == ' ')
1056 {
1057 buffer.append(": ");
1058 Base64.encode(valueBytes, buffer);
1059 base64Encoded = true;
1060 return;
1061 }
1062
1063 // If any character in the value is outside the ASCII range, or is the
1064 // NUL, LF, or CR character, then the value should be base64-encoded.
1065 for (int i=0; i < length; i++)
1066 {
1067 if ((valueBytes[i] & 0x7F) != (valueBytes[i] & 0xFF))
1068 {
1069 buffer.append(": ");
1070 Base64.encode(valueBytes, buffer);
1071 base64Encoded = true;
1072 return;
1073 }
1074
1075 switch (valueBytes[i])
1076 {
1077 case 0x00: // The NUL character
1078 case 0x0A: // The LF character
1079 case 0x0D: // The CR character
1080 buffer.append(": ");
1081 Base64.encode(valueBytes, buffer);
1082 base64Encoded = true;
1083 return;
1084 }
1085 }
1086
1087 // If we've gotten here, then the string value is acceptable.
1088 buffer.append(' ');
1089 buffer.append(value.stringValue());
1090 }
1091 finally
1092 {
1093 if (wrapColumn > 2)
1094 {
1095 final int length = buffer.length() - bufferStartPos;
1096 if (length > wrapColumn)
1097 {
1098 final String EOL_PLUS_SPACE = EOL + ' ';
1099 buffer.insert((bufferStartPos+wrapColumn), EOL_PLUS_SPACE);
1100
1101 int pos = bufferStartPos + (2*wrapColumn) +
1102 EOL_PLUS_SPACE.length() - 1;
1103 while (pos < buffer.length())
1104 {
1105 buffer.insert(pos, EOL_PLUS_SPACE);
1106 pos += (wrapColumn - 1 + EOL_PLUS_SPACE.length());
1107 }
1108 }
1109 }
1110
1111 if (base64Encoded && commentAboutBase64EncodedValues)
1112 {
1113 writeBase64DecodedValueComment(valueBytes, buffer, wrapColumn);
1114 }
1115 }
1116 }
1117
1118
1119
1120 /**
1121 * Appends a comment to the provided buffer with an unencoded representation
1122 * of the provided value. This will only have any effect if
1123 * {@code commentAboutBase64EncodedValues} is {@code true}.
1124 *
1125 * @param valueBytes The bytes that comprise the value.
1126 * @param buffer The buffer to which the comment should be appended.
1127 * @param wrapColumn The column at which to wrap long lines.
1128 */
1129 private static void writeBase64DecodedValueComment(final byte[] valueBytes,
1130 final StringBuilder buffer,
1131 final int wrapColumn)
1132 {
1133 if (commentAboutBase64EncodedValues)
1134 {
1135 final int wrapColumnMinusTwo;
1136 if (wrapColumn <= 5)
1137 {
1138 wrapColumnMinusTwo = TERMINAL_WIDTH_COLUMNS - 3;
1139 }
1140 else
1141 {
1142 wrapColumnMinusTwo = wrapColumn - 2;
1143 }
1144
1145 final int wrapColumnMinusThree = wrapColumnMinusTwo - 1;
1146
1147 boolean first = true;
1148 final String comment =
1149 "Non-base64-encoded representation of the above value: " +
1150 getEscapedValue(valueBytes);
1151 for (final String s :
1152 wrapLine(comment, wrapColumnMinusTwo, wrapColumnMinusThree))
1153 {
1154 buffer.append(EOL);
1155 buffer.append("# ");
1156 if (first)
1157 {
1158 first = false;
1159 }
1160 else
1161 {
1162 buffer.append(' ');
1163 }
1164 buffer.append(s);
1165 }
1166 }
1167 }
1168
1169
1170
1171 /**
1172 * Appends a string to the provided buffer consisting of the provided
1173 * attribute name followed by either a single colon and the string
1174 * representation of the provided value, or two colons and the base64-encoded
1175 * representation of the provided value. It may optionally be wrapped at the
1176 * specified column.
1177 *
1178 * @param name The name for the attribute.
1179 * @param value The value for the attribute.
1180 * @param buffer The buffer to which the name and value are to be
1181 * written.
1182 * @param wrapColumn The column at which to wrap long lines. A value that
1183 * is less than or equal to two indicates that no
1184 * wrapping should be performed.
1185 */
1186 public static void encodeNameAndValue(final String name,
1187 final ASN1OctetString value,
1188 final ByteStringBuffer buffer,
1189 final int wrapColumn)
1190 {
1191 final int bufferStartPos = buffer.length();
1192 boolean base64Encoded = false;
1193
1194 try
1195 {
1196 buffer.append(name);
1197 base64Encoded = encodeValue(value, buffer);
1198 }
1199 finally
1200 {
1201 if (wrapColumn > 2)
1202 {
1203 final int length = buffer.length() - bufferStartPos;
1204 if (length > wrapColumn)
1205 {
1206 final byte[] EOL_BYTES_PLUS_SPACE = new byte[EOL_BYTES.length + 1];
1207 System.arraycopy(EOL_BYTES, 0, EOL_BYTES_PLUS_SPACE, 0,
1208 EOL_BYTES.length);
1209 EOL_BYTES_PLUS_SPACE[EOL_BYTES.length] = ' ';
1210
1211 buffer.insert((bufferStartPos+wrapColumn), EOL_BYTES_PLUS_SPACE);
1212
1213 int pos = bufferStartPos + (2*wrapColumn) +
1214 EOL_BYTES_PLUS_SPACE.length - 1;
1215 while (pos < buffer.length())
1216 {
1217 buffer.insert(pos, EOL_BYTES_PLUS_SPACE);
1218 pos += (wrapColumn - 1 + EOL_BYTES_PLUS_SPACE.length);
1219 }
1220 }
1221 }
1222
1223 if (base64Encoded && commentAboutBase64EncodedValues)
1224 {
1225 writeBase64DecodedValueComment(value.getValue(), buffer, wrapColumn);
1226 }
1227 }
1228 }
1229
1230
1231
1232 /**
1233 * Appends a string to the provided buffer consisting of the properly-encoded
1234 * representation of the provided value, including the necessary colon(s) and
1235 * space that precede it. Depending on the content of the value, it will
1236 * either be used as-is or base64-encoded.
1237 *
1238 * @param value The value for the attribute.
1239 * @param buffer The buffer to which the value is to be written.
1240 *
1241 * @return {@code true} if the value was base64-encoded, or {@code false} if
1242 * not.
1243 */
1244 static boolean encodeValue(final ASN1OctetString value,
1245 final ByteStringBuffer buffer)
1246 {
1247 buffer.append(':');
1248
1249 final byte[] valueBytes = value.getValue();
1250 final int length = valueBytes.length;
1251 if (length == 0)
1252 {
1253 buffer.append(' ');
1254 return false;
1255 }
1256
1257 // If the value starts with a space, colon, or less-than character, then
1258 // it must be base64-encoded.
1259 switch (valueBytes[0])
1260 {
1261 case ' ':
1262 case ':':
1263 case '<':
1264 buffer.append(':');
1265 buffer.append(' ');
1266 Base64.encode(valueBytes, buffer);
1267 return true;
1268 }
1269
1270 // If the value ends with a space, then it should be base64-encoded.
1271 if (valueBytes[length-1] == ' ')
1272 {
1273 buffer.append(':');
1274 buffer.append(' ');
1275 Base64.encode(valueBytes, buffer);
1276 return true;
1277 }
1278
1279 // If any character in the value is outside the ASCII range, or is the
1280 // NUL, LF, or CR character, then the value should be base64-encoded.
1281 for (int i=0; i < length; i++)
1282 {
1283 if ((valueBytes[i] & 0x7F) != (valueBytes[i] & 0xFF))
1284 {
1285 buffer.append(':');
1286 buffer.append(' ');
1287 Base64.encode(valueBytes, buffer);
1288 return true;
1289 }
1290
1291 switch (valueBytes[i])
1292 {
1293 case 0x00: // The NUL character
1294 case 0x0A: // The LF character
1295 case 0x0D: // The CR character
1296 buffer.append(':');
1297 buffer.append(' ');
1298
1299 Base64.encode(valueBytes, buffer);
1300 return true;
1301 }
1302 }
1303
1304 // If we've gotten here, then the string value is acceptable.
1305 buffer.append(' ');
1306 buffer.append(valueBytes);
1307 return false;
1308 }
1309
1310
1311
1312 /**
1313 * Appends a comment to the provided buffer with an unencoded representation
1314 * of the provided value. This will only have any effect if
1315 * {@code commentAboutBase64EncodedValues} is {@code true}.
1316 *
1317 * @param valueBytes The bytes that comprise the value.
1318 * @param buffer The buffer to which the comment should be appended.
1319 * @param wrapColumn The column at which to wrap long lines.
1320 */
1321 private static void writeBase64DecodedValueComment(final byte[] valueBytes,
1322 final ByteStringBuffer buffer,
1323 final int wrapColumn)
1324 {
1325 if (commentAboutBase64EncodedValues)
1326 {
1327 final int wrapColumnMinusTwo;
1328 if (wrapColumn <= 5)
1329 {
1330 wrapColumnMinusTwo = TERMINAL_WIDTH_COLUMNS - 3;
1331 }
1332 else
1333 {
1334 wrapColumnMinusTwo = wrapColumn - 2;
1335 }
1336
1337 final int wrapColumnMinusThree = wrapColumnMinusTwo - 1;
1338
1339 boolean first = true;
1340 final String comment =
1341 "Non-base64-encoded representation of the above value: " +
1342 getEscapedValue(valueBytes);
1343 for (final String s :
1344 wrapLine(comment, wrapColumnMinusTwo, wrapColumnMinusThree))
1345 {
1346 buffer.append(EOL);
1347 buffer.append("# ");
1348 if (first)
1349 {
1350 first = false;
1351 }
1352 else
1353 {
1354 buffer.append(' ');
1355 }
1356 buffer.append(s);
1357 }
1358 }
1359 }
1360
1361
1362
1363 /**
1364 * Retrieves a string representation of the provided value with all special
1365 * characters escaped with backslashes.
1366 *
1367 * @param valueBytes The byte array containing the value to encode.
1368 *
1369 * @return A string representation of the provided value with any special
1370 * characters
1371 */
1372 private static String getEscapedValue(final byte[] valueBytes)
1373 {
1374 final StringBuilder buffer = new StringBuilder(valueBytes.length * 2);
1375 for (int i=0; i < valueBytes.length; i++)
1376 {
1377 final byte b = valueBytes[i];
1378 switch (b)
1379 {
1380 case '\n':
1381 buffer.append("\\n");
1382 break;
1383 case '\r':
1384 buffer.append("\\r");
1385 break;
1386 case '\t':
1387 buffer.append("\\t");
1388 break;
1389 case ' ':
1390 if (i == 0)
1391 {
1392 buffer.append("\\ ");
1393 }
1394 else if ( i == (valueBytes.length - 1))
1395 {
1396 buffer.append("\\20");
1397 }
1398 else
1399 {
1400 buffer.append(' ');
1401 }
1402 break;
1403 case '<':
1404 if (i == 0)
1405 {
1406 buffer.append('\\');
1407 }
1408 buffer.append('<');
1409 break;
1410 case ':':
1411 if (i == 0)
1412 {
1413 buffer.append('\\');
1414 }
1415 buffer.append(':');
1416 break;
1417 default:
1418 if ((b >= '!') && (b <= '~'))
1419 {
1420 buffer.append((char) b);
1421 }
1422 else
1423 {
1424 buffer.append("\\");
1425 toHex(b, buffer);
1426 }
1427 break;
1428 }
1429 }
1430
1431 return buffer.toString();
1432 }
1433
1434
1435
1436 /**
1437 * If the provided exception is non-null, then it will be rethrown as an
1438 * unchecked exception or an IOException.
1439 *
1440 * @param t The exception to rethrow as an an unchecked exception or an
1441 * IOException or {@code null} if none.
1442 *
1443 * @throws IOException If t is a checked exception.
1444 */
1445 static void rethrow(final Throwable t)
1446 throws IOException
1447 {
1448 if (t == null)
1449 {
1450 return;
1451 }
1452
1453 if (t instanceof IOException)
1454 {
1455 throw (IOException) t;
1456 }
1457 else if (t instanceof RuntimeException)
1458 {
1459 throw (RuntimeException) t;
1460 }
1461 else if (t instanceof Error)
1462 {
1463 throw (Error) t;
1464 }
1465 else
1466 {
1467 throw createIOExceptionWithCause(null, t);
1468 }
1469 }
1470 }