001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2013-2015 ForgeRock AS.
026 */
027package org.opends.server.util;
028
029import java.io.BufferedWriter;
030import java.io.Closeable;
031import java.io.IOException;
032import java.util.Collection;
033import java.util.Iterator;
034import java.util.List;
035import java.util.regex.Pattern;
036
037import org.forgerock.i18n.LocalizableMessage;
038import org.forgerock.opendj.ldap.ByteSequence;
039import org.forgerock.opendj.ldap.ByteString;
040import org.opends.server.tools.makeldif.TemplateEntry;
041import org.opends.server.types.*;
042
043import static org.forgerock.util.Reject.*;
044import static org.opends.server.util.StaticUtils.*;
045
046/**
047 * This class provides a mechanism for writing entries in LDIF form to a file or
048 * an output stream.
049 */
050@org.opends.server.types.PublicAPI(
051     stability=org.opends.server.types.StabilityLevel.UNCOMMITTED,
052     mayInstantiate=true,
053     mayExtend=false,
054     mayInvoke=true)
055public final class LDIFWriter implements Closeable
056{
057  // FIXME -- Add support for generating a hash when writing the data.
058  // FIXME -- Add support for signing the hash that is generated.
059
060  /** The writer to which the LDIF information will be written. */
061  private BufferedWriter writer;
062
063  /** The configuration to use for the export. */
064  private LDIFExportConfig exportConfig;
065
066  /** Regular expression used for splitting comments on line-breaks. */
067  private static final Pattern SPLIT_NEWLINE = Pattern.compile("\\r?\\n");
068
069
070
071  /**
072   * Creates a new LDIF writer with the provided configuration.
073   *
074   * @param  exportConfig  The configuration to use for the export.  It must not
075   *                       be <CODE>null</CODE>.
076   *
077   * @throws  IOException  If a problem occurs while opening the writer.
078   */
079  public LDIFWriter(LDIFExportConfig exportConfig)
080         throws IOException
081  {
082    ifNull(exportConfig);
083    this.exportConfig = exportConfig;
084
085    writer = exportConfig.getWriter();
086  }
087
088
089
090  /**
091   * Writes the provided comment to the LDIF file, optionally wrapping near the
092   * specified column.  Each line will be prefixed by the octothorpe (#)
093   * character followed by a space.  If the comment should be wrapped at a
094   * specified column, then it will attempt to do so at the first whitespace
095   * character at or before that column (so it will try not wrap in the middle
096   * of a word).
097   * <BR><BR>
098   * This comment will be ignored by the
099   * Directory Server's LDIF reader, as well as any other compliant LDIF parsing
100   * software.
101   *
102   * @param  comment     The comment to be written.  Any line breaks that it
103   *                     contains will be honored, and potentially new line
104   *                     breaks may be introduced by the wrapping process.  It
105   *                     must not be <CODE>null</CODE>.
106   * @param  wrapColumn  The column at which long lines should be wrapped, or
107   *                     -1 to indicate that no additional wrapping should be
108   *                     added.  This will override the wrap column setting
109   *                     specified in the LDIF export configuration.
110   *
111   * @throws  IOException  If a problem occurs while attempting to write the
112   *                       comment to the LDIF file.
113   */
114  public void writeComment(LocalizableMessage comment, int wrapColumn)
115         throws IOException
116  {
117    ifNull(comment);
118
119
120    // First, break up the comment into multiple lines to preserve the original
121    // spacing that it contained.
122    String[] lines = SPLIT_NEWLINE.split(comment);
123
124    // Now iterate through the lines and write them out, prefixing and wrapping
125    // them as necessary.
126    for (String l : lines)
127    {
128      if (wrapColumn <= 0)
129      {
130        writer.write("# ");
131        writer.write(l);
132        writer.newLine();
133      }
134      else
135      {
136        int breakColumn = wrapColumn - 2;
137
138        if (l.length() <= breakColumn)
139        {
140          writer.write("# ");
141          writer.write(l);
142          writer.newLine();
143        }
144        else
145        {
146          int startPos = 0;
147outerLoop:
148          while (startPos < l.length())
149          {
150            if (startPos + breakColumn >= l.length())
151            {
152              writer.write("# ");
153              writer.write(l.substring(startPos));
154              writer.newLine();
155              startPos = l.length();
156            }
157            else
158            {
159              int endPos = startPos + breakColumn;
160
161              int i=endPos - 1;
162              while (i > startPos)
163              {
164                if (l.charAt(i) == ' ')
165                {
166                  writer.write("# ");
167                  writer.write(l.substring(startPos, i));
168                  writer.newLine();
169
170                  startPos = i+1;
171                  continue outerLoop;
172                }
173
174                i--;
175              }
176
177              // If we've gotten here, then there are no spaces on the entire
178              // line.  If that happens, then we'll have to break in the middle
179              // of a word.
180              writer.write("# ");
181              writer.write(l.substring(startPos, endPos));
182              writer.newLine();
183
184              startPos = endPos;
185            }
186          }
187        }
188      }
189    }
190  }
191
192  /**
193 * Iterates over each entry contained in the map and writes out the entry in
194 * LDIF format. The main benefit of this method is that the entries can be
195 * sorted by DN and output in sorted order.
196 *
197 * @param entries The Map containing the entries keyed by DN.
198 *
199 * @return <CODE>true</CODE>of all of the entries were
200 *                  written out, <CODE>false</CODE>if it was not
201 *                  because of the export configuration.
202 *
203 * @throws IOException  If a problem occurs while writing the entry to LDIF.
204 *
205 * @throws LDIFException If a problem occurs while trying to determine
206 *                         whether to include the entry in the export.
207 */
208  public boolean writeEntries(Collection<Entry> entries) throws IOException,
209      LDIFException
210  {
211    for (Entry entry : entries)
212    {
213      if (!writeEntry(entry))
214      {
215        return false;
216      }
217    }
218    return true;
219  }
220
221
222  /**
223   * Writes the provided entry to LDIF.
224   *
225   * @param  entry  The entry to be written.  It must not be <CODE>null</CODE>.
226   *
227   * @return  <CODE>true</CODE> if the entry was actually written, or
228   *          <CODE>false</CODE> if it was not because of the export
229   *          configuration.
230   *
231   * @throws  IOException  If a problem occurs while writing the entry to LDIF.
232   *
233   * @throws  LDIFException  If a problem occurs while trying to determine
234   *                         whether to include the entry in the export.
235   */
236  public boolean writeEntry(Entry entry)
237         throws IOException, LDIFException
238  {
239    ifNull(entry);
240    return entry.toLDIF(exportConfig);
241  }
242
243
244  /**
245   * Writes the provided template entry to LDIF.
246   *
247   * @param  templateEntry  The template entry to be written.  It must not be
248   * <CODE>null</CODE>.
249   *
250   * @return  <CODE>true</CODE> if the entry was actually written, or
251   *          <CODE>false</CODE> if it was not because of the export
252   *          configuration.
253   *
254   * @throws  IOException  If a problem occurs while writing the template entry
255   *                       to LDIF.
256   *
257   * @throws  LDIFException  If a problem occurs while trying to determine
258   *                         whether to include the template entry in the
259   *                         export.
260   */
261  public boolean writeTemplateEntry(TemplateEntry templateEntry)
262  throws IOException, LDIFException
263  {
264    ifNull(templateEntry);
265    return templateEntry.toLDIF(exportConfig);
266  }
267
268  /**
269   * Writes a change record entry for the provided change record.
270   *
271   * @param  changeRecord  The change record entry to be written.
272   *
273   * @throws  IOException  If a problem occurs while writing the change record.
274   */
275  public void writeChangeRecord(ChangeRecordEntry changeRecord)
276         throws IOException
277  {
278    ifNull(changeRecord);
279
280
281    // Get the information necessary to write the LDIF.
282    BufferedWriter writer     = exportConfig.getWriter();
283    int            wrapColumn = exportConfig.getWrapColumn();
284    boolean        wrapLines  = wrapColumn > 1;
285
286
287    // First, write the DN.
288    writeDN("dn", changeRecord.getDN(), writer, wrapLines, wrapColumn);
289
290
291    // Figure out what type of change it is and act accordingly.
292    if (changeRecord instanceof AddChangeRecordEntry)
293    {
294      StringBuilder changeTypeLine = new StringBuilder("changetype: add");
295      writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn);
296
297      AddChangeRecordEntry addRecord = (AddChangeRecordEntry) changeRecord;
298      for (Attribute a : addRecord.getAttributes())
299      {
300        for (ByteString v : a)
301        {
302          final String attrName = a.getNameWithOptions();
303          writeAttribute(attrName, v, writer, wrapLines, wrapColumn);
304        }
305      }
306    }
307    else if (changeRecord instanceof DeleteChangeRecordEntry)
308    {
309      StringBuilder changeTypeLine = new StringBuilder("changetype: delete");
310      writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn);
311    }
312    else if (changeRecord instanceof ModifyChangeRecordEntry)
313    {
314      StringBuilder changeTypeLine = new StringBuilder("changetype: modify");
315      writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn);
316
317      ModifyChangeRecordEntry modifyRecord =
318           (ModifyChangeRecordEntry) changeRecord;
319      List<RawModification> mods = modifyRecord.getModifications();
320      Iterator<RawModification> iterator = mods.iterator();
321      while (iterator.hasNext())
322      {
323        RawModification m = iterator.next();
324        RawAttribute a = m.getAttribute();
325        String attrName = a.getAttributeType();
326        StringBuilder modTypeLine = new StringBuilder();
327        modTypeLine.append(m.getModificationType());
328        modTypeLine.append(": ");
329        modTypeLine.append(attrName);
330        writeLDIFLine(modTypeLine, writer, wrapLines, wrapColumn);
331
332        for (ByteString s : a.getValues())
333        {
334          StringBuilder valueLine = new StringBuilder(attrName);
335          String stringValue = s.toString();
336
337          if (needsBase64Encoding(stringValue))
338          {
339            valueLine.append(":: ");
340            valueLine.append(Base64.encode(s));
341          }
342          else
343          {
344            valueLine.append(": ");
345            valueLine.append(stringValue);
346          }
347
348          writeLDIFLine(valueLine, writer, wrapLines, wrapColumn);
349        }
350
351        if (iterator.hasNext())
352        {
353          StringBuilder dashLine = new StringBuilder("-");
354          writeLDIFLine(dashLine, writer, wrapLines, wrapColumn);
355        }
356      }
357    }
358    else if (changeRecord instanceof ModifyDNChangeRecordEntry)
359    {
360      StringBuilder changeTypeLine = new StringBuilder("changetype: moddn");
361      writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn);
362
363      ModifyDNChangeRecordEntry modifyDNRecord =
364           (ModifyDNChangeRecordEntry) changeRecord;
365
366      StringBuilder newRDNLine = new StringBuilder("newrdn: ");
367      modifyDNRecord.getNewRDN().toString(newRDNLine);
368      writeLDIFLine(newRDNLine, writer, wrapLines, wrapColumn);
369
370      StringBuilder deleteOldRDNLine = new StringBuilder("deleteoldrdn: ");
371      deleteOldRDNLine.append(modifyDNRecord.deleteOldRDN() ? "1" : "0");
372      writeLDIFLine(deleteOldRDNLine, writer, wrapLines, wrapColumn);
373
374      DN newSuperiorDN = modifyDNRecord.getNewSuperiorDN();
375      if (newSuperiorDN != null)
376      {
377        StringBuilder newSuperiorLine = new StringBuilder("newsuperior: ");
378        newSuperiorDN.toString(newSuperiorLine);
379        writeLDIFLine(newSuperiorLine, writer, wrapLines, wrapColumn);
380      }
381    }
382
383
384    // Make sure there is a blank line after the entry.
385    writer.newLine();
386  }
387
388
389
390  /**
391   * Writes an add change record for the provided entry.  No filtering will be
392   * performed for this entry, nor will any export plugins be invoked.  Further,
393   * only the user attributes will be included.
394   *
395   * @param  entry  The entry to include in the add change record.  It must not
396   *                be <CODE>null</CODE>.
397   *
398   * @throws  IOException  If a problem occurs while writing the add record.
399   */
400  public void writeAddChangeRecord(Entry entry)
401         throws IOException
402  {
403    ifNull(entry);
404
405
406    // Get the information necessary to write the LDIF.
407    BufferedWriter writer     = exportConfig.getWriter();
408    int            wrapColumn = exportConfig.getWrapColumn();
409    boolean        wrapLines  = wrapColumn > 1;
410
411
412    // First, write the DN.
413    writeDN("dn", entry.getName(), writer, wrapLines, wrapColumn);
414
415
416    // Next, the changetype.
417    StringBuilder changeTypeLine = new StringBuilder("changetype: add");
418    writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn);
419
420
421    // Now the objectclasses.
422    for (String s : entry.getObjectClasses().values())
423    {
424      StringBuilder ocLine = new StringBuilder();
425      ocLine.append("objectClass: ");
426      ocLine.append(s);
427      writeLDIFLine(ocLine, writer, wrapLines, wrapColumn);
428    }
429
430
431    // Finally, the set of user attributes.
432    for (AttributeType attrType : entry.getUserAttributes().keySet())
433    {
434      for (Attribute a : entry.getUserAttribute(attrType))
435      {
436        StringBuilder attrName = new StringBuilder(a.getName());
437        for (String o : a.getOptions())
438        {
439          attrName.append(";");
440          attrName.append(o);
441        }
442
443        for (ByteString v : a)
444        {
445          writeAttribute(attrName, v, writer, wrapLines, wrapColumn);
446        }
447      }
448    }
449
450
451    // Make sure there is a blank line after the entry.
452    writer.newLine();
453  }
454
455
456
457  /**
458   * Writes a delete change record for the provided entry, optionally including
459   * a comment with the full entry contents.  No filtering will be performed for
460   * this entry, nor will any export plugins be invoked.  Further, only the user
461   * attributes will be included.
462   *
463   * @param  entry         The entry to include in the delete change record.  It
464   *                       must not be <CODE>null</CODE>.
465   * @param  commentEntry  Indicates whether to include a comment with the
466   *                       contents of the entry.
467   *
468   * @throws  IOException  If a problem occurs while writing the delete record.
469   */
470  public void writeDeleteChangeRecord(Entry entry, boolean commentEntry)
471         throws IOException
472  {
473    ifNull(entry);
474
475    // Get the information necessary to write the LDIF.
476    BufferedWriter writer     = exportConfig.getWriter();
477    int            wrapColumn = exportConfig.getWrapColumn();
478    boolean        wrapLines  = wrapColumn > 1;
479
480
481    // Add the DN and changetype lines.
482    writeDN("dn", entry.getName(), writer, wrapLines, wrapColumn);
483
484    StringBuilder changeTypeLine = new StringBuilder("changetype: delete");
485    writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn);
486
487
488    // If we should include a comment with the rest of the entry contents, then
489    // do so now.
490    if (commentEntry)
491    {
492      // Write the objectclasses.
493      for (String s : entry.getObjectClasses().values())
494      {
495        StringBuilder ocLine = new StringBuilder();
496        ocLine.append("# objectClass: ");
497        ocLine.append(s);
498        writeLDIFLine(ocLine, writer, wrapLines, wrapColumn);
499      }
500
501      // Write the set of user attributes.
502      for (AttributeType attrType : entry.getUserAttributes().keySet())
503      {
504        for (Attribute a : entry.getUserAttribute(attrType))
505        {
506          StringBuilder attrName = new StringBuilder();
507          attrName.append("# ");
508          attrName.append(a.getName());
509          for (String o : a.getOptions())
510          {
511            attrName.append(";");
512            attrName.append(o);
513          }
514
515          for (ByteString v : a)
516          {
517            writeAttribute(attrName, v, writer, wrapLines, wrapColumn);
518          }
519        }
520      }
521    }
522
523
524    // Make sure there is a blank line after the entry.
525    writer.newLine();
526  }
527
528
529
530  /**
531   * Writes a modify change record with the provided information.  No filtering
532   * will be performed, nor will any export plugins be invoked.
533   *
534   * @param  dn             The DN of the entry being modified.  It must not be
535   *                        <CODE>null</CODE>.
536   * @param  modifications  The set of modifications to include in the change
537   *                        record.  It must not be <CODE>null</CODE>.
538   *
539   * @throws  IOException  If a problem occurs while writing the modify record.
540   */
541  public void writeModifyChangeRecord(DN dn, List<Modification> modifications)
542         throws IOException
543  {
544    ifNull(dn, modifications);
545
546    // If there aren't any modifications, then there's nothing to do.
547    if (modifications.isEmpty())
548    {
549      return;
550    }
551
552
553    // Get the information necessary to write the LDIF.
554    BufferedWriter writer     = exportConfig.getWriter();
555    int            wrapColumn = exportConfig.getWrapColumn();
556    boolean        wrapLines  = wrapColumn > 1;
557
558
559    // Write the DN and changetype.
560    writeDN("dn", dn, writer, wrapLines, wrapColumn);
561
562    StringBuilder changeTypeLine = new StringBuilder("changetype: modify");
563    writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn);
564
565
566    // Iterate through the modifications and write them to the LDIF.
567    Iterator<Modification> iterator = modifications.iterator();
568    while (iterator.hasNext())
569    {
570      Modification m    = iterator.next();
571      Attribute    a    = m.getAttribute();
572
573      StringBuilder nameBuffer = new StringBuilder(a.getName());
574      for (String o : a.getOptions())
575      {
576        nameBuffer.append(";");
577        nameBuffer.append(o);
578      }
579      String  name = nameBuffer.toString();
580
581      StringBuilder modTypeLine = new StringBuilder();
582      modTypeLine.append(m.getModificationType());
583      modTypeLine.append(": ");
584      modTypeLine.append(name);
585      writeLDIFLine(modTypeLine, writer, wrapLines, wrapColumn);
586
587      for (ByteString v : a)
588      {
589        writeAttribute(name, v, writer, wrapLines, wrapColumn);
590      }
591
592
593      // If this is the last modification, then append blank line.  Otherwise
594      // write a line with just a dash.
595      if (iterator.hasNext())
596      {
597        writer.write("-");
598      }
599      writer.newLine();
600    }
601  }
602
603
604
605  /**
606   * Writes a modify DN change record with the provided information.  No
607   * filtering will be performed, nor will any export plugins be invoked.
608   *
609   * @param  dn            The DN of the entry before the rename.  It must not
610   *                       be <CODE>null</CODE>.
611   * @param  newRDN        The new RDN for the entry.  It must not be
612   *                       <CODE>null</CODE>.
613   * @param  deleteOldRDN  Indicates whether the old RDN value should be removed
614   *                       from the entry.
615   * @param  newSuperior   The new superior DN for the entry, or
616   *                       <CODE>null</CODE> if the entry will stay below the
617   *                       same parent.
618   *
619   * @throws  IOException  If a problem occurs while writing the modify record.
620   */
621  public void writeModifyDNChangeRecord(DN dn, RDN newRDN, boolean deleteOldRDN,
622                                        DN newSuperior)
623         throws IOException
624  {
625    ifNull(dn, newRDN);
626
627
628    // Get the information necessary to write the LDIF.
629    BufferedWriter writer     = exportConfig.getWriter();
630    int            wrapColumn = exportConfig.getWrapColumn();
631    boolean        wrapLines  = wrapColumn > 1;
632
633
634    // Write the current DN.
635    writeDN("dn", dn, writer, wrapLines, wrapColumn);
636
637
638    // Write the changetype.  Some older tools may not support the "moddn"
639    // changetype, so only use it if a newSuperior element has been provided,
640    // but use modrdn elsewhere.
641    String changeType = newSuperior == null ? "changetype: modrdn" : "changetype: moddn";
642    writeLDIFLine(new StringBuilder(changeType), writer, wrapLines, wrapColumn);
643
644
645    // Write the newRDN element.
646    StringBuilder rdnLine = new StringBuilder("newrdn");
647    appendLDIFSeparatorAndValue(rdnLine, ByteString.valueOfUtf8(newRDN.toString()));
648    writeLDIFLine(rdnLine, writer, wrapLines, wrapColumn);
649
650
651    // Write the deleteOldRDN element.
652    StringBuilder deleteOldRDNLine = new StringBuilder();
653    deleteOldRDNLine.append("deleteoldrdn: ");
654    deleteOldRDNLine.append(deleteOldRDN ? "1" : "0");
655    writeLDIFLine(deleteOldRDNLine, writer, wrapLines, wrapColumn);
656
657    if (newSuperior != null)
658    {
659      writeDN("newsuperior", newSuperior, writer, wrapLines, wrapColumn);
660    }
661
662
663    // Make sure there is a blank line after the entry.
664    writer.newLine();
665  }
666
667  private void writeDN(String attrType, DN dn, BufferedWriter writer,
668      boolean wrapLines, int wrapColumn) throws IOException
669  {
670    final StringBuilder newLine = new StringBuilder(attrType);
671    appendLDIFSeparatorAndValue(newLine, ByteString.valueOfUtf8(dn.toString()));
672    writeLDIFLine(newLine, writer, wrapLines, wrapColumn);
673  }
674
675  private void writeAttribute(CharSequence attrName, ByteString v,
676      BufferedWriter writer, boolean wrapLines, int wrapColumn)
677      throws IOException
678  {
679    StringBuilder newLine = new StringBuilder(attrName);
680    appendLDIFSeparatorAndValue(newLine, v);
681    writeLDIFLine(newLine, writer, wrapLines, wrapColumn);
682  }
683
684  /**
685   * Flushes the data written to the output stream or underlying file.
686   *
687   * @throws  IOException  If a problem occurs while flushing the output.
688   */
689  public void flush()
690         throws IOException
691  {
692    writer.flush();
693  }
694
695
696
697  /**
698   * Closes the LDIF writer and the underlying output stream or file.
699   *
700   * @throws  IOException  If a problem occurs while closing the writer.
701   */
702  @Override
703  public void close()
704         throws IOException
705  {
706    writer.flush();
707    writer.close();
708  }
709
710
711
712  /**
713   * Appends an LDIF separator and properly-encoded form of the given
714   * value to the provided buffer.  If the value is safe to include
715   * as-is, then a single colon, a single space, space, and the
716   * provided value will be appended.  Otherwise, two colons, a single
717   * space, and a base64-encoded form of the value will be appended.
718   *
719   * @param  buffer      The buffer to which the information should be
720   *                     appended.  It must not be <CODE>null</CODE>.
721   * @param  valueBytes  The value to append to the buffer.  It must not be
722   *                     <CODE>null</CODE>.
723   */
724  public static void appendLDIFSeparatorAndValue(StringBuilder buffer,
725                                                 ByteSequence valueBytes)
726  {
727    appendLDIFSeparatorAndValue(buffer, valueBytes, false, false);
728  }
729
730  /**
731   * Appends an LDIF separator and properly-encoded form of the given
732   * value to the provided buffer.  If the value is safe to include
733   * as-is, then a single colon, a single space, space, and the
734   * provided value will be appended.  Otherwise, two colons, a single
735   * space, and a base64-encoded form of the value will be appended.
736   * @param  buffer      The buffer to which the information should be
737   *                     appended.  It must not be <CODE>null</CODE>.
738   * @param  valueBytes  The value to append to the buffer.  It must not be
739   *                     <CODE>null</CODE>.
740   * @param isURL        Whether the provided value is an URL value or not.
741   * @param isBase64     Whether the provided value is a base 64 value or not.
742   */
743  public static void appendLDIFSeparatorAndValue(StringBuilder buffer,
744      ByteSequence valueBytes, boolean isURL, boolean isBase64)
745  {
746    ifNull(buffer, valueBytes);
747
748
749    // If the value is empty, then just append a single colon (the URL '<' if
750    // required) and a single space.
751    final boolean valueIsEmpty = valueBytes == null || valueBytes.length() == 0;
752    if (isURL)
753    {
754      buffer.append(":< ");
755      if (!valueIsEmpty)
756      {
757        buffer.append(valueBytes.toString());
758      }
759    }
760    else if (isBase64)
761    {
762      buffer.append(":: ");
763      if (!valueIsEmpty)
764      {
765        buffer.append(valueBytes.toString());
766      }
767    }
768    else if (needsBase64Encoding(valueBytes))
769    {
770      buffer.append(":: ");
771      buffer.append(Base64.encode(valueBytes));
772    }
773    else
774    {
775      buffer.append(": ");
776      if (!valueIsEmpty)
777      {
778        buffer.append(valueBytes.toString());
779      }
780    }
781  }
782
783
784
785  /**
786   * Writes the provided line to LDIF using the provided information.
787   *
788   * @param  line        The line of information to write.  It must not be
789   *                     <CODE>null</CODE>.
790   * @param  writer      The writer to which the data should be written.  It
791   *                     must not be <CODE>null</CODE>.
792   * @param  wrapLines   Indicates whether to wrap long lines.
793   * @param  wrapColumn  The column at which long lines should be wrapped.
794   *
795   * @throws  IOException  If a problem occurs while writing the information.
796   */
797  public static void writeLDIFLine(StringBuilder line, BufferedWriter writer,
798                                   boolean wrapLines, int wrapColumn)
799          throws IOException
800  {
801    ifNull(line, writer);
802
803    int length = line.length();
804    if (wrapLines && length > wrapColumn)
805    {
806      writer.write(line.substring(0, wrapColumn));
807      writer.newLine();
808
809      int pos = wrapColumn;
810      while (pos < length)
811      {
812        int writeLength = Math.min(wrapColumn-1, length-pos);
813        writer.write(' ');
814        writer.write(line.substring(pos, pos+writeLength));
815        writer.newLine();
816
817        pos += wrapColumn-1;
818      }
819    }
820    else
821    {
822      writer.write(line.toString());
823      writer.newLine();
824    }
825  }
826}