package com.hwlcn.ldap.ldif;
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.hwlcn.ldap.asn1.ASN1OctetString;
import com.hwlcn.ldap.ldap.sdk.Entry;
import com.hwlcn.ldap.util.Base64;
import com.hwlcn.ldap.util.LDAPSDKThreadFactory;
import com.hwlcn.core.annotation.ThreadSafety;
import com.hwlcn.ldap.util.ThreadSafetyLevel;
import com.hwlcn.ldap.util.ByteStringBuffer;
import com.hwlcn.ldap.util.parallel.ParallelProcessor;
import com.hwlcn.ldap.util.parallel.Result;
import com.hwlcn.ldap.util.parallel.Processor;
import static com.hwlcn.ldap.util.Debug.*;
import static com.hwlcn.ldap.util.StaticUtils.*;
import static com.hwlcn.ldap.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
* <A HREF="http://www.ietf.org/rfc/rfc2849.txt">RFC 2849</A>.
* <BR><BR>
* <H2>Example</H2>
* The following example performs a search to find all users in the "Sales"
* department and then writes their entries to an LDIF file:
* <PRE>
* SearchResult searchResult =
* connection.search("dc=example,dc=com", SearchScope.SUB, "(ou=Sales)");
*
* LDIFWriter ldifWriter = new LDIFWriter(pathToLDIF);
* for (SearchResultEntry entry : searchResult.getSearchEntries())
* {
* ldifWriter.writeEntry(entry);
* }
*
* ldifWriter.close();
* </PRE>
*/
@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
public final class LDIFWriter
{
private static final int DEFAULT_BUFFER_SIZE = 128 * 1024;
private final BufferedOutputStream writer;
private final ByteStringBuffer buffer;
private final LDIFWriterEntryTranslator entryTranslator;
private int wrapColumn = 0;
private int wrapColumnMinusTwo = -2;
private final ParallelProcessor<LDIFRecord,ByteStringBuffer>
toLdifBytesInvoker;
public LDIFWriter(final String path)
throws IOException
{
this(new FileOutputStream(path));
}
public LDIFWriter(final File file)
throws IOException
{
this(new FileOutputStream(file));
}
public LDIFWriter(final OutputStream outputStream)
{
this(outputStream, 0);
}
public LDIFWriter(final OutputStream outputStream, final int parallelThreads)
{
this(outputStream, parallelThreads, null);
}
public LDIFWriter(final OutputStream outputStream, final int parallelThreads,
final LDIFWriterEntryTranslator entryTranslator)
{
ensureNotNull(outputStream);
ensureTrue(parallelThreads >= 0,
"LDIFWriter.parallelThreads must not be negative.");
this.entryTranslator = entryTranslator;
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<LDIFRecord,ByteStringBuffer>(
new Processor<LDIFRecord,ByteStringBuffer>() {
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
{
r = input;
}
final ByteStringBuffer b = new ByteStringBuffer(200);
r.toLDIF(b, wrapColumn);
return b;
}
}, threadFactory, parallelThreads, 5);
}
}
public void flush()
throws IOException
{
writer.flush();
}
public void close()
throws IOException
{
try
{
if (toLdifBytesInvoker != null)
{
try
{
toLdifBytesInvoker.shutdown();
}
catch (InterruptedException e)
{
debugException(e);
}
}
}
finally
{
writer.close();
}
}
public int getWrapColumn()
{
return wrapColumn;
}
public void setWrapColumn(final int wrapColumn)
{
this.wrapColumn = wrapColumn;
wrapColumnMinusTwo = wrapColumn - 2;
}
public void writeEntry(final Entry entry)
throws IOException
{
writeEntry(entry, null);
}
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(entry);
writeLDIF(entry);
}
public void writeChangeRecord(final LDIFChangeRecord changeRecord)
throws IOException
{
ensureNotNull(changeRecord);
debugLDIFWrite(changeRecord);
writeLDIF(changeRecord);
}
public void writeChangeRecord(final LDIFChangeRecord changeRecord,
final String comment)
throws IOException
{
ensureNotNull(changeRecord);
debugLDIFWrite(changeRecord);
if (comment != null)
{
writeComment(comment, false, false);
}
writeLDIF(changeRecord);
}
public void writeLDIFRecord(final LDIFRecord record)
throws IOException
{
writeLDIFRecord(record, null);
}
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
{
r = record;
}
debugLDIFWrite(r);
if (comment != null)
{
writeComment(comment, false, false);
}
writeLDIF(r);
}
public void writeLDIFRecords(final List<? extends LDIFRecord> ldifRecords)
throws IOException, InterruptedException
{
if (toLdifBytesInvoker == null)
{
for (final LDIFRecord ldifRecord : ldifRecords)
{
writeLDIFRecord(ldifRecord);
}
}
else
{
final List<Result<LDIFRecord,ByteStringBuffer>> results =
toLdifBytesInvoker.processAll(ldifRecords);
for (final Result<LDIFRecord,ByteStringBuffer> result: results)
{
rethrow(result.getFailureCause());
final ByteStringBuffer encodedBytes = result.getOutput();
if (encodedBytes != null)
{
encodedBytes.write(writer);
writer.write(EOL_BYTES);
}
}
}
}
public void writeComment(final String comment, final boolean spaceBefore,
final boolean spaceAfter)
throws IOException
{
ensureNotNull(comment);
if (spaceBefore)
{
writer.write(EOL_BYTES);
}
if (comment.indexOf('\n') < 0)
{
writeSingleLineComment(comment);
}
else
{
final String[] lines = comment.split("\\r?\\n");
for (final String line: lines)
{
writeSingleLineComment(line);
}
}
if (spaceAfter)
{
writer.write(EOL_BYTES);
}
}
private void writeSingleLineComment(final String comment)
throws IOException
{
final int commentWrapMinusTwo;
if (wrapColumn <= 0)
{
commentWrapMinusTwo = 77;
}
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;
}
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)
{
buffer.append("# ");
buffer.append(comment.substring(minPos));
buffer.append(EOL_BYTES);
break;
}
}
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);
}
private void writeLDIF(final LDIFRecord record)
throws IOException
{
buffer.clear();
record.toLDIF(buffer, wrapColumn);
buffer.append(EOL_BYTES);
buffer.write(writer);
}
public static List<String> wrapLines(final int wrapColumn,
final String... ldifLines)
{
return wrapLines(wrapColumn, Arrays.asList(ldifLines));
}
public static List<String> wrapLines(final int wrapColumn,
final List<String> ldifLines)
{
if (wrapColumn <= 2)
{
return new ArrayList<String>(ldifLines);
}
final ArrayList<String> newLines = new ArrayList<String>(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;
}
public static String encodeNameAndValue(final String name,
final ASN1OctetString value)
{
final StringBuilder buffer = new StringBuilder();
encodeNameAndValue(name, value, buffer);
return buffer.toString();
}
public static void encodeNameAndValue(final String name,
final ASN1OctetString value,
final StringBuilder buffer)
{
encodeNameAndValue(name, value, buffer, 0);
}
public static void encodeNameAndValue(final String name,
final ASN1OctetString value,
final StringBuilder buffer,
final int wrapColumn)
{
final int bufferStartPos = buffer.length();
try
{
buffer.append(name);
buffer.append(':');
final byte[] valueBytes = value.getValue();
final int length = valueBytes.length;
if (length == 0)
{
buffer.append(' ');
return;
}
switch (valueBytes[0])
{
case ' ':
case ':':
case '<':
buffer.append(": ");
Base64.encode(valueBytes, buffer);
return;
}
if (valueBytes[length-1] == ' ')
{
buffer.append(": ");
Base64.encode(valueBytes, buffer);
return;
}
for (int i=0; i < length; i++)
{
if ((valueBytes[i] & 0x7F) != (valueBytes[i] & 0xFF))
{
buffer.append(": ");
Base64.encode(valueBytes, buffer);
return;
}
switch (valueBytes[i])
{
case 0x00:
case 0x0A:
case 0x0D:
buffer.append(": ");
Base64.encode(valueBytes, buffer);
return;
}
}
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());
}
}
}
}
}
public static void encodeNameAndValue(final String name,
final ASN1OctetString value,
final ByteStringBuffer buffer,
final int wrapColumn)
{
final int bufferStartPos = buffer.length();
try
{
buffer.append(name);
buffer.append(':');
final byte[] valueBytes = value.getValue();
final int length = valueBytes.length;
if (length == 0)
{
buffer.append(' ');
return;
}
switch (valueBytes[0])
{
case ' ':
case ':':
case '<':
buffer.append(':');
buffer.append(' ');
Base64.encode(valueBytes, buffer);
return;
}
if (valueBytes[length-1] == ' ')
{
buffer.append(':');
buffer.append(' ');
Base64.encode(valueBytes, buffer);
return;
}
for (int i=0; i < length; i++)
{
if ((valueBytes[i] & 0x7F) != (valueBytes[i] & 0xFF))
{
buffer.append(':');
buffer.append(' ');
Base64.encode(valueBytes, buffer);
return;
}
switch (valueBytes[i])
{
case 0x00:
case 0x0A:
case 0x0D:
buffer.append(':');
buffer.append(' ');
Base64.encode(valueBytes, buffer);
return;
}
}
buffer.append(' ');
buffer.append(valueBytes);
}
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);
}
}
}
}
}
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 new IOException(getExceptionMessage(t));
}
}
}