/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2006-2009 Sun Microsystems, Inc. * Portions Copyright 2013 ForgeRock AS. */ package org.opends.server.util; import java.io.BufferedWriter; import java.io.Closeable; import java.io.IOException; import java.util.Iterator; import java.util.List; import java.util.regex.Pattern; import java.util.Collection; import org.opends.messages.Message; import org.opends.server.tools.makeldif.TemplateEntry; import org.opends.server.types.*; import static org.opends.server.util.StaticUtils.*; import static org.opends.server.util.Validator.*; /** * This class provides a mechanism for writing entries in LDIF form to a file or * an output stream. */ @org.opends.server.types.PublicAPI( stability=org.opends.server.types.StabilityLevel.UNCOMMITTED, mayInstantiate=true, mayExtend=false, mayInvoke=true) public final class LDIFWriter implements Closeable { // FIXME -- Add support for generating a hash when writing the data. // FIXME -- Add support for signing the hash that is generated. // The writer to which the LDIF information will be written. private BufferedWriter writer; // The configuration to use for the export. private LDIFExportConfig exportConfig; // Regular expression used for splitting comments on line-breaks. private static final Pattern SPLIT_NEWLINE = Pattern.compile("\\r?\\n"); /** * Creates a new LDIF writer with the provided configuration. * * @param exportConfig The configuration to use for the export. It must not * be <CODE>null</CODE>. * * @throws IOException If a problem occurs while opening the writer. */ public LDIFWriter(LDIFExportConfig exportConfig) throws IOException { ensureNotNull(exportConfig); this.exportConfig = exportConfig; writer = exportConfig.getWriter(); } /** * Writes the provided comment to the LDIF file, optionally wrapping near the * specified column. Each line will be prefixed by the octothorpe (#) * character followed by a space. If the comment should be wrapped at a * specified column, then it will attempt to do so at the first whitespace * character at or before that column (so it will try not wrap in the middle * of a word). * <BR><BR> * This comment will be ignored by the * Directory Server's LDIF reader, as well as any other compliant LDIF parsing * software. * * @param comment The comment to be written. Any line breaks that it * contains will be honored, and potentially new line * breaks may be introduced by the wrapping process. It * must not be <CODE>null</CODE>. * @param wrapColumn The column at which long lines should be wrapped, or * -1 to indicate that no additional wrapping should be * added. This will override the wrap column setting * specified in the LDIF export configuration. * * @throws IOException If a problem occurs while attempting to write the * comment to the LDIF file. */ public void writeComment(Message comment, int wrapColumn) throws IOException { ensureNotNull(comment); // First, break up the comment into multiple lines to preserve the original // spacing that it contained. String[] lines = SPLIT_NEWLINE.split(comment); // Now iterate through the lines and write them out, prefixing and wrapping // them as necessary. for (String l : lines) { if (wrapColumn <= 0) { writer.write("# "); writer.write(l); writer.newLine(); } else { int breakColumn = wrapColumn - 2; if (l.length() <= breakColumn) { writer.write("# "); writer.write(l); writer.newLine(); } else { int startPos = 0; outerLoop: while (startPos < l.length()) { if ((startPos+breakColumn) >= l.length()) { writer.write("# "); writer.write(l.substring(startPos)); writer.newLine(); startPos = l.length(); } else { int endPos = startPos + breakColumn; int i=endPos - 1; while (i > startPos) { if (l.charAt(i) == ' ') { writer.write("# "); writer.write(l.substring(startPos, i)); writer.newLine(); startPos = i+1; continue outerLoop; } i--; } // If we've gotten here, then there are no spaces on the entire // line. If that happens, then we'll have to break in the middle // of a word. writer.write("# "); writer.write(l.substring(startPos, endPos)); writer.newLine(); startPos = endPos; } } } } } } /** * Iterates over each entry contained in the map and writes out the entry in * LDIF format. The main benefit of this method is that the entries can be * sorted by DN and output in sorted order. * * @param entries The Map containing the entries keyed by DN. * * @return <CODE>true</CODE>of all of the entries were * written out, <CODE>false</CODE>if it was not * because of the export configuration. * * @throws IOException If a problem occurs while writing the entry to LDIF. * * @throws LDIFException If a problem occurs while trying to determine * whether to include the entry in the export. */ public boolean writeEntries(Collection <Entry> entries) throws IOException, LDIFException { boolean ret=true; Iterator<Entry> i = entries.iterator(); while(ret && i.hasNext()) { ret=writeEntry(i.next()); } return ret; } /** * Writes the provided entry to LDIF. * * @param entry The entry to be written. It must not be <CODE>null</CODE>. * * @return <CODE>true</CODE> if the entry was actually written, or * <CODE>false</CODE> if it was not because of the export * configuration. * * @throws IOException If a problem occurs while writing the entry to LDIF. * * @throws LDIFException If a problem occurs while trying to determine * whether to include the entry in the export. */ public boolean writeEntry(Entry entry) throws IOException, LDIFException { ensureNotNull(entry); return entry.toLDIF(exportConfig); } /** * Writes the provided template entry to LDIF. * * @param templateEntry The template entry to be written. It must not be * <CODE>null</CODE>. * * @return <CODE>true</CODE> if the entry was actually written, or * <CODE>false</CODE> if it was not because of the export * configuration. * * @throws IOException If a problem occurs while writing the template entry * to LDIF. * * @throws LDIFException If a problem occurs while trying to determine * whether to include the template entry in the * export. */ public boolean writeTemplateEntry(TemplateEntry templateEntry) throws IOException, LDIFException { ensureNotNull(templateEntry); // Delegate to TemplateEntry.toLDIF(...) return templateEntry.toLDIF(exportConfig); } /** * Writes a change record entry for the provided change record. * * @param changeRecord The change record entry to be written. * * @throws IOException If a problem occurs while writing the change record. */ public void writeChangeRecord(ChangeRecordEntry changeRecord) throws IOException { ensureNotNull(changeRecord); // Get the information necessary to write the LDIF. BufferedWriter writer = exportConfig.getWriter(); int wrapColumn = exportConfig.getWrapColumn(); boolean wrapLines = (wrapColumn > 1); // First, write the DN. StringBuilder dnLine = new StringBuilder(); dnLine.append("dn"); appendLDIFSeparatorAndValue(dnLine, ByteString.valueOf(changeRecord.getDN().toString())); writeLDIFLine(dnLine, writer, wrapLines, wrapColumn); // Figure out what type of change it is and act accordingly. if (changeRecord instanceof AddChangeRecordEntry) { StringBuilder changeTypeLine = new StringBuilder("changetype: add"); writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn); AddChangeRecordEntry addRecord = (AddChangeRecordEntry) changeRecord; for (Attribute a : addRecord.getAttributes()) { for (AttributeValue v : a) { StringBuilder line = new StringBuilder(); line.append(a.getNameWithOptions()); appendLDIFSeparatorAndValue(line, v.getValue()); writeLDIFLine(line, writer, wrapLines, wrapColumn); } } } else if (changeRecord instanceof DeleteChangeRecordEntry) { StringBuilder changeTypeLine = new StringBuilder("changetype: delete"); writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn); } else if (changeRecord instanceof ModifyChangeRecordEntry) { StringBuilder changeTypeLine = new StringBuilder("changetype: modify"); writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn); ModifyChangeRecordEntry modifyRecord = (ModifyChangeRecordEntry) changeRecord; List<RawModification> mods = modifyRecord.getModifications(); Iterator<RawModification> iterator = mods.iterator(); while (iterator.hasNext()) { RawModification m = iterator.next(); RawAttribute a = m.getAttribute(); String attrName = a.getAttributeType(); StringBuilder modTypeLine = new StringBuilder(); modTypeLine.append(m.getModificationType().getLDIFName()); modTypeLine.append(": "); modTypeLine.append(attrName); writeLDIFLine(modTypeLine, writer, wrapLines, wrapColumn); for (ByteString s : a.getValues()) { StringBuilder valueLine = new StringBuilder(); String stringValue = s.toString(); valueLine.append(attrName); if (needsBase64Encoding(stringValue)) { valueLine.append(":: "); valueLine.append(Base64.encode(s)); } else { valueLine.append(": "); valueLine.append(stringValue); } writeLDIFLine(valueLine, writer, wrapLines, wrapColumn); } if (iterator.hasNext()) { StringBuilder dashLine = new StringBuilder("-"); writeLDIFLine(dashLine, writer, wrapLines, wrapColumn); } } } else if (changeRecord instanceof ModifyDNChangeRecordEntry) { StringBuilder changeTypeLine = new StringBuilder("changetype: moddn"); writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn); ModifyDNChangeRecordEntry modifyDNRecord = (ModifyDNChangeRecordEntry) changeRecord; StringBuilder newRDNLine = new StringBuilder(); newRDNLine.append("newrdn: "); modifyDNRecord.getNewRDN().toString(newRDNLine); writeLDIFLine(newRDNLine, writer, wrapLines, wrapColumn); StringBuilder deleteOldRDNLine = new StringBuilder(); deleteOldRDNLine.append("deleteoldrdn: "); if (modifyDNRecord.deleteOldRDN()) { deleteOldRDNLine.append("1"); } else { deleteOldRDNLine.append("0"); } writeLDIFLine(deleteOldRDNLine, writer, wrapLines, wrapColumn); DN newSuperiorDN = modifyDNRecord.getNewSuperiorDN(); if (newSuperiorDN != null) { StringBuilder newSuperiorLine = new StringBuilder(); newSuperiorLine.append("newsuperior: "); newSuperiorDN.toString(newSuperiorLine); writeLDIFLine(newSuperiorLine, writer, wrapLines, wrapColumn); } } // Make sure there is a blank line after the entry. writer.newLine(); } /** * Writes an add change record for the provided entry. No filtering will be * performed for this entry, nor will any export plugins be invoked. Further, * only the user attributes will be included. * * @param entry The entry to include in the add change record. It must not * be <CODE>null</CODE>. * * @throws IOException If a problem occurs while writing the add record. */ public void writeAddChangeRecord(Entry entry) throws IOException { ensureNotNull(entry); // Get the information necessary to write the LDIF. BufferedWriter writer = exportConfig.getWriter(); int wrapColumn = exportConfig.getWrapColumn(); boolean wrapLines = (wrapColumn > 1); // First, write the DN. StringBuilder dnLine = new StringBuilder(); dnLine.append("dn"); appendLDIFSeparatorAndValue(dnLine, ByteString.valueOf(entry.getDN().toString())); writeLDIFLine(dnLine, writer, wrapLines, wrapColumn); // Next, the changetype. StringBuilder changeTypeLine = new StringBuilder("changetype: add"); writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn); // Now the objectclasses. for (String s : entry.getObjectClasses().values()) { StringBuilder ocLine = new StringBuilder(); ocLine.append("objectClass: "); ocLine.append(s); writeLDIFLine(ocLine, writer, wrapLines, wrapColumn); } // Finally, the set of user attributes. for (AttributeType attrType : entry.getUserAttributes().keySet()) { List<Attribute> attrList = entry.getUserAttribute(attrType); for (Attribute a : attrList) { StringBuilder attrName = new StringBuilder(a.getName()); for (String o : a.getOptions()) { attrName.append(";"); attrName.append(o); } for (AttributeValue v : a) { StringBuilder attrLine = new StringBuilder(); attrLine.append(attrName); appendLDIFSeparatorAndValue(attrLine, v.getValue()); writeLDIFLine(attrLine, writer, wrapLines, wrapColumn); } } } // Make sure there is a blank line after the entry. writer.newLine(); } /** * Writes a delete change record for the provided entry, optionally including * a comment with the full entry contents. No filtering will be performed for * this entry, nor will any export plugins be invoked. Further, only the user * attributes will be included. * * @param entry The entry to include in the delete change record. It * must not be <CODE>null</CODE>. * @param commentEntry Indicates whether to include a comment with the * contents of the entry. * * @throws IOException If a problem occurs while writing the delete record. */ public void writeDeleteChangeRecord(Entry entry, boolean commentEntry) throws IOException { ensureNotNull(entry); // Get the information necessary to write the LDIF. BufferedWriter writer = exportConfig.getWriter(); int wrapColumn = exportConfig.getWrapColumn(); boolean wrapLines = (wrapColumn > 1); // Add the DN and changetype lines. StringBuilder dnLine = new StringBuilder(); dnLine.append("dn"); appendLDIFSeparatorAndValue(dnLine, ByteString.valueOf(entry.getDN().toString())); writeLDIFLine(dnLine, writer, wrapLines, wrapColumn); StringBuilder changeTypeLine = new StringBuilder("changetype: delete"); writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn); // If we should include a comment with the rest of the entry contents, then // do so now. if (commentEntry) { // Write the objectclasses. for (String s : entry.getObjectClasses().values()) { StringBuilder ocLine = new StringBuilder(); ocLine.append("# objectClass: "); ocLine.append(s); writeLDIFLine(ocLine, writer, wrapLines, wrapColumn); } // Write the set of user attributes. for (AttributeType attrType : entry.getUserAttributes().keySet()) { List<Attribute> attrList = entry.getUserAttribute(attrType); for (Attribute a : attrList) { StringBuilder attrName = new StringBuilder(); attrName.append("# "); attrName.append(a.getName()); for (String o : a.getOptions()) { attrName.append(";"); attrName.append(o); } for (AttributeValue v : a) { StringBuilder attrLine = new StringBuilder(); attrLine.append(attrName); appendLDIFSeparatorAndValue(attrLine, v.getValue()); writeLDIFLine(attrLine, writer, wrapLines, wrapColumn); } } } } // Make sure there is a blank line after the entry. writer.newLine(); } /** * Writes a modify change record with the provided information. No filtering * will be performed, nor will any export plugins be invoked. * * @param dn The DN of the entry being modified. It must not be * <CODE>null</CODE>. * @param modifications The set of modifications to include in the change * record. It must not be <CODE>null</CODE>. * * @throws IOException If a problem occurs while writing the modify record. */ public void writeModifyChangeRecord(DN dn, List<Modification> modifications) throws IOException { ensureNotNull(dn, modifications); // If there aren't any modifications, then there's nothing to do. if (modifications.isEmpty()) { return; } // Get the information necessary to write the LDIF. BufferedWriter writer = exportConfig.getWriter(); int wrapColumn = exportConfig.getWrapColumn(); boolean wrapLines = (wrapColumn > 1); // Write the DN and changetype. StringBuilder dnLine = new StringBuilder(); dnLine.append("dn"); appendLDIFSeparatorAndValue(dnLine, ByteString.valueOf(dn.toString())); writeLDIFLine(dnLine, writer, wrapLines, wrapColumn); StringBuilder changeTypeLine = new StringBuilder("changetype: modify"); writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn); // Iterate through the modifications and write them to the LDIF. Iterator<Modification> iterator = modifications.iterator(); while (iterator.hasNext()) { Modification m = iterator.next(); Attribute a = m.getAttribute(); StringBuilder nameBuffer = new StringBuilder(a.getName()); for (String o : a.getOptions()) { nameBuffer.append(";"); nameBuffer.append(o); } String name = nameBuffer.toString(); StringBuilder modTypeLine = new StringBuilder(); switch (m.getModificationType()) { case ADD: modTypeLine.append("add: "); modTypeLine.append(name); break; case DELETE: modTypeLine.append("delete: "); modTypeLine.append(name); break; case REPLACE: modTypeLine.append("replace: "); modTypeLine.append(name); break; case INCREMENT: modTypeLine.append("increment: "); modTypeLine.append(name); break; default: // We have no idea what the changetype is, so we can't write anything. continue; } writeLDIFLine(modTypeLine, writer, wrapLines, wrapColumn); for (AttributeValue v : a) { StringBuilder valueLine = new StringBuilder(); valueLine.append(name); appendLDIFSeparatorAndValue(valueLine, v.getValue()); writeLDIFLine(valueLine, writer, wrapLines, wrapColumn); } // If this is the last modification, then append blank line. Otherwise // write a line with just a dash. if (iterator.hasNext()) { writer.write("-"); writer.newLine(); } else { writer.newLine(); } } } /** * Writes a modify DN change record with the provided information. No * filtering will be performed, nor will any export plugins be invoked. * * @param dn The DN of the entry before the rename. It must not * be <CODE>null</CODE>. * @param newRDN The new RDN for the entry. It must not be * <CODE>null</CODE>. * @param deleteOldRDN Indicates whether the old RDN value should be removed * from the entry. * @param newSuperior The new superior DN for the entry, or * <CODE>null</CODE> if the entry will stay below the * same parent. * * @throws IOException If a problem occurs while writing the modify record. */ public void writeModifyDNChangeRecord(DN dn, RDN newRDN, boolean deleteOldRDN, DN newSuperior) throws IOException { ensureNotNull(dn, newRDN); // Get the information necessary to write the LDIF. BufferedWriter writer = exportConfig.getWriter(); int wrapColumn = exportConfig.getWrapColumn(); boolean wrapLines = (wrapColumn > 1); // Write the current DN. StringBuilder dnLine = new StringBuilder(); dnLine.append("dn"); appendLDIFSeparatorAndValue(dnLine, ByteString.valueOf(dn.toString())); writeLDIFLine(dnLine, writer, wrapLines, wrapColumn); // Write the changetype. Some older tools may not support the "moddn" // changetype, so only use it if a newSuperior element has been provided, // but use modrdn elsewhere. if (newSuperior == null) { StringBuilder changeTypeLine = new StringBuilder("changetype: modrdn"); writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn); } else { StringBuilder changeTypeLine = new StringBuilder("changetype: moddn"); writeLDIFLine(changeTypeLine, writer, wrapLines, wrapColumn); } // Write the newRDN element. StringBuilder rdnLine = new StringBuilder(); rdnLine.append("newrdn"); appendLDIFSeparatorAndValue(rdnLine, ByteString.valueOf(newRDN.toString())); writeLDIFLine(rdnLine, writer, wrapLines, wrapColumn); // Write the deleteOldRDN element. StringBuilder deleteOldRDNLine = new StringBuilder(); deleteOldRDNLine.append("deleteoldrdn: "); deleteOldRDNLine.append(deleteOldRDN ? "1" : "0"); writeLDIFLine(deleteOldRDNLine, writer, wrapLines, wrapColumn); if (newSuperior != null) { StringBuilder newSuperiorLine = new StringBuilder(); newSuperiorLine.append("newsuperior"); appendLDIFSeparatorAndValue(newSuperiorLine, ByteString.valueOf(newSuperior.toString())); writeLDIFLine(newSuperiorLine, writer, wrapLines, wrapColumn); } // Make sure there is a blank line after the entry. writer.newLine(); } /** * Flushes the data written to the output stream or underlying file. * * @throws IOException If a problem occurs while flushing the output. */ public void flush() throws IOException { writer.flush(); } /** * Closes the LDIF writer and the underlying output stream or file. * * @throws IOException If a problem occurs while closing the writer. */ public void close() throws IOException { writer.flush(); writer.close(); } /** * Appends an LDIF separator and properly-encoded form of the given * value to the provided buffer. If the value is safe to include * as-is, then a single colon, a single space, space, and the * provided value will be appended. Otherwise, two colons, a single * space, and a base64-encoded form of the value will be appended. * * @param buffer The buffer to which the information should be * appended. It must not be <CODE>null</CODE>. * @param valueBytes The value to append to the buffer. It must not be * <CODE>null</CODE>. */ public static void appendLDIFSeparatorAndValue(StringBuilder buffer, ByteSequence valueBytes) { appendLDIFSeparatorAndValue(buffer, valueBytes, false, false); } /** * Appends an LDIF separator and properly-encoded form of the given * value to the provided buffer. If the value is safe to include * as-is, then a single colon, a single space, space, and the * provided value will be appended. Otherwise, two colons, a single * space, and a base64-encoded form of the value will be appended. * @param buffer The buffer to which the information should be * appended. It must not be <CODE>null</CODE>. * @param valueBytes The value to append to the buffer. It must not be * <CODE>null</CODE>. * @param isURL Whether the provided value is an URL value or not. * @param isBase64 Whether the provided value is a base 64 value or not. */ public static void appendLDIFSeparatorAndValue(StringBuilder buffer, ByteSequence valueBytes, boolean isURL, boolean isBase64) { ensureNotNull(buffer, valueBytes); // If the value is empty, then just append a single colon (the URL '<' if // required) and a single space. if ((valueBytes == null) || (valueBytes.length() == 0)) { if (isURL) { buffer.append(":< "); } else if (isBase64) { buffer.append(":: "); } else { buffer.append(": "); } return; } if (isURL) { buffer.append(":< "); buffer.append(valueBytes.toString()); } else if (isBase64) { buffer.append(":: "); buffer.append(valueBytes.toString()); } else if (needsBase64Encoding(valueBytes)) { buffer.append(":: "); buffer.append(Base64.encode(valueBytes)); } else { buffer.append(": "); buffer.append(valueBytes.toString()); } } /** * Writes the provided line to LDIF using the provided information. * * @param line The line of information to write. It must not be * <CODE>null</CODE>. * @param writer The writer to which the data should be written. It * must not be <CODE>null</CODE>. * @param wrapLines Indicates whether to wrap long lines. * @param wrapColumn The column at which long lines should be wrapped. * * @throws IOException If a problem occurs while writing the information. */ public static void writeLDIFLine(StringBuilder line, BufferedWriter writer, boolean wrapLines, int wrapColumn) throws IOException { ensureNotNull(line, writer); int length = line.length(); if (wrapLines && (length > wrapColumn)) { writer.write(line.substring(0, wrapColumn)); writer.newLine(); int pos = wrapColumn; while (pos < length) { int writeLength = Math.min(wrapColumn-1, length-pos); writer.write(' '); writer.write(line.substring(pos, pos+writeLength)); writer.newLine(); pos += wrapColumn-1; } } else { writer.write(line.toString()); writer.newLine(); } } }