/*
* 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-2010 Sun Microsystems, Inc.
*/
package org.opends.sdk.ldif;
import static com.forgerock.opendj.util.StaticUtils.toLowerCase;
import static org.opends.sdk.CoreMessages.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.NoSuchElementException;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.opends.sdk.*;
import org.opends.sdk.requests.ModifyDNRequest;
import org.opends.sdk.requests.ModifyRequest;
import org.opends.sdk.requests.Requests;
import org.opends.sdk.schema.Schema;
import com.forgerock.opendj.util.Validator;
/**
* An LDIF change record reader reads change records using the LDAP Data
* Interchange Format (LDIF) from a user defined source.
*
* @see RFC 2849 - The LDAP Data
* Interchange Format (LDIF) - Technical Specification
*/
public final class LDIFChangeRecordReader extends AbstractLDIFReader implements
ChangeRecordReader
{
/**
* Parses the provided array of LDIF lines as a single LDIF change record.
*
* @param ldifLines
* The lines of LDIF to be parsed.
* @return The parsed LDIF change record.
* @throws LocalizedIllegalArgumentException
* If {@code ldifLines} did not contain an LDIF change record, if it
* contained multiple change records, if contained malformed LDIF,
* or if the change record could not be decoded using the default
* schema.
* @throws NullPointerException
* If {@code ldifLines} was {@code null}.
*/
public static ChangeRecord valueOfLDIFChangeRecord(final String... ldifLines)
throws LocalizedIllegalArgumentException, NullPointerException
{
// LDIF change record reader is tolerant to missing change types.
final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(ldifLines);
try
{
if (!reader.hasNext())
{
// No change record found.
final LocalizableMessage message = WARN_READ_LDIF_RECORD_NO_CHANGE_RECORD_FOUND
.get();
throw new LocalizedIllegalArgumentException(message);
}
final ChangeRecord record = reader.readChangeRecord();
if (reader.hasNext())
{
// Multiple change records found.
final LocalizableMessage message = WARN_READ_LDIF_RECORD_MULTIPLE_CHANGE_RECORDS_FOUND
.get();
throw new LocalizedIllegalArgumentException(message);
}
return record;
}
catch (final DecodeException e)
{
// Badly formed LDIF.
throw new LocalizedIllegalArgumentException(e.getMessageObject());
}
catch (final IOException e)
{
// This should never happen for a String based reader.
final LocalizableMessage message = WARN_READ_LDIF_RECORD_UNEXPECTED_IO_ERROR
.get(e.getMessage());
throw new LocalizedIllegalArgumentException(message);
}
}
private ChangeRecord nextChangeRecord = null;
// Poison used to indicate end of LDIF.
private static final ChangeRecord EOF = Requests.newAddRequest(DN.rootDN());
/**
* Creates a new LDIF change record reader whose source is the provided input
* stream.
*
* @param in
* The input stream to use.
* @throws NullPointerException
* If {@code in} was {@code null}.
*/
public LDIFChangeRecordReader(final InputStream in)
throws NullPointerException
{
super(in);
}
/**
* Creates a new LDIF change record reader which will read lines of LDIF from
* the provided list of LDIF lines.
*
* @param ldifLines
* The lines of LDIF to be read.
* @throws NullPointerException
* If {@code ldifLines} was {@code null}.
*/
public LDIFChangeRecordReader(final List ldifLines)
throws NullPointerException
{
super(ldifLines);
}
/**
* Creates a new LDIF change record reader which will read lines of LDIF from
* the provided array of LDIF lines.
*
* @param ldifLines
* The lines of LDIF to be read.
* @throws NullPointerException
* If {@code ldifLines} was {@code null}.
*/
public LDIFChangeRecordReader(final String... ldifLines)
throws NullPointerException
{
super(Arrays.asList(ldifLines));
}
/**
* {@inheritDoc}
*/
@Override
public void close() throws IOException
{
close0();
}
/**
* {@inheritDoc}
*
* @throws DecodeException
* If the change record could not be decoded because it was
* malformed.
*/
@Override
public boolean hasNext() throws DecodeException, IOException
{
return getNextChangeRecord() != EOF;
}
/**
* {@inheritDoc}
*
* @throws DecodeException
* If the entry could not be decoded because it was malformed.
*/
@Override
public ChangeRecord readChangeRecord() throws DecodeException, IOException
{
if (!hasNext())
{
// LDIF reader has completed successfully.
throw new NoSuchElementException();
}
final ChangeRecord changeRecord = nextChangeRecord;
nextChangeRecord = null;
return changeRecord;
}
/**
* Specifies whether or not all operational attributes should be excluded from
* any change records that are read from LDIF. The default is {@code false}.
*
* @param excludeOperationalAttributes
* {@code true} if all operational attributes should be excluded, or
* {@code false} otherwise.
* @return A reference to this {@code LDIFChangeRecordReader}.
*/
public LDIFChangeRecordReader setExcludeAllOperationalAttributes(
final boolean excludeOperationalAttributes)
{
this.excludeOperationalAttributes = excludeOperationalAttributes;
return this;
}
/**
* Specifies whether or not all user attributes should be excluded from any
* change records that are read from LDIF. The default is {@code false}.
*
* @param excludeUserAttributes
* {@code true} if all user attributes should be excluded, or
* {@code false} otherwise.
* @return A reference to this {@code LDIFChangeRecordReader}.
*/
public LDIFChangeRecordReader setExcludeAllUserAttributes(
final boolean excludeUserAttributes)
{
this.excludeUserAttributes = excludeUserAttributes;
return this;
}
/**
* Excludes the named attribute from any change records that are read from
* LDIF. By default all attributes are included unless explicitly excluded.
*
* @param attributeDescription
* The name of the attribute to be excluded.
* @return A reference to this {@code LDIFChangeRecordReader}.
*/
public LDIFChangeRecordReader setExcludeAttribute(
final AttributeDescription attributeDescription)
{
Validator.ensureNotNull(attributeDescription);
excludeAttributes.add(attributeDescription);
return this;
}
/**
* Excludes all change records which target entries beneath the named entry
* (inclusive) from being read from LDIF. By default all change records are
* read unless explicitly excluded or included.
*
* @param excludeBranch
* The distinguished name of the branch to be excluded.
* @return A reference to this {@code LDIFChangeRecordReader}.
*/
public LDIFChangeRecordReader setExcludeBranch(final DN excludeBranch)
{
Validator.ensureNotNull(excludeBranch);
excludeBranches.add(excludeBranch);
return this;
}
/**
* Ensures that the named attribute is not excluded from any change records
* that are read from LDIF. By default all attributes are included unless
* explicitly excluded.
*
* @param attributeDescription
* The name of the attribute to be included.
* @return A reference to this {@code LDIFChangeRecordReader}.
*/
public LDIFChangeRecordReader setIncludeAttribute(
final AttributeDescription attributeDescription)
{
Validator.ensureNotNull(attributeDescription);
includeAttributes.add(attributeDescription);
return this;
}
/**
* Ensures that all change records which target entries beneath the named
* entry (inclusive) are read from LDIF. By default all change records are
* read unless explicitly excluded or included.
*
* @param includeBranch
* The distinguished name of the branch to be included.
* @return A reference to this {@code LDIFChangeRecordReader}.
*/
public LDIFChangeRecordReader setIncludeBranch(final DN includeBranch)
{
Validator.ensureNotNull(includeBranch);
includeBranches.add(includeBranch);
return this;
}
/**
* Sets the schema which should be used for decoding change records that are
* read from LDIF. The default schema is used if no other is specified.
*
* @param schema
* The schema which should be used for decoding change records that
* are read from LDIF.
* @return A reference to this {@code LDIFChangeRecordReader}.
*/
public LDIFChangeRecordReader setSchema(final Schema schema)
{
Validator.ensureNotNull(schema);
this.schema = schema;
return this;
}
/**
* Specifies whether or not schema validation should be performed for change
* records that are read from LDIF. The default is {@code true} .
*
* @param validateSchema
* {@code true} if schema validation should be performed, or
* {@code false} otherwise.
* @return A reference to this {@code LDIFChangeRecordReader}.
*/
public LDIFChangeRecordReader setValidateSchema(final boolean validateSchema)
{
this.validateSchema = validateSchema;
return this;
}
private ChangeRecord getNextChangeRecord() throws DecodeException,
IOException
{
while (nextChangeRecord == null)
{
LDIFRecord record = null;
// Read the set of lines that make up the next entry.
record = readLDIFRecord();
if (record == null)
{
nextChangeRecord = EOF;
break;
}
// Read the DN of the entry and see if it is one that should be
// included in the import.
DN entryDN;
try
{
entryDN = readLDIFRecordDN(record);
if (entryDN == null)
{
// Skip version record.
continue;
}
}
catch (final DecodeException e)
{
rejectLDIFRecord(record, e.getMessageObject());
continue;
}
// Skip if branch containing the entry DN is excluded.
if (isBranchExcluded(entryDN))
{
final LocalizableMessage message = LocalizableMessage
.raw("Skipping entry because it is in excluded branch");
skipLDIFRecord(record, message);
continue;
}
ChangeRecord changeRecord = null;
try
{
if (!record.iterator.hasNext())
{
// FIXME: improve error.
final LocalizableMessage message = LocalizableMessage
.raw("Missing changetype");
throw DecodeException.error(message);
}
final KeyValuePair pair = new KeyValuePair();
final String ldifLine = readLDIFRecordKeyValuePair(record, pair, false);
if (!toLowerCase(pair.key).equals("changetype"))
{
// Default to add change record.
changeRecord = parseAddChangeRecordEntry(entryDN, ldifLine, record);
}
else
{
final String changeType = toLowerCase(pair.value);
if (changeType.equals("add"))
{
changeRecord = parseAddChangeRecordEntry(entryDN, null, record);
}
else if (changeType.equals("delete"))
{
changeRecord = parseDeleteChangeRecordEntry(entryDN, record);
}
else if (changeType.equals("modify"))
{
changeRecord = parseModifyChangeRecordEntry(entryDN, record);
}
else if (changeType.equals("modrdn"))
{
changeRecord = parseModifyDNChangeRecordEntry(entryDN, record);
}
else if (changeType.equals("moddn"))
{
changeRecord = parseModifyDNChangeRecordEntry(entryDN, record);
}
else
{
// FIXME: improve error.
final LocalizableMessage message = ERR_LDIF_INVALID_CHANGETYPE_ATTRIBUTE
.get(pair.value, "add, delete, modify, moddn, modrdn");
throw DecodeException.error(message);
}
}
}
catch (final DecodeException e)
{
rejectLDIFRecord(record, e.getMessageObject());
continue;
}
if (changeRecord != null)
{
nextChangeRecord = changeRecord;
}
}
return nextChangeRecord;
}
private ChangeRecord parseAddChangeRecordEntry(final DN entryDN,
final String lastLDIFLine, final LDIFRecord record)
throws DecodeException
{
// Use an Entry for the AttributeSequence.
final Entry entry = new LinkedHashMapEntry(entryDN);
if (lastLDIFLine != null)
{
// This line was read when looking for the change type.
readLDIFRecordAttributeValue(record, lastLDIFLine, entry);
}
while (record.iterator.hasNext())
{
final String ldifLine = record.iterator.next();
readLDIFRecordAttributeValue(record, ldifLine, entry);
}
return Requests.newAddRequest(entry);
}
private ChangeRecord parseDeleteChangeRecordEntry(final DN entryDN,
final LDIFRecord record) throws DecodeException
{
if (record.iterator.hasNext())
{
// FIXME: include line number in error.
final LocalizableMessage message = ERR_LDIF_INVALID_DELETE_ATTRIBUTES
.get();
throw DecodeException.error(message);
}
return Requests.newDeleteRequest(entryDN);
}
private ChangeRecord parseModifyChangeRecordEntry(final DN entryDN,
final LDIFRecord record) throws DecodeException
{
final ModifyRequest modifyRequest = Requests.newModifyRequest(entryDN);
final KeyValuePair pair = new KeyValuePair();
final List attributeValues = new ArrayList();
while (record.iterator.hasNext())
{
readLDIFRecordKeyValuePair(record, pair, false);
final String changeType = toLowerCase(pair.key);
ModificationType modType;
if (changeType.equals("add"))
{
modType = ModificationType.ADD;
}
else if (changeType.equals("delete"))
{
modType = ModificationType.DELETE;
}
else if (changeType.equals("replace"))
{
modType = ModificationType.REPLACE;
}
else if (changeType.equals("increment"))
{
modType = ModificationType.INCREMENT;
}
else
{
// FIXME: improve error.
final LocalizableMessage message = ERR_LDIF_INVALID_MODIFY_ATTRIBUTE
.get(pair.key, "add, delete, replace, increment");
throw DecodeException.error(message);
}
AttributeDescription attributeDescription;
try
{
attributeDescription = AttributeDescription.valueOf(pair.value, schema);
}
catch (final LocalizedIllegalArgumentException e)
{
throw DecodeException.error(e.getMessageObject());
}
// Skip the attribute if requested before performing any schema
// checking: the attribute may have been excluded because it is
// known to violate the schema.
if (isAttributeExcluded(attributeDescription))
{
continue;
}
// Ensure that the binary option is present if required.
if (!attributeDescription.getAttributeType().getSyntax()
.isBEREncodingRequired())
{
if (validateSchema && attributeDescription.containsOption("binary"))
{
final LocalizableMessage message = ERR_LDIF_INVALID_ATTR_OPTION.get(
entryDN.toString(), record.lineNumber, pair.value);
throw DecodeException.error(message);
}
}
else
{
attributeDescription = AttributeDescription.create(
attributeDescription, "binary");
}
// Now go through the rest of the attributes until the "-" line is
// reached.
attributeValues.clear();
while (record.iterator.hasNext())
{
final String ldifLine = record.iterator.next();
if (ldifLine.equals("-"))
{
break;
}
// Parse the attribute description.
final int colonPos = parseColonPosition(record, ldifLine);
final String attrDescr = ldifLine.substring(0, colonPos);
AttributeDescription attributeDescription2;
try
{
attributeDescription2 = AttributeDescription.valueOf(attrDescr,
schema);
}
catch (final LocalizedIllegalArgumentException e)
{
throw DecodeException.error(e.getMessageObject());
}
// Ensure that the binary option is present if required.
if (attributeDescription.getAttributeType().getSyntax()
.isBEREncodingRequired())
{
attributeDescription2 = AttributeDescription.create(
attributeDescription2, "binary");
}
if (!attributeDescription2.equals(attributeDescription))
{
// TODO: include line number.
final LocalizableMessage message = ERR_LDIF_INVALID_CHANGERECORD_ATTRIBUTE
.get(attributeDescription2.toString(),
attributeDescription.toString());
throw DecodeException.error(message);
}
// Now parse the attribute value.
attributeValues.add(parseSingleValue(record, ldifLine, entryDN,
colonPos, attrDescr));
}
final Modification change = new Modification(modType,
new LinkedAttribute(attributeDescription, attributeValues));
modifyRequest.addModification(change);
}
return modifyRequest;
}
private ChangeRecord parseModifyDNChangeRecordEntry(final DN entryDN,
final LDIFRecord record) throws DecodeException
{
ModifyDNRequest modifyDNRequest;
// Parse the newrdn.
if (!record.iterator.hasNext())
{
// TODO: include line number.
final LocalizableMessage message = ERR_LDIF_NO_MOD_DN_ATTRIBUTES.get();
throw DecodeException.error(message);
}
final KeyValuePair pair = new KeyValuePair();
String ldifLine = record.iterator.next();
readLDIFRecordKeyValuePair(record, pair, true);
if (!toLowerCase(pair.key).equals("newrdn"))
{
// FIXME: improve error.
final LocalizableMessage message = LocalizableMessage
.raw("Missing newrdn");
throw DecodeException.error(message);
}
try
{
final RDN newRDN = RDN.valueOf(pair.value, schema);
modifyDNRequest = Requests.newModifyDNRequest(entryDN, newRDN);
}
catch (final LocalizedIllegalArgumentException e)
{
final LocalizableMessage message = ERR_LDIF_INVALID_DN.get(
record.lineNumber, ldifLine, e.getMessageObject());
throw DecodeException.error(message);
}
// Parse the deleteoldrdn.
if (!record.iterator.hasNext())
{
// TODO: include line number.
final LocalizableMessage message = ERR_LDIF_NO_DELETE_OLDRDN_ATTRIBUTE
.get();
throw DecodeException.error(message);
}
ldifLine = record.iterator.next();
readLDIFRecordKeyValuePair(record, pair, true);
if (!toLowerCase(pair.key).equals("deleteoldrdn"))
{
// FIXME: improve error.
final LocalizableMessage message = LocalizableMessage
.raw("Missing deleteoldrdn");
throw DecodeException.error(message);
}
final String delStr = toLowerCase(pair.value);
if (delStr.equals("false") || delStr.equals("no") || delStr.equals("0"))
{
modifyDNRequest.setDeleteOldRDN(false);
}
else if (delStr.equals("true") || delStr.equals("yes")
|| delStr.equals("1"))
{
modifyDNRequest.setDeleteOldRDN(true);
}
else
{
// FIXME: improve error.
final LocalizableMessage message = ERR_LDIF_INVALID_DELETE_OLDRDN_ATTRIBUTE
.get(pair.value);
throw DecodeException.error(message);
}
// Parse the newsuperior if present.
if (record.iterator.hasNext())
{
ldifLine = record.iterator.next();
readLDIFRecordKeyValuePair(record, pair, true);
if (!toLowerCase(pair.key).equals("newsuperior"))
{
// FIXME: improve error.
final LocalizableMessage message = LocalizableMessage
.raw("Missing newsuperior");
throw DecodeException.error(message);
}
try
{
final DN newSuperiorDN = DN.valueOf(pair.value, schema);
modifyDNRequest.setNewSuperior(newSuperiorDN.toString());
}
catch (final LocalizedIllegalArgumentException e)
{
final LocalizableMessage message = ERR_LDIF_INVALID_DN.get(
record.lineNumber, ldifLine, e.getMessageObject());
throw DecodeException.error(message);
}
}
return modifyDNRequest;
}
}