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