/*
* 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-2010 Sun Microsystems, Inc.
* Portions Copyright 2012-2013 ForgeRock AS
*/
package org.opends.server.util;
import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import org.opends.messages.Message;
import org.opends.messages.MessageBuilder;
import static org.opends.messages.UtilityMessages.*;
import org.opends.server.api.plugin.PluginResult;
import org.opends.server.backends.jeb.EntryID;
import org.opends.server.backends.jeb.RootContainer;
import org.opends.server.backends.jeb.importLDIF.Importer;
import org.opends.server.backends.jeb.importLDIF.Suffix;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.PluginConfigManager;
import static org.opends.server.loggers.ErrorLogger.logError;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.loggers.debug.DebugLogger.getTracer;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.protocols.ldap.LDAPAttribute;
import org.opends.server.protocols.ldap.LDAPModification;
import org.opends.server.types.*;
import static org.opends.server.util.StaticUtils.addSuperiorObjectClasses;
import static org.opends.server.util.StaticUtils.toLowerCase;
import static org.opends.server.util.Validator.ensureNotNull;
/**
* This class provides the ability to read information from an LDIF file. It
* provides support for both standard entries and change entries (as would be
* used with a tool like ldapmodify).
*/
@org.opends.server.types.PublicAPI(
stability=org.opends.server.types.StabilityLevel.UNCOMMITTED,
mayInstantiate=true,
mayExtend=false,
mayInvoke=true)
public final class LDIFReader implements Closeable
{
/**
* The tracer object for the debug logger.
*/
private static final DebugTracer TRACER = getTracer();
// The reader that will be used to read the data.
private BufferedReader reader;
// The buffer to use to read data from a URL.
private byte[] buffer;
// The import configuration that specifies what should be imported.
private LDIFImportConfig importConfig;
// The lines that comprise the body of the last entry read.
private LinkedList<StringBuilder> lastEntryBodyLines;
// The lines that comprise the header (DN and any comments) for the last entry
// read.
private LinkedList<StringBuilder> lastEntryHeaderLines;
// The number of entries that have been ignored by this LDIF reader because
// they didn't match the criteria.
private final AtomicLong entriesIgnored = new AtomicLong();
// The number of entries that have been read by this LDIF reader, including
// those that were ignored because they didn't match the criteria, and
// including those that were rejected because they were invalid in some way.
private final AtomicLong entriesRead = new AtomicLong();
// The number of entries that have been rejected by this LDIF reader.
private final AtomicLong entriesRejected = new AtomicLong();
// The line number on which the last entry started.
private long lastEntryLineNumber;
// The line number of the last line read from the LDIF file, starting with 1.
private long lineNumber;
// The plugin config manager that will be used if we are to invoke plugins
// on the entries as they are read.
private PluginConfigManager pluginConfigManager;
private RootContainer rootContainer;
/**
* Creates a new LDIF reader that will read information from the specified
* file.
*
* @param importConfig The import configuration for this LDIF reader. It
* must not be <CODE>null</CODE>.
*
* @throws IOException If a problem occurs while opening the LDIF file for
* reading.
*/
public LDIFReader(LDIFImportConfig importConfig)
throws IOException
{
ensureNotNull(importConfig);
this.importConfig = importConfig;
reader = importConfig.getReader();
buffer = new byte[4096];
lineNumber = 0;
lastEntryLineNumber = -1;
lastEntryBodyLines = new LinkedList<StringBuilder>();
lastEntryHeaderLines = new LinkedList<StringBuilder>();
pluginConfigManager = DirectoryServer.getPluginConfigManager();
// If we should invoke import plugins, then do so.
if (importConfig.invokeImportPlugins())
{
// Inform LDIF import plugins that an import session is ending
pluginConfigManager.invokeLDIFImportBeginPlugins(importConfig);
}
}
/**
* Creates a new LDIF reader that will read information from the
* specified file.
*
* @param importConfig
* The import configuration for this LDIF reader. It must not
* be <CODE>null</CODE>.
* @param rootContainer The root container needed to get the next entry ID.
* @param size The size of the buffer to read the LDIF bytes into.
*
* @throws IOException
* If a problem occurs while opening the LDIF file for
* reading.
*/
public LDIFReader(LDIFImportConfig importConfig, RootContainer rootContainer,
int size)
throws IOException
{
ensureNotNull(importConfig);
this.importConfig = importConfig;
this.reader = importConfig.getReader();
this.lineNumber = 0;
this.lastEntryLineNumber = -1;
this.lastEntryBodyLines = new LinkedList<StringBuilder>();
this.lastEntryHeaderLines = new LinkedList<StringBuilder>();
this.pluginConfigManager = DirectoryServer.getPluginConfigManager();
this.buffer = new byte[size];
this.rootContainer = rootContainer;
// If we should invoke import plugins, then do so.
if (importConfig.invokeImportPlugins())
{
// Inform LDIF import plugins that an import session is ending
this.pluginConfigManager.invokeLDIFImportBeginPlugins(importConfig);
}
}
/**
* Reads the next entry from the LDIF source.
*
* @return The next entry read from the LDIF source, or <CODE>null</CODE> if
* the end of the LDIF data is reached.
*
* @throws IOException If an I/O problem occurs while reading from the file.
*
* @throws LDIFException If the information read cannot be parsed as an LDIF
* entry.
*/
public Entry readEntry()
throws IOException, LDIFException
{
return readEntry(importConfig.validateSchema());
}
/**
* Reads the next entry from the LDIF source.
*
* @return The next entry read from the LDIF source, or <CODE>null</CODE> if
* the end of the LDIF data is reached.
*
* @param map A map of suffixes instances.
*
* @param entryInfo A object to hold information about the entry ID and what
* suffix was selected.
*
* @throws IOException If an I/O problem occurs while reading from the file.
*
* @throws LDIFException If the information read cannot be parsed as an LDIF
* entry.
*/
public final Entry readEntry(Map<DN, Suffix> map,
Importer.EntryInformation entryInfo)
throws IOException, LDIFException
{
return readEntry(importConfig.validateSchema(), map, entryInfo);
}
private Entry readEntry(boolean checkSchema, Map<DN, Suffix> map,
Importer.EntryInformation entryInfo)
throws IOException, LDIFException
{
while (true)
{
LinkedList<StringBuilder> lines;
DN entryDN;
EntryID entryID;
Suffix suffix;
synchronized (this)
{
// Read the set of lines that make up the next entry.
lines = readEntryLines();
if (lines == null)
{
return null;
}
lastEntryBodyLines = lines;
lastEntryHeaderLines = new LinkedList<StringBuilder>();
// Read the DN of the entry and see if it is one that should be included
// in the import.
try
{
entryDN = readDN(lines);
} catch (LDIFException le) {
continue;
}
if (entryDN == null)
{
// This should only happen if the LDIF starts with the "version:" line
// and has a blank line immediately after that. In that case, simply
// read and return the next entry.
continue;
}
else if (!importConfig.includeEntry(entryDN))
{
if (debugEnabled())
{
TRACER.debugInfo("Skipping entry %s because the DN isn't" +
"one that should be included based on the include and " +
"exclude branches.", entryDN);
}
entriesRead.incrementAndGet();
Message message = ERR_LDIF_SKIP.get(String.valueOf(entryDN));
logToSkipWriter(lines, message);
continue;
}
entryID = rootContainer.getNextEntryID();
suffix = Importer.getMatchSuffix(entryDN, map);
if(suffix == null)
{
if (debugEnabled())
{
TRACER.debugInfo("Skipping entry %s because the DN isn't" +
"one that should be included based on a suffix match" +
"check." ,entryDN);
}
entriesRead.incrementAndGet();
Message message = ERR_LDIF_SKIP.get(String.valueOf(entryDN));
logToSkipWriter(lines, message);
continue;
}
entriesRead.incrementAndGet();
suffix.addPending(entryDN);
}
// Read the set of attributes from the entry.
HashMap<ObjectClass,String> objectClasses =
new HashMap<ObjectClass,String>();
HashMap<AttributeType,List<AttributeBuilder>> userAttrBuilders =
new HashMap<AttributeType,List<AttributeBuilder>>();
HashMap<AttributeType,List<AttributeBuilder>> operationalAttrBuilders =
new HashMap<AttributeType,List<AttributeBuilder>>();
try
{
for (StringBuilder line : lines)
{
readAttribute(lines, line, entryDN, objectClasses, userAttrBuilders,
operationalAttrBuilders, checkSchema);
}
}
catch (LDIFException e)
{
if (debugEnabled())
{
TRACER.debugInfo("Skipping entry %s because reading" +
"its attributes failed.", entryDN);
}
Message message = ERR_LDIF_READ_ATTR_SKIP.get(String.valueOf(entryDN),
e.getMessage());
logToSkipWriter(lines, message);
suffix.removePending(entryDN);
continue;
}
// Create the entry and see if it is one that should be included in the
// import.
HashMap<AttributeType,List<Attribute>> userAttributes =
new HashMap<AttributeType,List<Attribute>>(
userAttrBuilders.size());
HashMap<AttributeType,List<Attribute>> operationalAttributes =
new HashMap<AttributeType,List<Attribute>>(
operationalAttrBuilders.size());
for (Map.Entry<AttributeType, List<AttributeBuilder>>
attrTypeEntry : userAttrBuilders.entrySet())
{
AttributeType attrType = attrTypeEntry.getKey();
List<AttributeBuilder> attrBuilderList = attrTypeEntry.getValue();
List<Attribute> attrList =
new ArrayList<Attribute>(attrBuilderList.size());
for (AttributeBuilder builder : attrBuilderList)
{
attrList.add(builder.toAttribute());
}
userAttributes.put(attrType, attrList);
}
for (Map.Entry<AttributeType, List<AttributeBuilder>>
attrTypeEntry : operationalAttrBuilders.entrySet())
{
AttributeType attrType = attrTypeEntry.getKey();
List<AttributeBuilder> attrBuilderList = attrTypeEntry.getValue();
List<Attribute> attrList =
new ArrayList<Attribute>(attrBuilderList.size());
for (AttributeBuilder builder : attrBuilderList)
{
attrList.add(builder.toAttribute());
}
operationalAttributes.put(attrType, attrList);
}
// Create the entry and see if it is one that should be included in the
// import.
Entry entry = new Entry(entryDN, objectClasses, userAttributes,
operationalAttributes);
TRACER.debugProtocolElement(DebugLogLevel.VERBOSE, entry.toString());
try
{
if (! importConfig.includeEntry(entry))
{
if (debugEnabled())
{
TRACER.debugInfo("Skipping entry %s because the DN is not one " +
"that should be included based on the include and exclude " +
"filters.", entryDN);
}
Message message = ERR_LDIF_SKIP.get(String.valueOf(entryDN));
logToSkipWriter(lines, message);
suffix.removePending(entryDN);
continue;
}
}
catch (Exception e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
suffix.removePending(entryDN);
Message message = ERR_LDIF_COULD_NOT_EVALUATE_FILTERS_FOR_IMPORT.
get(String.valueOf(entry.getDN()), lastEntryLineNumber,
String.valueOf(e));
logToSkipWriter(lines, message);
suffix.removePending(entryDN);
continue;
}
// If we should invoke import plugins, then do so.
if (importConfig.invokeImportPlugins())
{
PluginResult.ImportLDIF pluginResult =
pluginConfigManager.invokeLDIFImportPlugins(importConfig, entry);
if (! pluginResult.continueProcessing())
{
Message m;
Message rejectMessage = pluginResult.getErrorMessage();
if (rejectMessage == null)
{
m = ERR_LDIF_REJECTED_BY_PLUGIN_NOMESSAGE.get(
String.valueOf(entryDN));
}
else
{
m = ERR_LDIF_REJECTED_BY_PLUGIN.get(String.valueOf(entryDN),
rejectMessage);
}
logToRejectWriter(lines, m);
suffix.removePending(entryDN);
continue;
}
}
// Make sure that the entry is valid as per the server schema if it is
// appropriate to do so.
if (checkSchema)
{
//Add the RDN attributes.
addRDNAttributesIfNecessary(entryDN,userAttributes,
operationalAttributes);
//Add any superior objectclass(s) missing in the objectclass map.
addSuperiorObjectClasses(objectClasses);
MessageBuilder invalidReason = new MessageBuilder();
if (! entry.conformsToSchema(null, false, true, false, invalidReason))
{
Message message = ERR_LDIF_SCHEMA_VIOLATION.get(
String.valueOf(entryDN),
lastEntryLineNumber,
invalidReason.toString());
logToRejectWriter(lines, message);
suffix.removePending(entryDN);
continue;
}
}
entryInfo.setEntryID(entryID);
entryInfo.setSuffix(suffix);
// The entry should be included in the import, so return it.
return entry;
}
}
/**
* Reads the next entry from the LDIF source.
*
* @param checkSchema Indicates whether this reader should perform schema
* checking on the entry before returning it to the
* caller. Note that some basic schema checking (like
* refusing multiple values for a single-valued
* attribute) may always be performed.
*
*
* @return The next entry read from the LDIF source, or <CODE>null</CODE> if
* the end of the LDIF data is reached.
*
* @throws IOException If an I/O problem occurs while reading from the file.
*
* @throws LDIFException If the information read cannot be parsed as an LDIF
* entry.
*/
public Entry readEntry(boolean checkSchema)
throws IOException, LDIFException
{
while (true)
{
// Read the set of lines that make up the next entry.
LinkedList<StringBuilder> lines = readEntryLines();
if (lines == null)
{
return null;
}
lastEntryBodyLines = lines;
lastEntryHeaderLines = new LinkedList<StringBuilder>();
// Read the DN of the entry and see if it is one that should be included
// in the import.
DN entryDN = readDN(lines);
if (entryDN == null)
{
// This should only happen if the LDIF starts with the "version:" line
// and has a blank line immediately after that. In that case, simply
// read and return the next entry.
continue;
}
else if (!importConfig.includeEntry(entryDN))
{
if (debugEnabled())
{
TRACER.debugInfo("Skipping entry %s because the DN is not one that " +
"should be included based on the include and exclude branches.",
entryDN);
}
entriesRead.incrementAndGet();
Message message = ERR_LDIF_SKIP.get(String.valueOf(entryDN));
logToSkipWriter(lines, message);
continue;
}
else
{
entriesRead.incrementAndGet();
}
// Read the set of attributes from the entry.
HashMap<ObjectClass,String> objectClasses =
new HashMap<ObjectClass,String>();
HashMap<AttributeType,List<AttributeBuilder>> userAttrBuilders =
new HashMap<AttributeType,List<AttributeBuilder>>();
HashMap<AttributeType,List<AttributeBuilder>> operationalAttrBuilders =
new HashMap<AttributeType,List<AttributeBuilder>>();
try
{
for (StringBuilder line : lines)
{
readAttribute(lines, line, entryDN, objectClasses, userAttrBuilders,
operationalAttrBuilders, checkSchema);
}
}
catch (LDIFException e)
{
throw e;
}
// Create the entry and see if it is one that should be included in the
// import.
HashMap<AttributeType,List<Attribute>> userAttributes =
new HashMap<AttributeType,List<Attribute>>(
userAttrBuilders.size());
HashMap<AttributeType,List<Attribute>> operationalAttributes =
new HashMap<AttributeType,List<Attribute>>(
operationalAttrBuilders.size());
for (Map.Entry<AttributeType, List<AttributeBuilder>>
attrTypeEntry : userAttrBuilders.entrySet())
{
AttributeType attrType = attrTypeEntry.getKey();
List<AttributeBuilder> attrBuilderList = attrTypeEntry.getValue();
List<Attribute> attrList =
new ArrayList<Attribute>(attrBuilderList.size());
for (AttributeBuilder builder : attrBuilderList)
{
attrList.add(builder.toAttribute());
}
userAttributes.put(attrType, attrList);
}
for (Map.Entry<AttributeType, List<AttributeBuilder>>
attrTypeEntry : operationalAttrBuilders.entrySet())
{
AttributeType attrType = attrTypeEntry.getKey();
List<AttributeBuilder> attrBuilderList = attrTypeEntry.getValue();
List<Attribute> attrList =
new ArrayList<Attribute>(attrBuilderList.size());
for (AttributeBuilder builder : attrBuilderList)
{
attrList.add(builder.toAttribute());
}
operationalAttributes.put(attrType, attrList);
}
Entry entry = new Entry(entryDN, objectClasses, userAttributes,
operationalAttributes);
TRACER.debugProtocolElement(DebugLogLevel.VERBOSE, entry.toString());
try
{
if (! importConfig.includeEntry(entry))
{
if (debugEnabled())
{
TRACER.debugInfo("Skipping entry %s because the DN is not one " +
"that should be included based on the include and exclude " +
"filters.", entryDN);
}
Message message = ERR_LDIF_SKIP.get(String.valueOf(entryDN));
logToSkipWriter(lines, message);
continue;
}
}
catch (Exception e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
Message message = ERR_LDIF_COULD_NOT_EVALUATE_FILTERS_FOR_IMPORT.
get(String.valueOf(entry.getDN()), lastEntryLineNumber,
String.valueOf(e));
throw new LDIFException(message, lastEntryLineNumber, true, e);
}
// If we should invoke import plugins, then do so.
if (importConfig.invokeImportPlugins())
{
PluginResult.ImportLDIF pluginResult =
pluginConfigManager.invokeLDIFImportPlugins(importConfig, entry);
if (! pluginResult.continueProcessing())
{
Message m;
Message rejectMessage = pluginResult.getErrorMessage();
if (rejectMessage == null)
{
m = ERR_LDIF_REJECTED_BY_PLUGIN_NOMESSAGE.get(
String.valueOf(entryDN));
}
else
{
m = ERR_LDIF_REJECTED_BY_PLUGIN.get(String.valueOf(entryDN),
rejectMessage);
}
logToRejectWriter(lines, m);
continue;
}
}
// Make sure that the entry is valid as per the server schema if it is
// appropriate to do so.
if (checkSchema)
{
MessageBuilder invalidReason = new MessageBuilder();
if (! entry.conformsToSchema(null, false, true, false, invalidReason))
{
Message message = ERR_LDIF_SCHEMA_VIOLATION.get(
String.valueOf(entryDN),
lastEntryLineNumber,
invalidReason.toString());
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber, true);
}
//Add any superior objectclass(s) missing in an entries
//objectclass map.
addSuperiorObjectClasses(entry.getObjectClasses());
}
// The entry should be included in the import, so return it.
return entry;
}
}
/**
* Reads the next change record from the LDIF source.
*
* @param defaultAdd Indicates whether the change type should default to
* "add" if none is explicitly provided.
*
* @return The next change record from the LDIF source, or <CODE>null</CODE>
* if the end of the LDIF data is reached.
*
* @throws IOException If an I/O problem occurs while reading from the file.
*
* @throws LDIFException If the information read cannot be parsed as an LDIF
* entry.
*/
public ChangeRecordEntry readChangeRecord(boolean defaultAdd)
throws IOException, LDIFException
{
while (true)
{
// Read the set of lines that make up the next entry.
LinkedList<StringBuilder> lines = readEntryLines();
if (lines == null)
{
return null;
}
// Read the DN of the entry and see if it is one that should be included
// in the import.
DN entryDN = readDN(lines);
if (entryDN == null)
{
// This should only happen if the LDIF starts with the "version:" line
// and has a blank line immediately after that. In that case, simply
// read and return the next entry.
continue;
}
String changeType = readChangeType(lines);
ChangeRecordEntry entry;
if(changeType != null)
{
if(changeType.equals("add"))
{
entry = parseAddChangeRecordEntry(entryDN, lines);
} else if (changeType.equals("delete"))
{
entry = parseDeleteChangeRecordEntry(entryDN, lines);
} else if (changeType.equals("modify"))
{
entry = parseModifyChangeRecordEntry(entryDN, lines);
} else if (changeType.equals("modrdn"))
{
entry = parseModifyDNChangeRecordEntry(entryDN, lines);
} else if (changeType.equals("moddn"))
{
entry = parseModifyDNChangeRecordEntry(entryDN, lines);
} else
{
Message message = ERR_LDIF_INVALID_CHANGETYPE_ATTRIBUTE.get(
changeType, "add, delete, modify, moddn, modrdn");
throw new LDIFException(message, lastEntryLineNumber, false);
}
} else
{
// default to "add"?
if(defaultAdd)
{
entry = parseAddChangeRecordEntry(entryDN, lines);
} else
{
Message message = ERR_LDIF_INVALID_CHANGETYPE_ATTRIBUTE.get(
null, "add, delete, modify, moddn, modrdn");
throw new LDIFException(message, lastEntryLineNumber, false);
}
}
return entry;
}
}
/**
* Reads a set of lines from the next entry in the LDIF source.
*
* @return A set of lines from the next entry in the LDIF source.
*
* @throws IOException If a problem occurs while reading from the LDIF
* source.
*
* @throws LDIFException If the information read is not valid LDIF.
*/
private LinkedList<StringBuilder> readEntryLines()
throws IOException, LDIFException
{
// Read the entry lines into a buffer.
LinkedList<StringBuilder> lines = new LinkedList<StringBuilder>();
int lastLine = -1;
if(reader == null)
{
return null;
}
while (true)
{
String line = reader.readLine();
lineNumber++;
if (line == null)
{
// This must mean that we have reached the end of the LDIF source.
// If the set of lines read so far is empty, then move onto the next
// file or return null. Otherwise, break out of this loop.
if (lines.isEmpty())
{
reader = importConfig.nextReader();
if (reader == null)
{
return null;
}
else
{
return readEntryLines();
}
}
else
{
break;
}
}
else if (line.length() == 0)
{
// This is a blank line. If the set of lines read so far is empty,
// then just skip over it. Otherwise, break out of this loop.
if (lines.isEmpty())
{
continue;
}
else
{
break;
}
}
else if (line.charAt(0) == '#')
{
// This is a comment. Ignore it.
continue;
}
else if ((line.charAt(0) == ' ') || (line.charAt(0) == '\t'))
{
// This is a continuation of the previous line. If there is no
// previous line, then that's a problem. Note that while RFC 2849
// technically only allows a space in this position, both OpenLDAP and
// the Sun Java System Directory Server allow a tab as well, so we will
// too for compatibility reasons. See issue #852 for details.
if (lastLine >= 0)
{
lines.get(lastLine).append(line.substring(1));
}
else
{
Message message =
ERR_LDIF_INVALID_LEADING_SPACE.get(lineNumber, line);
logToRejectWriter(lines, message);
throw new LDIFException(message, lineNumber, false);
}
}
else
{
// This is a new line.
if (lines.isEmpty())
{
lastEntryLineNumber = lineNumber;
}
if(((byte)line.charAt(0) == (byte)0xEF) &&
((byte)line.charAt(1) == (byte)0xBB) &&
((byte)line.charAt(2) == (byte)0xBF))
{
// This is a UTF-8 BOM that Java doesn't skip. We will skip it here.
line = line.substring(3, line.length());
}
lines.add(new StringBuilder(line));
lastLine++;
}
}
return lines;
}
/**
* Reads the DN of the entry from the provided list of lines. The DN must be
* the first line in the list, unless the first line starts with "version",
* in which case the DN should be the second line.
*
* @param lines The set of lines from which the DN should be read.
*
* @return The decoded entry DN.
*
* @throws LDIFException If DN is not the first element in the list (or the
* second after the LDIF version), or if a problem
* occurs while trying to parse it.
*/
private DN readDN(LinkedList<StringBuilder> lines)
throws LDIFException
{
if (lines.isEmpty())
{
// This is possible if the contents of the first "entry" were just
// the version identifier. If that is the case, then return null and
// use that as a signal to the caller to go ahead and read the next entry.
return null;
}
StringBuilder line = lines.remove();
lastEntryHeaderLines.add(line);
int colonPos = line.indexOf(":");
if (colonPos <= 0)
{
Message message =
ERR_LDIF_NO_ATTR_NAME.get(lastEntryLineNumber, line.toString());
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber, true);
}
String attrName = toLowerCase(line.substring(0, colonPos));
if (attrName.equals("version"))
{
// This is the version line, and we can skip it.
return readDN(lines);
}
else if (! attrName.equals("dn"))
{
Message message =
ERR_LDIF_NO_DN.get(lastEntryLineNumber, line.toString());
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber, true);
}
// Look at the character immediately after the colon. If there is none,
// then assume the null DN. If it is another colon, then the DN must be
// base64-encoded. Otherwise, it may be one or more spaces.
int length = line.length();
if (colonPos == (length-1))
{
return DN.nullDN();
}
if (line.charAt(colonPos+1) == ':')
{
// The DN is base64-encoded. Find the first non-blank character and
// take the rest of the line, base64-decode it, and parse it as a DN.
int pos = colonPos+2;
while ((pos < length) && (line.charAt(pos) == ' '))
{
pos++;
}
String encodedDNStr = line.substring(pos);
String dnStr;
try
{
dnStr = new String(Base64.decode(encodedDNStr), "UTF-8");
}
catch (Exception e)
{
// The value did not have a valid base64-encoding.
if (debugEnabled())
{
TRACER.debugInfo("Base64 decode failed for dn: ",
line.substring(pos));
}
Message message =
ERR_LDIF_COULD_NOT_BASE64_DECODE_DN.get(
lastEntryLineNumber, line,
String.valueOf(e));
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber, true, e);
}
try
{
return DN.decode(dnStr);
}
catch (DirectoryException de)
{
if (debugEnabled())
{
TRACER.debugInfo("DN decode failed for: ", dnStr);
}
Message message = ERR_LDIF_INVALID_DN.get(
lastEntryLineNumber, line.toString(),
de.getMessageObject());
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber, true, de);
}
catch (Exception e)
{
if (debugEnabled())
{
TRACER.debugInfo("DN decode failed for: ", dnStr);
}
Message message = ERR_LDIF_INVALID_DN.get(
lastEntryLineNumber, line.toString(),
String.valueOf(e));
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber, true, e);
}
}
else
{
// The rest of the value should be the DN. Skip over any spaces and
// attempt to decode the rest of the line as the DN.
int pos = colonPos+1;
while ((pos < length) && (line.charAt(pos) == ' '))
{
pos++;
}
String dnString = line.substring(pos);
try
{
return DN.decode(dnString);
}
catch (DirectoryException de)
{
if (debugEnabled())
{
TRACER.debugInfo("DN decode failed for: ", line.substring(pos));
}
Message message = ERR_LDIF_INVALID_DN.get(
lastEntryLineNumber, line.toString(), de.getMessageObject());
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber, true, de);
}
catch (Exception e)
{
if (debugEnabled())
{
TRACER.debugInfo("DN decode failed for: ", line.substring(pos));
}
Message message = ERR_LDIF_INVALID_DN.get(
lastEntryLineNumber, line.toString(),
String.valueOf(e));
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber, true, e);
}
}
}
/**
* Reads the changetype of the entry from the provided list of lines. If
* there is no changetype attribute then an add is assumed.
*
* @param lines The set of lines from which the DN should be read.
*
* @return The decoded entry DN.
*
* @throws LDIFException If DN is not the first element in the list (or the
* second after the LDIF version), or if a problem
* occurs while trying to parse it.
*/
private String readChangeType(LinkedList<StringBuilder> lines)
throws LDIFException
{
if (lines.isEmpty())
{
// Error. There must be other entries.
return null;
}
StringBuilder line = lines.get(0);
lastEntryHeaderLines.add(line);
int colonPos = line.indexOf(":");
if (colonPos <= 0)
{
Message message = ERR_LDIF_NO_ATTR_NAME.get(
lastEntryLineNumber, line.toString());
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber, true);
}
String attrName = toLowerCase(line.substring(0, colonPos));
if (! attrName.equals("changetype"))
{
// No changetype attribute - return null
return null;
} else
{
// Remove the line
lines.remove();
}
// Look at the character immediately after the colon. If there is none,
// then no value was specified. Throw an exception
int length = line.length();
if (colonPos == (length-1))
{
Message message = ERR_LDIF_INVALID_CHANGETYPE_ATTRIBUTE.get(
null, "add, delete, modify, moddn, modrdn");
throw new LDIFException(message, lastEntryLineNumber, false );
}
if (line.charAt(colonPos+1) == ':')
{
// The change type is base64-encoded. Find the first non-blank
// character and
// take the rest of the line, and base64-decode it.
int pos = colonPos+2;
while ((pos < length) && (line.charAt(pos) == ' '))
{
pos++;
}
String encodedChangeTypeStr = line.substring(pos);
String changeTypeStr;
try
{
changeTypeStr = new String(Base64.decode(encodedChangeTypeStr),
"UTF-8");
}
catch (Exception e)
{
// The value did not have a valid base64-encoding.
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
Message message = ERR_LDIF_COULD_NOT_BASE64_DECODE_DN.get(
lastEntryLineNumber, line,
String.valueOf(e));
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber, true, e);
}
return changeTypeStr;
}
else
{
// The rest of the value should be the changetype.
// Skip over any spaces and
// attempt to decode the rest of the line as the changetype string.
int pos = colonPos+1;
while ((pos < length) && (line.charAt(pos) == ' '))
{
pos++;
}
return line.substring(pos);
}
}
/**
* Decodes the provided line as an LDIF attribute and adds it to the
* appropriate hash.
*
* @param lines The full set of lines that comprise the
* entry (used for writing reject information).
* @param line The line to decode.
* @param entryDN The DN of the entry being decoded.
* @param objectClasses The set of objectclasses decoded so far for
* the current entry.
* @param userAttrBuilders The map of user attribute builders decoded
* so far for the current entry.
* @param operationalAttrBuilders The map of operational attribute builders
* decoded so far for the current entry.
* @param checkSchema Indicates whether to perform schema
* validation for the attribute.
*
* @throws LDIFException If a problem occurs while trying to decode the
* attribute contained in the provided entry.
*/
private void readAttribute(LinkedList<StringBuilder> lines,
StringBuilder line, DN entryDN,
HashMap<ObjectClass,String> objectClasses,
HashMap<AttributeType,List<AttributeBuilder>> userAttrBuilders,
HashMap<AttributeType,List<AttributeBuilder>> operationalAttrBuilders,
boolean checkSchema)
throws LDIFException
{
// Parse the attribute type description.
int colonPos = parseColonPosition(lines, line);
String attrDescr = line.substring(0, colonPos);
final Attribute attribute = parseAttrDescription(attrDescr);
final String attrName = attribute.getName();
final String lowerName = toLowerCase(attrName);
// Now parse the attribute value.
ByteString value = parseSingleValue(lines, line, entryDN,
colonPos, attrName);
// See if this is an objectclass or an attribute. Then get the
// corresponding definition and add the value to the appropriate hash.
if (lowerName.equals("objectclass"))
{
if (! importConfig.includeObjectClasses())
{
if (debugEnabled())
{
TRACER.debugVerbose("Skipping objectclass %s for entry %s due to " +
"the import configuration.", value, entryDN);
}
return;
}
String ocName = value.toString().trim();
String lowerOCName = toLowerCase(ocName);
ObjectClass objectClass = DirectoryServer.getObjectClass(lowerOCName);
if (objectClass == null)
{
objectClass = DirectoryServer.getDefaultObjectClass(ocName);
}
if (objectClasses.containsKey(objectClass))
{
logError(WARN_LDIF_DUPLICATE_OBJECTCLASS.get(
String.valueOf(entryDN), lastEntryLineNumber, ocName));
}
else
{
objectClasses.put(objectClass, ocName);
}
}
else
{
AttributeType attrType = DirectoryServer.getAttributeType(lowerName);
if (attrType == null)
{
attrType = DirectoryServer.getDefaultAttributeType(attrName);
}
if (! importConfig.includeAttribute(attrType))
{
if (debugEnabled())
{
TRACER.debugVerbose("Skipping attribute %s for entry %s due to the " +
"import configuration.", attrName, entryDN);
}
return;
}
//The attribute is not being ignored so check for binary option.
if(checkSchema && !attrType.isBinary())
{
if(attribute.hasOption("binary"))
{
Message message = ERR_LDIF_INVALID_ATTR_OPTION.get(
String.valueOf(entryDN),lastEntryLineNumber, attrName);
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber,true);
}
}
if (checkSchema &&
(DirectoryServer.getSyntaxEnforcementPolicy() !=
AcceptRejectWarn.ACCEPT))
{
MessageBuilder invalidReason = new MessageBuilder();
if (! attrType.getSyntax().valueIsAcceptable(value, invalidReason))
{
Message message = WARN_LDIF_VALUE_VIOLATES_SYNTAX.get(
String.valueOf(entryDN),
lastEntryLineNumber, value.toString(),
attrName, invalidReason.toString());
if (DirectoryServer.getSyntaxEnforcementPolicy() ==
AcceptRejectWarn.WARN)
{
logError(message);
}
else
{
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber,
true);
}
}
}
AttributeValue attributeValue =
AttributeValues.create(attrType, value);
List<AttributeBuilder> attrList;
if (attrType.isOperational())
{
attrList = operationalAttrBuilders.get(attrType);
if (attrList == null)
{
AttributeBuilder builder = new AttributeBuilder(attribute, true);
builder.add(attributeValue);
attrList = new ArrayList<AttributeBuilder>();
attrList.add(builder);
operationalAttrBuilders.put(attrType, attrList);
return;
}
}
else
{
attrList = userAttrBuilders.get(attrType);
if (attrList == null)
{
AttributeBuilder builder = new AttributeBuilder(attribute, true);
builder.add(attributeValue);
attrList = new ArrayList<AttributeBuilder>();
attrList.add(builder);
userAttrBuilders.put(attrType, attrList);
return;
}
}
// Check to see if any of the attributes in the list have the same set of
// options. If so, then try to add a value to that attribute.
for (AttributeBuilder a : attrList)
{
if (a.optionsEqual(attribute.getOptions()))
{
if (!a.add(attributeValue) && checkSchema)
{
Message message = WARN_LDIF_DUPLICATE_ATTR.get(
String.valueOf(entryDN),
lastEntryLineNumber, attrName,
value.toString());
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber,
true);
}
if (attrType.isSingleValue() && (a.size() > 1) && checkSchema)
{
Message message = ERR_LDIF_MULTIPLE_VALUES_FOR_SINGLE_VALUED_ATTR
.get(String.valueOf(entryDN),
lastEntryLineNumber, attrName);
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber, true);
}
return;
}
}
// No set of matching options was found, so create a new one and
// add it to the list.
AttributeBuilder builder = new AttributeBuilder(attribute, true);
builder.add(attributeValue);
attrList.add(builder);
}
}
/**
* Decodes the provided line as an LDIF attribute and returns the
* Attribute (name and values) for the specified attribute name.
*
* @param lines The full set of lines that comprise the
* entry (used for writing reject information).
* @param line The line to decode.
* @param entryDN The DN of the entry being decoded.
* @param attributeName The name and options of the attribute to
* return the values for.
*
* @return The attribute in octet string form.
* @throws LDIFException If a problem occurs while trying to decode
* the attribute contained in the provided
* entry or if the parsed attribute name does
* not match the specified attribute name.
*/
private Attribute readSingleValueAttribute(
LinkedList<StringBuilder> lines, StringBuilder line, DN entryDN,
String attributeName) throws LDIFException
{
// Parse the attribute type description.
int colonPos = parseColonPosition(lines, line);
String attrDescr = line.substring(0, colonPos);
Attribute attribute = parseAttrDescription(attrDescr);
String attrName = attribute.getName();
if (attributeName != null)
{
Attribute expectedAttr = parseAttrDescription(attributeName);
if (!attribute.equals(expectedAttr))
{
Message message = ERR_LDIF_INVALID_CHANGERECORD_ATTRIBUTE.get(
attrDescr, attributeName);
throw new LDIFException(message, lastEntryLineNumber, false);
}
}
// Now parse the attribute value.
ByteString value = parseSingleValue(lines, line, entryDN,
colonPos, attrName);
AttributeBuilder builder = new AttributeBuilder(attribute, true);
AttributeType attrType = attribute.getAttributeType();
builder.add(AttributeValues.create(attrType, value));
return builder.toAttribute();
}
/**
* Retrieves the starting line number for the last entry read from the LDIF
* source.
*
* @return The starting line number for the last entry read from the LDIF
* source.
*/
public long getLastEntryLineNumber()
{
return lastEntryLineNumber;
}
/**
* Rejects the last entry read from the LDIF. This method is intended for use
* by components that perform their own validation of entries (e.g., backends
* during import processing) in which the entry appeared valid to the LDIF
* reader but some other problem was encountered.
*
* @param message A human-readable message providing the reason that the
* last entry read was not acceptable.
*/
public void rejectLastEntry(Message message)
{
entriesRejected.incrementAndGet();
BufferedWriter rejectWriter = importConfig.getRejectWriter();
if (rejectWriter != null)
{
try
{
if ((message != null) && (message.length() > 0))
{
rejectWriter.write("# ");
rejectWriter.write(message.toString());
rejectWriter.newLine();
}
for (StringBuilder sb : lastEntryHeaderLines)
{
rejectWriter.write(sb.toString());
rejectWriter.newLine();
}
for (StringBuilder sb : lastEntryBodyLines)
{
rejectWriter.write(sb.toString());
rejectWriter.newLine();
}
rejectWriter.newLine();
}
catch (Exception e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
}
}
}
/**
* Log the specified entry and messages in the reject writer. The method is
* intended to be used in a threaded environment, where individual import
* threads need to log an entry and message to the reject file.
*
* @param e The entry to log.
* @param message The message to log.
*/
public synchronized void rejectEntry(Entry e, Message message) {
BufferedWriter rejectWriter = importConfig.getRejectWriter();
entriesRejected.incrementAndGet();
if (rejectWriter != null) {
try {
if ((message != null) && (message.length() > 0)) {
rejectWriter.write("# ");
rejectWriter.write(message.toString());
rejectWriter.newLine();
}
String dnStr = e.getDN().toString();
rejectWriter.write(dnStr);
rejectWriter.newLine();
List<StringBuilder> eLDIF = e.toLDIF();
for(StringBuilder l : eLDIF) {
rejectWriter.write(l.toString());
rejectWriter.newLine();
}
rejectWriter.newLine();
} catch (IOException ex) {
if (debugEnabled())
TRACER.debugCaught(DebugLogLevel.ERROR, ex);
}
}
}
/**
* Closes this LDIF reader and the underlying file or input stream.
*/
public void close()
{
// If we should invoke import plugins, then do so.
if (importConfig.invokeImportPlugins())
{
// Inform LDIF import plugins that an import session is ending
pluginConfigManager.invokeLDIFImportEndPlugins(importConfig);
}
importConfig.close();
}
/**
* Parse an AttributeDescription (an attribute type name and its
* options).
*
* @param attrDescr
* The attribute description to be parsed.
* @return A new attribute with no values, representing the
* attribute type and its options.
*/
public static Attribute parseAttrDescription(String attrDescr)
{
AttributeBuilder builder;
int semicolonPos = attrDescr.indexOf(';');
if (semicolonPos > 0)
{
builder = new AttributeBuilder(attrDescr.substring(0, semicolonPos));
int nextPos = attrDescr.indexOf(';', semicolonPos + 1);
while (nextPos > 0)
{
String option = attrDescr.substring(semicolonPos + 1, nextPos);
if (option.length() > 0)
{
builder.setOption(option);
semicolonPos = nextPos;
nextPos = attrDescr.indexOf(';', semicolonPos + 1);
}
}
String option = attrDescr.substring(semicolonPos + 1);
if (option.length() > 0)
{
builder.setOption(option);
}
}
else
{
builder = new AttributeBuilder(attrDescr);
}
if(builder.getAttributeType().isBinary())
{
//resetting doesn't hurt and returns false.
builder.setOption("binary");
}
return builder.toAttribute();
}
/**
* Retrieves the total number of entries read so far by this LDIF reader,
* including those that have been ignored or rejected.
*
* @return The total number of entries read so far by this LDIF reader.
*/
public long getEntriesRead()
{
return entriesRead.get();
}
/**
* Retrieves the total number of entries that have been ignored so far by this
* LDIF reader because they did not match the import criteria.
*
* @return The total number of entries ignored so far by this LDIF reader.
*/
public long getEntriesIgnored()
{
return entriesIgnored.get();
}
/**
* Retrieves the total number of entries rejected so far by this LDIF reader.
* This includes both entries that were rejected because of internal
* validation failure (e.g., they didn't conform to the defined server
* schema) or an external validation failure (e.g., the component using this
* LDIF reader didn't accept the entry because it didn't have a parent).
*
* @return The total number of entries rejected so far by this LDIF reader.
*/
public long getEntriesRejected()
{
return entriesRejected.get();
}
/**
* Parse a modifyDN change record entry from LDIF.
*
* @param entryDN
* The name of the entry being modified.
* @param lines
* The lines to parse.
* @return Returns the parsed modifyDN change record entry.
* @throws LDIFException
* If there was an error when parsing the change record.
*/
private ChangeRecordEntry parseModifyDNChangeRecordEntry(DN entryDN,
LinkedList<StringBuilder> lines) throws LDIFException {
DN newSuperiorDN = null;
RDN newRDN;
boolean deleteOldRDN;
if(lines.isEmpty())
{
Message message = ERR_LDIF_NO_MOD_DN_ATTRIBUTES.get();
throw new LDIFException(message, lineNumber, true);
}
StringBuilder line = lines.remove();
String rdnStr = getModifyDNAttributeValue(lines, line, entryDN, "newrdn");
try
{
newRDN = RDN.decode(rdnStr);
} catch (DirectoryException de)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, de);
}
Message message = ERR_LDIF_INVALID_DN.get(
lineNumber, line.toString(), de.getMessageObject());
throw new LDIFException(message, lineNumber, true);
} catch (Exception e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
Message message =
ERR_LDIF_INVALID_DN.get(lineNumber, line.toString(), e.getMessage());
throw new LDIFException(message, lineNumber, true);
}
if(lines.isEmpty())
{
Message message = ERR_LDIF_NO_DELETE_OLDRDN_ATTRIBUTE.get();
throw new LDIFException(message, lineNumber, true);
}
lineNumber++;
line = lines.remove();
String delStr = getModifyDNAttributeValue(lines, line,
entryDN, "deleteoldrdn");
if(delStr.equalsIgnoreCase("false") ||
delStr.equalsIgnoreCase("no") ||
delStr.equalsIgnoreCase("0"))
{
deleteOldRDN = false;
} else if(delStr.equalsIgnoreCase("true") ||
delStr.equalsIgnoreCase("yes") ||
delStr.equalsIgnoreCase("1"))
{
deleteOldRDN = true;
} else
{
Message message = ERR_LDIF_INVALID_DELETE_OLDRDN_ATTRIBUTE.get(delStr);
throw new LDIFException(message, lineNumber, true);
}
if(!lines.isEmpty())
{
lineNumber++;
line = lines.remove();
String dnStr = getModifyDNAttributeValue(lines, line,
entryDN, "newsuperior");
try
{
newSuperiorDN = DN.decode(dnStr);
} catch (DirectoryException de)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, de);
}
Message message = ERR_LDIF_INVALID_DN.get(
lineNumber, line.toString(), de.getMessageObject());
throw new LDIFException(message, lineNumber, true);
} catch (Exception e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
Message message = ERR_LDIF_INVALID_DN.get(
lineNumber, line.toString(), e.getMessage());
throw new LDIFException(message, lineNumber, true);
}
}
return new ModifyDNChangeRecordEntry(entryDN, newRDN, deleteOldRDN,
newSuperiorDN);
}
/**
* Return the string value for the specified attribute name which only
* has one value.
*
* @param lines
* The set of lines for this change record entry.
* @param line
* The line currently being examined.
* @param entryDN
* The name of the entry being modified.
* @param attributeName
* The attribute name
* @return the string value for the attribute name.
* @throws LDIFException
* If a problem occurs while attempting to determine the
* attribute value.
*/
private String getModifyDNAttributeValue(LinkedList<StringBuilder> lines,
StringBuilder line,
DN entryDN,
String attributeName) throws LDIFException
{
Attribute attr =
readSingleValueAttribute(lines, line, entryDN, attributeName);
return attr.iterator().next().getValue().toString();
}
/**
* Parse a modify change record entry from LDIF.
*
* @param entryDN
* The name of the entry being modified.
* @param lines
* The lines to parse.
* @return Returns the parsed modify change record entry.
* @throws LDIFException
* If there was an error when parsing the change record.
*/
private ChangeRecordEntry parseModifyChangeRecordEntry(DN entryDN,
LinkedList<StringBuilder> lines) throws LDIFException {
List<RawModification> modifications = new ArrayList<RawModification>();
while(!lines.isEmpty())
{
ModificationType modType;
StringBuilder line = lines.remove();
Attribute attr =
readSingleValueAttribute(lines, line, entryDN, null);
String name = attr.getName();
// Get the attribute description
String attrDescr = attr.iterator().next().getValue().toString();
String lowerName = toLowerCase(name);
if (lowerName.equals("add"))
{
modType = ModificationType.ADD;
}
else if (lowerName.equals("delete"))
{
modType = ModificationType.DELETE;
}
else if (lowerName.equals("replace"))
{
modType = ModificationType.REPLACE;
}
else if (lowerName.equals("increment"))
{
modType = ModificationType.INCREMENT;
}
else
{
// Invalid attribute name.
Message message = ERR_LDIF_INVALID_MODIFY_ATTRIBUTE.get(name,
"add, delete, replace, increment");
throw new LDIFException(message, lineNumber, true);
}
// Now go through the rest of the attributes till the "-" line is
// reached.
Attribute modAttr = LDIFReader.parseAttrDescription(attrDescr);
AttributeBuilder builder = new AttributeBuilder(modAttr, true);
while (! lines.isEmpty())
{
line = lines.remove();
if(line.toString().equals("-"))
{
break;
}
Attribute a = readSingleValueAttribute(lines, line, entryDN, attrDescr);
builder.addAll(a);
}
LDAPAttribute ldapAttr = new LDAPAttribute(builder.toAttribute());
LDAPModification mod = new LDAPModification(modType, ldapAttr);
modifications.add(mod);
}
return new ModifyChangeRecordEntry(entryDN, modifications);
}
/**
* Parse a delete change record entry from LDIF.
*
* @param entryDN
* The name of the entry being deleted.
* @param lines
* The lines to parse.
* @return Returns the parsed delete change record entry.
* @throws LDIFException
* If there was an error when parsing the change record.
*/
private ChangeRecordEntry parseDeleteChangeRecordEntry(DN entryDN,
LinkedList<StringBuilder> lines) throws LDIFException {
if (!lines.isEmpty())
{
Message message = ERR_LDIF_INVALID_DELETE_ATTRIBUTES.get();
throw new LDIFException(message, lineNumber, true);
}
return new DeleteChangeRecordEntry(entryDN);
}
/**
* Parse an add change record entry from LDIF.
*
* @param entryDN
* The name of the entry being added.
* @param lines
* The lines to parse.
* @return Returns the parsed add change record entry.
* @throws LDIFException
* If there was an error when parsing the change record.
*/
private ChangeRecordEntry parseAddChangeRecordEntry(DN entryDN,
LinkedList<StringBuilder> lines) throws LDIFException {
HashMap<ObjectClass,String> objectClasses =
new HashMap<ObjectClass,String>();
HashMap<AttributeType,List<AttributeBuilder>> attrBuilders =
new HashMap<AttributeType, List<AttributeBuilder>>();
for(StringBuilder line : lines)
{
readAttribute(lines, line, entryDN, objectClasses,
attrBuilders, attrBuilders, importConfig.validateSchema());
}
// Reconstruct the object class attribute.
AttributeType ocType = DirectoryServer.getObjectClassAttributeType();
AttributeBuilder builder = new AttributeBuilder(ocType, "objectClass");
for (String value : objectClasses.values()) {
AttributeValue av = AttributeValues.create(ocType, value);
builder.add(av);
}
List<Attribute> ocAttrList = new ArrayList<Attribute>(1);
ocAttrList.add(builder.toAttribute());
HashMap<AttributeType,List<Attribute>> attributes =
new HashMap<AttributeType, List<Attribute>>(attrBuilders.size());
attributes.put(ocType, ocAttrList);
for (Map.Entry<AttributeType, List<AttributeBuilder>>
attrTypeEntry : attrBuilders.entrySet())
{
AttributeType attrType = attrTypeEntry.getKey();
List<AttributeBuilder> attrBuilderList = attrTypeEntry.getValue();
List<Attribute> attrList =
new ArrayList<Attribute>(attrBuilderList.size());
for (AttributeBuilder attrBuilder : attrBuilderList)
{
attrList.add(attrBuilder.toAttribute());
}
attributes.put(attrType, attrList);
}
return new AddChangeRecordEntry(entryDN, attributes);
}
/**
* Parse colon position in an attribute description.
*
* @param lines
* The current set of lines.
* @param line
* The current line.
* @return The colon position.
* @throws LDIFException
* If the colon was badly placed or not found.
*/
private int parseColonPosition(LinkedList<StringBuilder> lines,
StringBuilder line) throws LDIFException {
int colonPos = line.indexOf(":");
if (colonPos <= 0)
{
Message message = ERR_LDIF_NO_ATTR_NAME.get(
lastEntryLineNumber, line.toString());
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber, true);
}
return colonPos;
}
/**
* Parse a single attribute value from a line of LDIF.
*
* @param lines
* The current set of lines.
* @param line
* The current line.
* @param entryDN
* The DN of the entry being parsed.
* @param colonPos
* The position of the separator colon in the line.
* @param attrName
* The name of the attribute being parsed.
* @return The parsed attribute value.
* @throws LDIFException
* If an error occurred when parsing the attribute value.
*/
private ByteString parseSingleValue(
LinkedList<StringBuilder> lines,
StringBuilder line,
DN entryDN,
int colonPos,
String attrName) throws LDIFException {
// Look at the character immediately after the colon. If there is
// none, then assume an attribute with an empty value. If it is another
// colon, then the value must be base64-encoded. If it is a less-than
// sign, then assume that it is a URL. Otherwise, it is a regular value.
int length = line.length();
ByteString value;
if (colonPos == (length-1))
{
value = ByteString.empty();
}
else
{
char c = line.charAt(colonPos+1);
if (c == ':')
{
// The value is base64-encoded. Find the first non-blank
// character, take the rest of the line, and base64-decode it.
int pos = colonPos+2;
while ((pos < length) && (line.charAt(pos) == ' '))
{
pos++;
}
try
{
value = ByteString.wrap(Base64.decode(line.substring(pos)));
}
catch (Exception e)
{
// The value did not have a valid base64-encoding.
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
Message message = ERR_LDIF_COULD_NOT_BASE64_DECODE_ATTR.get(
String.valueOf(entryDN),
lastEntryLineNumber, line,
String.valueOf(e));
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber, true, e);
}
}
else if (c == '<')
{
// Find the first non-blank character, decode the rest of the
// line as a URL, and read its contents.
int pos = colonPos+2;
while ((pos < length) && (line.charAt(pos) == ' '))
{
pos++;
}
URL contentURL;
try
{
contentURL = new URL(line.substring(pos));
}
catch (Exception e)
{
// The URL was malformed or had an invalid protocol.
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
Message message = ERR_LDIF_INVALID_URL.get(String.valueOf(entryDN),
lastEntryLineNumber,
String.valueOf(attrName),
String.valueOf(e));
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber, true, e);
}
InputStream inputStream = null;
ByteStringBuilder builder;
try
{
builder = new ByteStringBuilder();
inputStream = contentURL.openConnection().getInputStream();
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) > 0)
{
builder.append(buffer, 0, bytesRead);
}
value = builder.toByteString();
}
catch (Exception e)
{
// We were unable to read the contents of that URL for some
// reason.
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
Message message = ERR_LDIF_URL_IO_ERROR.get(String.valueOf(entryDN),
lastEntryLineNumber,
String.valueOf(attrName),
String.valueOf(contentURL),
String.valueOf(e));
logToRejectWriter(lines, message);
throw new LDIFException(message, lastEntryLineNumber, true, e);
}
finally
{
if (inputStream != null)
{
try
{
inputStream.close();
} catch (Exception e) {}
}
}
}
else
{
// The rest of the line should be the value. Skip over any
// spaces and take the rest of the line as the value.
int pos = colonPos+1;
while ((pos < length) && (line.charAt(pos) == ' '))
{
pos++;
}
value = ByteString.valueOf(line.substring(pos));
}
}
return value;
}
/**
* Log a message to the reject writer if one is configured.
*
* @param lines
* The set of rejected lines.
* @param message
* The associated error message.
*/
private void logToRejectWriter(LinkedList<StringBuilder> lines,
Message message) {
entriesRejected.incrementAndGet();
BufferedWriter rejectWriter = importConfig.getRejectWriter();
if (rejectWriter != null)
{
logToWriter(rejectWriter, lines, message);
}
}
/**
* Log a message to the reject writer if one is configured.
*
* @param lines
* The set of rejected lines.
* @param message
* The associated error message.
*/
private void logToSkipWriter(LinkedList<StringBuilder> lines,
Message message) {
entriesIgnored.incrementAndGet();
BufferedWriter skipWriter = importConfig.getSkipWriter();
if (skipWriter != null)
{
logToWriter(skipWriter, lines, message);
}
}
/**
* Log a message to the given writer.
*
* @param writer
* The writer to write to.
* @param lines
* The set of rejected lines.
* @param message
* The associated error message.
*/
private void logToWriter(BufferedWriter writer,
LinkedList<StringBuilder> lines,
Message message)
{
if (writer != null)
{
try
{
writer.write("# ");
writer.write(String.valueOf(message));
writer.newLine();
for (StringBuilder sb : lines)
{
writer.write(sb.toString());
writer.newLine();
}
writer.newLine();
}
catch (Exception e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
}
}
}
/**
* Adds any missing RDN attributes to the entry that is being imported.
*/
private void addRDNAttributesIfNecessary(DN entryDN,
HashMap<AttributeType,List<Attribute>>userAttributes,
HashMap<AttributeType,List<Attribute>> operationalAttributes)
{
RDN rdn = entryDN.getRDN();
int numAVAs = rdn.getNumValues();
for (int i=0; i < numAVAs; i++)
{
AttributeType t = rdn.getAttributeType(i);
AttributeValue v = rdn.getAttributeValue(i);
String n = rdn.getAttributeName(i);
if (t.isOperational())
{
List<Attribute> attrList = operationalAttributes.get(t);
if (attrList == null)
{
attrList = new ArrayList<Attribute>();
attrList.add(Attributes.create(t, n, v));
operationalAttributes.put(t, attrList);
}
else
{
boolean found = false;
for (int j = 0; j < attrList.size(); j++)
{
Attribute a = attrList.get(j);
if (a.hasOptions())
{
continue;
}
if (!a.contains(v))
{
AttributeBuilder builder = new AttributeBuilder(a);
builder.add(v);
attrList.set(j, builder.toAttribute());
}
found = true;
break;
}
if (!found)
{
attrList.add(Attributes.create(t, n, v));
}
}
}
else
{
List<Attribute> attrList = userAttributes.get(t);
if (attrList == null)
{
attrList = new ArrayList<Attribute>();
attrList.add(Attributes.create(t, n, v));
userAttributes.put(t, attrList);
}
else
{
boolean found = false;
for (int j = 0; j < attrList.size(); j++)
{
Attribute a = attrList.get(j);
if (a.hasOptions())
{
continue;
}
if (!a.contains(v))
{
AttributeBuilder builder = new AttributeBuilder(a);
builder.add(v);
attrList.set(j, builder.toAttribute());
}
found = true;
break;
}
if (!found)
{
attrList.add(Attributes.create(t, n, v));
}
}
}
}
}
}