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