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