All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.unboundid.ldif.LDIFWriter Maven / Gradle / Ivy

/*
 * Copyright 2007-2017 UnboundID Corp.
 * All Rights Reserved.
 */
/*
 * Copyright (C) 2008-2017 UnboundID Corp.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPLv2 only)
 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see .
 */
package com.unboundid.ldif;



import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.io.BufferedOutputStream;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

import com.unboundid.asn1.ASN1OctetString;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.util.Base64;
import com.unboundid.util.LDAPSDKThreadFactory;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;
import com.unboundid.util.ByteStringBuffer;
import com.unboundid.util.parallel.ParallelProcessor;
import com.unboundid.util.parallel.Result;
import com.unboundid.util.parallel.Processor;

import static com.unboundid.util.Debug.*;
import static com.unboundid.util.StaticUtils.*;
import static com.unboundid.util.Validator.*;



/**
 * This class provides an LDIF writer, which can be used to write entries and
 * change records in the LDAP Data Interchange Format as per
 * RFC 2849.
 * 

*

Example

* The following example performs a search to find all users in the "Sales" * department and then writes their entries to an LDIF file: *
 * // Perform a search to find all users who are members of the sales
 * // department.
 * SearchRequest searchRequest = new SearchRequest("dc=example,dc=com",
 *      SearchScope.SUB, Filter.createEqualityFilter("ou", "Sales"));
 * SearchResult searchResult;
 * try
 * {
 *   searchResult = connection.search(searchRequest);
 * }
 * catch (LDAPSearchException lse)
 * {
 *   searchResult = lse.getSearchResult();
 * }
 * LDAPTestUtils.assertResultCodeEquals(searchResult, ResultCode.SUCCESS);
 *
 * // Write all of the matching entries to LDIF.
 * int entriesWritten = 0;
 * LDIFWriter ldifWriter = new LDIFWriter(pathToLDIF);
 * for (SearchResultEntry entry : searchResult.getSearchEntries())
 * {
 *   ldifWriter.writeEntry(entry);
 *   entriesWritten++;
 * }
 *
 * ldifWriter.close();
 * 
*/ @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) public final class LDIFWriter implements Closeable { /** * Indicates whether LDIF records should include a comment above each * base64-encoded value that attempts to provide an unencoded representation * of that value (with special characters escaped). */ private static volatile boolean commentAboutBase64EncodedValues = false; /** * The bytes that comprise the LDIF version header. */ private static final byte[] VERSION_1_HEADER_BYTES = getBytes("version: 1" + EOL); /** * The default buffer size (128KB) that will be used when writing LDIF data * to the appropriate destination. */ private static final int DEFAULT_BUFFER_SIZE = 128 * 1024; // The writer that will be used to actually write the data. private final BufferedOutputStream writer; // The byte string buffer that will be used to convert LDIF records to LDIF. // It will only be used when operating synchronously. private final ByteStringBuffer buffer; // The translator to use for change records to be written, if any. private final LDIFWriterChangeRecordTranslator changeRecordTranslator; // The translator to use for entries to be written, if any. private final LDIFWriterEntryTranslator entryTranslator; // The column at which to wrap long lines. private int wrapColumn = 0; // A pre-computed value that is two less than the wrap column. private int wrapColumnMinusTwo = -2; // non-null if this writer was configured to use multiple threads when // writing batches of entries. private final ParallelProcessor toLdifBytesInvoker; /** * Creates a new LDIF writer that will write entries to the provided file. * * @param path The path to the LDIF file to be written. It must not be * {@code null}. * * @throws IOException If a problem occurs while opening the provided file * for writing. */ public LDIFWriter(final String path) throws IOException { this(new FileOutputStream(path)); } /** * Creates a new LDIF writer that will write entries to the provided file. * * @param file The LDIF file to be written. It must not be {@code null}. * * @throws IOException If a problem occurs while opening the provided file * for writing. */ public LDIFWriter(final File file) throws IOException { this(new FileOutputStream(file)); } /** * Creates a new LDIF writer that will write entries to the provided output * stream. * * @param outputStream The output stream to which the data is to be written. * It must not be {@code null}. */ public LDIFWriter(final OutputStream outputStream) { this(outputStream, 0); } /** * Creates a new LDIF writer that will write entries to the provided output * stream optionally using parallelThreads when writing batches of LDIF * records. * * @param outputStream The output stream to which the data is to be * written. It must not be {@code null}. * @param parallelThreads If this value is greater than zero, then the * specified number of threads will be used to * encode entries before writing them to the output * for the {@code writeLDIFRecords(List)} method. * Note this is the only output method that will * use multiple threads. * This should only be set to greater than zero when * performance analysis has demonstrated that writing * the LDIF is a bottleneck. The default * synchronous processing is normally fast enough. * There is no benefit in passing in a value * greater than the number of processors in the * system. A value of zero implies the * default behavior of reading and parsing LDIF * records synchronously when one of the read * methods is called. */ public LDIFWriter(final OutputStream outputStream, final int parallelThreads) { this(outputStream, parallelThreads, null); } /** * Creates a new LDIF writer that will write entries to the provided output * stream optionally using parallelThreads when writing batches of LDIF * records. * * @param outputStream The output stream to which the data is to be * written. It must not be {@code null}. * @param parallelThreads If this value is greater than zero, then the * specified number of threads will be used to * encode entries before writing them to the output * for the {@code writeLDIFRecords(List)} method. * Note this is the only output method that will * use multiple threads. * This should only be set to greater than zero when * performance analysis has demonstrated that writing * the LDIF is a bottleneck. The default * synchronous processing is normally fast enough. * There is no benefit in passing in a value * greater than the number of processors in the * system. A value of zero implies the * default behavior of reading and parsing LDIF * records synchronously when one of the read * methods is called. * @param entryTranslator An optional translator that will be used to alter * entries before they are actually written. This * may be {@code null} if no translator is needed. */ public LDIFWriter(final OutputStream outputStream, final int parallelThreads, final LDIFWriterEntryTranslator entryTranslator) { this(outputStream, parallelThreads, entryTranslator, null); } /** * Creates a new LDIF writer that will write entries to the provided output * stream optionally using parallelThreads when writing batches of LDIF * records. * * @param outputStream The output stream to which the data is to * be written. It must not be {@code null}. * @param parallelThreads If this value is greater than zero, then * the specified number of threads will be * used to encode entries before writing them * to the output for the * {@code writeLDIFRecords(List)} method. * Note this is the only output method that * will use multiple threads. This should * only be set to greater than zero when * performance analysis has demonstrated that * writing the LDIF is a bottleneck. The * default synchronous processing is normally * fast enough. There is no benefit in * passing in a value greater than the number * of processors in the system. A value of * zero implies the default behavior of * reading and parsing LDIF records * synchronously when one of the read methods * is called. * @param entryTranslator An optional translator that will be used to * alter entries before they are actually * written. This may be {@code null} if no * translator is needed. * @param changeRecordTranslator An optional translator that will be used to * alter change records before they are * actually written. This may be {@code null} * if no translator is needed. */ public LDIFWriter(final OutputStream outputStream, final int parallelThreads, final LDIFWriterEntryTranslator entryTranslator, final LDIFWriterChangeRecordTranslator changeRecordTranslator) { ensureNotNull(outputStream); ensureTrue(parallelThreads >= 0, "LDIFWriter.parallelThreads must not be negative."); this.entryTranslator = entryTranslator; this.changeRecordTranslator = changeRecordTranslator; buffer = new ByteStringBuffer(); if (outputStream instanceof BufferedOutputStream) { writer = (BufferedOutputStream) outputStream; } else { writer = new BufferedOutputStream(outputStream, DEFAULT_BUFFER_SIZE); } if (parallelThreads == 0) { toLdifBytesInvoker = null; } else { final LDAPSDKThreadFactory threadFactory = new LDAPSDKThreadFactory("LDIFWriter Worker", true, null); toLdifBytesInvoker = new ParallelProcessor( new Processor() { public ByteStringBuffer process(final LDIFRecord input) throws IOException { final LDIFRecord r; if ((entryTranslator != null) && (input instanceof Entry)) { r = entryTranslator.translateEntryToWrite((Entry) input); if (r == null) { return null; } } else if ((changeRecordTranslator != null) && (input instanceof LDIFChangeRecord)) { r = changeRecordTranslator.translateChangeRecordToWrite( (LDIFChangeRecord) input); if (r == null) { return null; } } else { r = input; } final ByteStringBuffer b = new ByteStringBuffer(200); r.toLDIF(b, wrapColumn); return b; } }, threadFactory, parallelThreads, 5); } } /** * Flushes the output stream used by this LDIF writer to ensure any buffered * data is written out. * * @throws IOException If a problem occurs while attempting to flush the * output stream. */ public void flush() throws IOException { writer.flush(); } /** * Closes this LDIF writer and the underlying LDIF target. * * @throws IOException If a problem occurs while closing the underlying LDIF * target. */ public void close() throws IOException { try { if (toLdifBytesInvoker != null) { try { toLdifBytesInvoker.shutdown(); } catch (InterruptedException e) { debugException(e); Thread.currentThread().interrupt(); } } } finally { writer.close(); } } /** * Retrieves the column at which to wrap long lines. * * @return The column at which to wrap long lines, or zero to indicate that * long lines should not be wrapped. */ public int getWrapColumn() { return wrapColumn; } /** * Specifies the column at which to wrap long lines. A value of zero * indicates that long lines should not be wrapped. * * @param wrapColumn The column at which to wrap long lines. */ public void setWrapColumn(final int wrapColumn) { this.wrapColumn = wrapColumn; wrapColumnMinusTwo = wrapColumn - 2; } /** * Indicates whether the LDIF writer should generate comments that attempt to * provide unencoded representations (with special characters escaped) of any * base64-encoded values in entries and change records that are written by * this writer. * * @return {@code true} if the LDIF writer should generate comments that * attempt to provide unencoded representations of any base64-encoded * values, or {@code false} if not. */ public static boolean commentAboutBase64EncodedValues() { return commentAboutBase64EncodedValues; } /** * Specifies whether the LDIF writer should generate comments that attempt to * provide unencoded representations (with special characters escaped) of any * base64-encoded values in entries and change records that are written by * this writer. * * @param commentAboutBase64EncodedValues Indicates whether the LDIF writer * should generate comments that * attempt to provide unencoded * representations (with special * characters escaped) of any * base64-encoded values in entries * and change records that are * written by this writer. */ public static void setCommentAboutBase64EncodedValues( final boolean commentAboutBase64EncodedValues) { LDIFWriter.commentAboutBase64EncodedValues = commentAboutBase64EncodedValues; } /** * Writes the LDIF version header (i.e.,"version: 1"). If a version header * is to be added to the LDIF content, it should be done before any entries or * change records have been written. * * @throws IOException If a problem occurs while writing the version header. */ public void writeVersionHeader() throws IOException { writer.write(VERSION_1_HEADER_BYTES); } /** * Writes the provided entry in LDIF form. * * @param entry The entry to be written. It must not be {@code null}. * * @throws IOException If a problem occurs while writing the LDIF data. */ public void writeEntry(final Entry entry) throws IOException { writeEntry(entry, null); } /** * Writes the provided entry in LDIF form, preceded by the provided comment. * * @param entry The entry to be written in LDIF form. It must not be * {@code null}. * @param comment The comment to be written before the entry. It may be * {@code null} if no comment is to be written. * * @throws IOException If a problem occurs while writing the LDIF data. */ public void writeEntry(final Entry entry, final String comment) throws IOException { ensureNotNull(entry); final Entry e; if (entryTranslator == null) { e = entry; } else { e = entryTranslator.translateEntryToWrite(entry); if (e == null) { return; } } if (comment != null) { writeComment(comment, false, false); } debugLDIFWrite(e); writeLDIF(e); } /** * Writes the provided change record in LDIF form. * * @param changeRecord The change record to be written. It must not be * {@code null}. * * @throws IOException If a problem occurs while writing the LDIF data. */ public void writeChangeRecord(final LDIFChangeRecord changeRecord) throws IOException { writeChangeRecord(changeRecord, null); } /** * Writes the provided change record in LDIF form, preceded by the provided * comment. * * @param changeRecord The change record to be written. It must not be * {@code null}. * @param comment The comment to be written before the entry. It may * be {@code null} if no comment is to be written. * * @throws IOException If a problem occurs while writing the LDIF data. */ public void writeChangeRecord(final LDIFChangeRecord changeRecord, final String comment) throws IOException { ensureNotNull(changeRecord); final LDIFChangeRecord r; if (changeRecordTranslator == null) { r = changeRecord; } else { r = changeRecordTranslator.translateChangeRecordToWrite(changeRecord); if (r == null) { return; } } if (comment != null) { writeComment(comment, false, false); } debugLDIFWrite(r); writeLDIF(r); } /** * Writes the provided record in LDIF form. * * @param record The LDIF record to be written. It must not be * {@code null}. * * @throws IOException If a problem occurs while writing the LDIF data. */ public void writeLDIFRecord(final LDIFRecord record) throws IOException { writeLDIFRecord(record, null); } /** * Writes the provided record in LDIF form, preceded by the provided comment. * * @param record The LDIF record to be written. It must not be * {@code null}. * @param comment The comment to be written before the LDIF record. It may * be {@code null} if no comment is to be written. * * @throws IOException If a problem occurs while writing the LDIF data. */ public void writeLDIFRecord(final LDIFRecord record, final String comment) throws IOException { ensureNotNull(record); final LDIFRecord r; if ((entryTranslator != null) && (record instanceof Entry)) { r = entryTranslator.translateEntryToWrite((Entry) record); if (r == null) { return; } } else if ((changeRecordTranslator != null) && (record instanceof LDIFChangeRecord)) { r = changeRecordTranslator.translateChangeRecordToWrite( (LDIFChangeRecord) record); if (r == null) { return; } } else { r = record; } debugLDIFWrite(r); if (comment != null) { writeComment(comment, false, false); } writeLDIF(r); } /** * Writes the provided list of LDIF records (most likely Entries) to the * output. If this LDIFWriter was constructed without any parallel * output threads, then this behaves identically to calling * {@code writeLDIFRecord()} sequentially for each item in the list. * If this LDIFWriter was constructed to write records in parallel, then * the configured number of threads are used to convert the records to raw * bytes, which are sequentially written to the input file. This can speed up * the total time to write a large set of records. Either way, the output * records are guaranteed to be written in the order they appear in the list. * * @param ldifRecords The LDIF records (most likely entries) to write to the * output. * * @throws IOException If a problem occurs while writing the LDIF data. * * @throws InterruptedException If this thread is interrupted while waiting * for the records to be written to the output. */ public void writeLDIFRecords(final List ldifRecords) throws IOException, InterruptedException { if (toLdifBytesInvoker == null) { for (final LDIFRecord ldifRecord : ldifRecords) { writeLDIFRecord(ldifRecord); } } else { final List> results = toLdifBytesInvoker.processAll(ldifRecords); for (final Result result: results) { rethrow(result.getFailureCause()); final ByteStringBuffer encodedBytes = result.getOutput(); if (encodedBytes != null) { encodedBytes.write(writer); writer.write(EOL_BYTES); } } } } /** * Writes the provided comment to the LDIF target, wrapping long lines as * necessary. * * @param comment The comment to be written to the LDIF target. It must * not be {@code null}. * @param spaceBefore Indicates whether to insert a blank line before the * comment. * @param spaceAfter Indicates whether to insert a blank line after the * comment. * * @throws IOException If a problem occurs while writing the LDIF data. */ public void writeComment(final String comment, final boolean spaceBefore, final boolean spaceAfter) throws IOException { ensureNotNull(comment); if (spaceBefore) { writer.write(EOL_BYTES); } // // Check for a newline explicitly to avoid the overhead of the regex // for the common case of a single-line comment. // if (comment.indexOf('\n') < 0) { writeSingleLineComment(comment); } else { // // Split on blank lines and wrap each line individually. // final String[] lines = comment.split("\\r?\\n"); for (final String line: lines) { writeSingleLineComment(line); } } if (spaceAfter) { writer.write(EOL_BYTES); } } /** * Writes the provided comment to the LDIF target, wrapping long lines as * necessary. * * @param comment The comment to be written to the LDIF target. It must * not be {@code null}, and it must not include any line * breaks. * * @throws IOException If a problem occurs while writing the LDIF data. */ private void writeSingleLineComment(final String comment) throws IOException { // We will always wrap comments, even if we won't wrap LDIF entries. If // there is a wrap column set, then use it. Otherwise use the terminal // width and back off two characters for the "# " at the beginning. final int commentWrapMinusTwo; if (wrapColumn <= 0) { commentWrapMinusTwo = TERMINAL_WIDTH_COLUMNS - 3; } else { commentWrapMinusTwo = wrapColumnMinusTwo; } buffer.clear(); final int length = comment.length(); if (length <= commentWrapMinusTwo) { buffer.append("# "); buffer.append(comment); buffer.append(EOL_BYTES); } else { int minPos = 0; while (minPos < length) { if ((length - minPos) <= commentWrapMinusTwo) { buffer.append("# "); buffer.append(comment.substring(minPos)); buffer.append(EOL_BYTES); break; } // First, adjust the position until we find a space. Go backwards if // possible, but if we can't find one there then go forward. boolean spaceFound = false; final int pos = minPos + commentWrapMinusTwo; int spacePos = pos; while (spacePos > minPos) { if (comment.charAt(spacePos) == ' ') { spaceFound = true; break; } spacePos--; } if (! spaceFound) { spacePos = pos + 1; while (spacePos < length) { if (comment.charAt(spacePos) == ' ') { spaceFound = true; break; } spacePos++; } if (! spaceFound) { // There are no spaces at all in the remainder of the comment, so // we'll just write the remainder of it all at once. buffer.append("# "); buffer.append(comment.substring(minPos)); buffer.append(EOL_BYTES); break; } } // We have a space, so we'll write up to the space position and then // start up after the next space. buffer.append("# "); buffer.append(comment.substring(minPos, spacePos)); buffer.append(EOL_BYTES); minPos = spacePos + 1; while ((minPos < length) && (comment.charAt(minPos) == ' ')) { minPos++; } } } buffer.write(writer); } /** * Writes the provided record to the LDIF target, wrapping long lines as * necessary. * * @param record The LDIF record to be written. * * @throws IOException If a problem occurs while writing the LDIF data. */ private void writeLDIF(final LDIFRecord record) throws IOException { buffer.clear(); record.toLDIF(buffer, wrapColumn); buffer.append(EOL_BYTES); buffer.write(writer); } /** * Performs any appropriate wrapping for the provided set of LDIF lines. * * @param wrapColumn The column at which to wrap long lines. A value that * is less than or equal to two indicates that no * wrapping should be performed. * @param ldifLines The set of lines that make up the LDIF data to be * wrapped. * * @return A new list of lines that have been wrapped as appropriate. */ public static List wrapLines(final int wrapColumn, final String... ldifLines) { return wrapLines(wrapColumn, Arrays.asList(ldifLines)); } /** * Performs any appropriate wrapping for the provided set of LDIF lines. * * @param wrapColumn The column at which to wrap long lines. A value that * is less than or equal to two indicates that no * wrapping should be performed. * @param ldifLines The set of lines that make up the LDIF data to be * wrapped. * * @return A new list of lines that have been wrapped as appropriate. */ public static List wrapLines(final int wrapColumn, final List ldifLines) { if (wrapColumn <= 2) { return new ArrayList(ldifLines); } final ArrayList newLines = new ArrayList(ldifLines.size()); for (final String s : ldifLines) { final int length = s.length(); if (length <= wrapColumn) { newLines.add(s); continue; } newLines.add(s.substring(0, wrapColumn)); int pos = wrapColumn; while (pos < length) { if ((length - pos + 1) <= wrapColumn) { newLines.add(' ' + s.substring(pos)); break; } else { newLines.add(' ' + s.substring(pos, (pos+wrapColumn-1))); pos += wrapColumn - 1; } } } return newLines; } /** * Creates a string consisting of the provided attribute name followed by * either a single colon and the string representation of the provided value, * or two colons and the base64-encoded representation of the provided value. * * @param name The name for the attribute. * @param value The value for the attribute. * * @return A string consisting of the provided attribute name followed by * either a single colon and the string representation of the * provided value, or two colons and the base64-encoded * representation of the provided value. */ public static String encodeNameAndValue(final String name, final ASN1OctetString value) { final StringBuilder buffer = new StringBuilder(); encodeNameAndValue(name, value, buffer); return buffer.toString(); } /** * Appends a string to the provided buffer consisting of the provided * attribute name followed by either a single colon and the string * representation of the provided value, or two colons and the base64-encoded * representation of the provided value. * * @param name The name for the attribute. * @param value The value for the attribute. * @param buffer The buffer to which the name and value are to be written. */ public static void encodeNameAndValue(final String name, final ASN1OctetString value, final StringBuilder buffer) { encodeNameAndValue(name, value, buffer, 0); } /** * Appends a string to the provided buffer consisting of the provided * attribute name followed by either a single colon and the string * representation of the provided value, or two colons and the base64-encoded * representation of the provided value. * * @param name The name for the attribute. * @param value The value for the attribute. * @param buffer The buffer to which the name and value are to be * written. * @param wrapColumn The column at which to wrap long lines. A value that * is less than or equal to two indicates that no * wrapping should be performed. */ public static void encodeNameAndValue(final String name, final ASN1OctetString value, final StringBuilder buffer, final int wrapColumn) { final int bufferStartPos = buffer.length(); final byte[] valueBytes = value.getValue(); boolean base64Encoded = false; try { buffer.append(name); buffer.append(':'); final int length = valueBytes.length; if (length == 0) { buffer.append(' '); return; } // If the value starts with a space, colon, or less-than character, then // it must be base64-encoded. switch (valueBytes[0]) { case ' ': case ':': case '<': buffer.append(": "); Base64.encode(valueBytes, buffer); base64Encoded = true; return; } // If the value ends with a space, then it should be base64-encoded. if (valueBytes[length-1] == ' ') { buffer.append(": "); Base64.encode(valueBytes, buffer); base64Encoded = true; return; } // If any character in the value is outside the ASCII range, or is the // NUL, LF, or CR character, then the value should be base64-encoded. for (int i=0; i < length; i++) { if ((valueBytes[i] & 0x7F) != (valueBytes[i] & 0xFF)) { buffer.append(": "); Base64.encode(valueBytes, buffer); base64Encoded = true; return; } switch (valueBytes[i]) { case 0x00: // The NUL character case 0x0A: // The LF character case 0x0D: // The CR character buffer.append(": "); Base64.encode(valueBytes, buffer); base64Encoded = true; return; } } // If we've gotten here, then the string value is acceptable. buffer.append(' '); buffer.append(value.stringValue()); } finally { if (wrapColumn > 2) { final int length = buffer.length() - bufferStartPos; if (length > wrapColumn) { final String EOL_PLUS_SPACE = EOL + ' '; buffer.insert((bufferStartPos+wrapColumn), EOL_PLUS_SPACE); int pos = bufferStartPos + (2*wrapColumn) + EOL_PLUS_SPACE.length() - 1; while (pos < buffer.length()) { buffer.insert(pos, EOL_PLUS_SPACE); pos += (wrapColumn - 1 + EOL_PLUS_SPACE.length()); } } } if (base64Encoded && commentAboutBase64EncodedValues) { writeBase64DecodedValueComment(valueBytes, buffer, wrapColumn); } } } /** * Appends a comment to the provided buffer with an unencoded representation * of the provided value. This will only have any effect if * {@code commentAboutBase64EncodedValues} is {@code true}. * * @param valueBytes The bytes that comprise the value. * @param buffer The buffer to which the comment should be appended. * @param wrapColumn The column at which to wrap long lines. */ private static void writeBase64DecodedValueComment(final byte[] valueBytes, final StringBuilder buffer, final int wrapColumn) { if (commentAboutBase64EncodedValues) { final int wrapColumnMinusTwo; if (wrapColumn <= 5) { wrapColumnMinusTwo = TERMINAL_WIDTH_COLUMNS - 3; } else { wrapColumnMinusTwo = wrapColumn - 2; } final int wrapColumnMinusThree = wrapColumnMinusTwo - 1; boolean first = true; final String comment = "Non-base64-encoded representation of the above value: " + getEscapedValue(valueBytes); for (final String s : wrapLine(comment, wrapColumnMinusTwo, wrapColumnMinusThree)) { buffer.append(EOL); buffer.append("# "); if (first) { first = false; } else { buffer.append(' '); } buffer.append(s); } } } /** * Appends a string to the provided buffer consisting of the provided * attribute name followed by either a single colon and the string * representation of the provided value, or two colons and the base64-encoded * representation of the provided value. It may optionally be wrapped at the * specified column. * * @param name The name for the attribute. * @param value The value for the attribute. * @param buffer The buffer to which the name and value are to be * written. * @param wrapColumn The column at which to wrap long lines. A value that * is less than or equal to two indicates that no * wrapping should be performed. */ public static void encodeNameAndValue(final String name, final ASN1OctetString value, final ByteStringBuffer buffer, final int wrapColumn) { final int bufferStartPos = buffer.length(); boolean base64Encoded = false; try { buffer.append(name); base64Encoded = encodeValue(value, buffer); } finally { if (wrapColumn > 2) { final int length = buffer.length() - bufferStartPos; if (length > wrapColumn) { final byte[] EOL_BYTES_PLUS_SPACE = new byte[EOL_BYTES.length + 1]; System.arraycopy(EOL_BYTES, 0, EOL_BYTES_PLUS_SPACE, 0, EOL_BYTES.length); EOL_BYTES_PLUS_SPACE[EOL_BYTES.length] = ' '; buffer.insert((bufferStartPos+wrapColumn), EOL_BYTES_PLUS_SPACE); int pos = bufferStartPos + (2*wrapColumn) + EOL_BYTES_PLUS_SPACE.length - 1; while (pos < buffer.length()) { buffer.insert(pos, EOL_BYTES_PLUS_SPACE); pos += (wrapColumn - 1 + EOL_BYTES_PLUS_SPACE.length); } } } if (base64Encoded && commentAboutBase64EncodedValues) { writeBase64DecodedValueComment(value.getValue(), buffer, wrapColumn); } } } /** * Appends a string to the provided buffer consisting of the properly-encoded * representation of the provided value, including the necessary colon(s) and * space that precede it. Depending on the content of the value, it will * either be used as-is or base64-encoded. * * @param value The value for the attribute. * @param buffer The buffer to which the value is to be written. * * @return {@code true} if the value was base64-encoded, or {@code false} if * not. */ static boolean encodeValue(final ASN1OctetString value, final ByteStringBuffer buffer) { buffer.append(':'); final byte[] valueBytes = value.getValue(); final int length = valueBytes.length; if (length == 0) { buffer.append(' '); return false; } // If the value starts with a space, colon, or less-than character, then // it must be base64-encoded. switch (valueBytes[0]) { case ' ': case ':': case '<': buffer.append(':'); buffer.append(' '); Base64.encode(valueBytes, buffer); return true; } // If the value ends with a space, then it should be base64-encoded. if (valueBytes[length-1] == ' ') { buffer.append(':'); buffer.append(' '); Base64.encode(valueBytes, buffer); return true; } // If any character in the value is outside the ASCII range, or is the // NUL, LF, or CR character, then the value should be base64-encoded. for (int i=0; i < length; i++) { if ((valueBytes[i] & 0x7F) != (valueBytes[i] & 0xFF)) { buffer.append(':'); buffer.append(' '); Base64.encode(valueBytes, buffer); return true; } switch (valueBytes[i]) { case 0x00: // The NUL character case 0x0A: // The LF character case 0x0D: // The CR character buffer.append(':'); buffer.append(' '); Base64.encode(valueBytes, buffer); return true; } } // If we've gotten here, then the string value is acceptable. buffer.append(' '); buffer.append(valueBytes); return false; } /** * Appends a comment to the provided buffer with an unencoded representation * of the provided value. This will only have any effect if * {@code commentAboutBase64EncodedValues} is {@code true}. * * @param valueBytes The bytes that comprise the value. * @param buffer The buffer to which the comment should be appended. * @param wrapColumn The column at which to wrap long lines. */ private static void writeBase64DecodedValueComment(final byte[] valueBytes, final ByteStringBuffer buffer, final int wrapColumn) { if (commentAboutBase64EncodedValues) { final int wrapColumnMinusTwo; if (wrapColumn <= 5) { wrapColumnMinusTwo = TERMINAL_WIDTH_COLUMNS - 3; } else { wrapColumnMinusTwo = wrapColumn - 2; } final int wrapColumnMinusThree = wrapColumnMinusTwo - 1; boolean first = true; final String comment = "Non-base64-encoded representation of the above value: " + getEscapedValue(valueBytes); for (final String s : wrapLine(comment, wrapColumnMinusTwo, wrapColumnMinusThree)) { buffer.append(EOL); buffer.append("# "); if (first) { first = false; } else { buffer.append(' '); } buffer.append(s); } } } /** * Retrieves a string representation of the provided value with all special * characters escaped with backslashes. * * @param valueBytes The byte array containing the value to encode. * * @return A string representation of the provided value with any special * characters */ private static String getEscapedValue(final byte[] valueBytes) { final StringBuilder buffer = new StringBuilder(valueBytes.length * 2); for (int i=0; i < valueBytes.length; i++) { final byte b = valueBytes[i]; switch (b) { case '\n': buffer.append("\\n"); break; case '\r': buffer.append("\\r"); break; case '\t': buffer.append("\\t"); break; case ' ': if (i == 0) { buffer.append("\\ "); } else if ( i == (valueBytes.length - 1)) { buffer.append("\\20"); } else { buffer.append(' '); } break; case '<': if (i == 0) { buffer.append('\\'); } buffer.append('<'); break; case ':': if (i == 0) { buffer.append('\\'); } buffer.append(':'); break; default: if ((b >= '!') && (b <= '~')) { buffer.append((char) b); } else { buffer.append("\\"); toHex(b, buffer); } break; } } return buffer.toString(); } /** * If the provided exception is non-null, then it will be rethrown as an * unchecked exception or an IOException. * * @param t The exception to rethrow as an an unchecked exception or an * IOException or {@code null} if none. * * @throws IOException If t is a checked exception. */ static void rethrow(final Throwable t) throws IOException { if (t == null) { return; } if (t instanceof IOException) { throw (IOException) t; } else if (t instanceof RuntimeException) { throw (RuntimeException) t; } else if (t instanceof Error) { throw (Error) t; } else { throw createIOExceptionWithCause(null, t); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy