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