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