/*
* 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 2009 Sun Microsystems, Inc.
*/
package org.opends.sdk.ldif;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.List;
import java.util.regex.Pattern;
import org.opends.sdk.ByteSequence;
import org.opends.sdk.ByteString;
import org.opends.sdk.controls.Control;
import com.sun.opends.sdk.util.Base64;
import com.sun.opends.sdk.util.Validator;
/**
* Common LDIF writer functionality.
*/
abstract class AbstractLDIFWriter extends AbstractLDIFStream
{
/**
* LDIF writer implementation interface.
*/
interface LDIFWriterImpl
{
/**
* Closes any resources associated with this LDIF writer
* implementation.
*
* @throws IOException
* If an error occurs while closing.
*/
void close() throws IOException;
/**
* Flushes this LDIF writer implementation so that any buffered data
* is written immediately to underlying stream, flushing the stream
* if it is also {@code Flushable}.
*
* If the intended destination of this stream is an abstraction
* provided by the underlying operating system, for example a file,
* then flushing the stream guarantees only that bytes previously
* written to the stream are passed to the operating system for
* writing; it does not guarantee that they are actually written to
* a physical device such as a disk drive.
*
* @throws IOException
* If an error occurs while flushing.
*/
void flush() throws IOException;
/**
* Prints the provided {@code CharSequence}. Implementations must
* not add a new-line character sequence.
*
* @param s
* The {@code CharSequence} to be printed.
* @throws IOException
* If an error occurs while printing {@code s}.
*/
void print(CharSequence s) throws IOException;
/**
* Prints a new-line character sequence.
*
* @throws IOException
* If an error occurs while printing the new-line
* character sequence.
*/
void println() throws IOException;
}
/**
* LDIF string list writer implementation.
*/
private final class LDIFWriterListImpl implements LDIFWriterImpl
{
private final StringBuilder builder = new StringBuilder();
private final List ldifLines;
/**
* Creates a new LDIF list writer.
*
* @param ldifLines
* The string list.
*/
LDIFWriterListImpl(List ldifLines)
{
this.ldifLines = ldifLines;
}
/**
* {@inheritDoc}
*/
public void close() throws IOException
{
// Nothing to do.
}
/**
* {@inheritDoc}
*/
public void flush() throws IOException
{
// Nothing to do.
}
/**
* {@inheritDoc}
*/
public void print(CharSequence s) throws IOException
{
builder.append(s);
}
/**
* {@inheritDoc}
*/
public void println() throws IOException
{
ldifLines.add(builder.toString());
builder.setLength(0);
}
}
/**
* LDIF output stream writer implementation.
*/
private final class LDIFWriterOutputStreamImpl implements
LDIFWriterImpl
{
private final BufferedWriter writer;
/**
* Creates a new LDIF output stream writer.
*
* @param out
* The output stream.
*/
LDIFWriterOutputStreamImpl(OutputStream out)
{
this.writer = new BufferedWriter(new OutputStreamWriter(out));
}
/**
* {@inheritDoc}
*/
public void close() throws IOException
{
writer.close();
}
/**
* {@inheritDoc}
*/
public void flush() throws IOException
{
writer.flush();
}
/**
* {@inheritDoc}
*/
public void print(CharSequence s) throws IOException
{
writer.append(s);
}
/**
* {@inheritDoc}
*/
public void println() throws IOException
{
writer.newLine();
}
}
// Regular expression used for splitting comments on line-breaks.
private static final Pattern SPLIT_NEWLINE =
Pattern.compile("\\r?\\n");
boolean addUserFriendlyComments = false;
final LDIFWriterImpl impl;
int wrapColumn = 0;
private final StringBuilder builder = new StringBuilder(80);
/**
* Creates a new LDIF entry writer which will append lines of LDIF to
* the provided list.
*
* @param ldifLines
* The list to which lines of LDIF should be appended.
*/
public AbstractLDIFWriter(List ldifLines)
{
Validator.ensureNotNull(ldifLines);
this.impl = new LDIFWriterListImpl(ldifLines);
}
/**
* Creates a new LDIF entry writer whose destination is the provided
* output stream.
*
* @param out
* The output stream to use.
*/
public AbstractLDIFWriter(OutputStream out)
{
Validator.ensureNotNull(out);
this.impl = new LDIFWriterOutputStreamImpl(out);
}
final void close0() throws IOException
{
flush0();
impl.close();
}
final void flush0() throws IOException
{
impl.flush();
}
final void writeComment0(CharSequence comment) throws IOException,
NullPointerException
{
Validator.ensureNotNull(comment);
// First, break up the comment into multiple lines to preserve the
// original spacing that it contained.
final String[] lines = SPLIT_NEWLINE.split(comment);
// Now iterate through the lines and write them out, prefixing and
// wrapping them as necessary.
for (final String line : lines)
{
if (!shouldWrap())
{
impl.print("# ");
impl.print(line);
impl.println();
}
else
{
final int breakColumn = wrapColumn - 2;
if (line.length() <= breakColumn)
{
impl.print("# ");
impl.print(line);
impl.println();
}
else
{
int startPos = 0;
outerLoop: while (startPos < line.length())
{
if (startPos + breakColumn >= line.length())
{
impl.print("# ");
impl.print(line.substring(startPos));
impl.println();
startPos = line.length();
}
else
{
final int endPos = startPos + breakColumn;
int i = endPos - 1;
while (i > startPos)
{
if (line.charAt(i) == ' ')
{
impl.print("# ");
impl.print(line.substring(startPos, i));
impl.println();
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.
impl.print("# ");
impl.print(line.substring(startPos, endPos));
impl.println();
startPos = endPos;
}
}
}
}
}
}
final void writeControls(Iterable controls)
throws IOException
{
for (final Control control : controls)
{
final StringBuilder key = new StringBuilder("control: ");
key.append(control.getOID());
key.append(control.isCritical() ? " true" : " false");
if (control.hasValue())
{
writeKeyAndValue(key, control.getValue());
}
else
{
writeLine(key);
}
}
}
final void writeKeyAndValue(CharSequence key, ByteSequence value)
throws IOException
{
builder.setLength(0);
// If the value is empty, then just append a single colon and a
// single space.
if (value.length() == 0)
{
builder.append(key);
builder.append(": ");
}
else if (needsBase64Encoding(value))
{
if (addUserFriendlyComments)
{
// TODO: Only display comments for valid UTF-8 values, not
// binary values.
}
builder.setLength(0);
builder.append(key);
builder.append(":: ");
builder.append(Base64.encode(value));
}
else
{
builder.append(key);
builder.append(": ");
builder.append(value.toString());
}
writeLine(builder);
}
final void writeKeyAndValue(CharSequence key, CharSequence value)
throws IOException
{
// FIXME: We should optimize this at some point.
writeKeyAndValue(key, ByteString.valueOf(value.toString()));
}
final void writeLine(CharSequence line) throws IOException
{
final int length = line.length();
if (shouldWrap() && length > wrapColumn)
{
impl.print(line.subSequence(0, wrapColumn));
impl.println();
int pos = wrapColumn;
while (pos < length)
{
final int writeLength = Math.min(wrapColumn - 1, length - pos);
impl.print(" ");
impl.print(line.subSequence(pos, pos + writeLength));
impl.println();
pos += wrapColumn - 1;
}
}
else
{
impl.print(line);
impl.println();
}
}
private boolean needsBase64Encoding(ByteSequence bytes)
{
final int length = bytes.length();
if (length == 0)
{
return false;
}
// If the value starts with a space, colon, or less than, then it
// needs to be base64 encoded.
switch (bytes.byteAt(0))
{
case 0x20: // Space
case 0x3A: // Colon
case 0x3C: // Less-than
return true;
}
// If the value ends with a space, then it needs to be
// base64 encoded.
if (length > 1 && bytes.byteAt(length - 1) == 0x20)
{
return true;
}
// If the value contains a null, newline, or return character, then
// it needs to be base64 encoded.
byte b;
for (int i = 0; i < bytes.length(); i++)
{
b = bytes.byteAt(i);
if (b > 127 || b < 0)
{
return true;
}
switch (b)
{
case 0x00: // Null
case 0x0A: // New line
case 0x0D: // Carriage return
return true;
}
}
// If we've made it here, then there's no reason to base64 encode.
return false;
}
private boolean shouldWrap()
{
return wrapColumn > 1;
}
@SuppressWarnings("unused")
private void writeKeyAndURL(CharSequence key, CharSequence url)
throws IOException
{
builder.setLength(0);
builder.append(key);
builder.append(":: ");
builder.append(url);
writeLine(builder);
}
}