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