/*
* 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/nasutekds/resource/legal-notices/NasuTekDS.LICENSE
* or https://NasuTekDS.dev.java.net/NasuTekDS.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/nasutekds/resource/legal-notices/NasuTekDS.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-2009 Sun Microsystems, Inc.
*/
package org.nasutekds.server.tools;
import org.nasutekds.messages.Message;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.TreeMap;
import org.nasutekds.server.core.DirectoryServer;
import org.nasutekds.server.extensions.ConfigFileHandler;
import org.nasutekds.server.protocols.ldap.LDAPResultCode;
import org.nasutekds.server.types.*;
import org.nasutekds.server.util.LDIFReader;
import org.nasutekds.server.util.LDIFWriter;
import org.nasutekds.server.util.args.ArgumentException;
import org.nasutekds.server.util.args.ArgumentParser;
import org.nasutekds.server.util.args.BooleanArgument;
import org.nasutekds.server.util.args.StringArgument;
import static org.nasutekds.messages.ToolMessages.*;
import static org.nasutekds.server.tools.ToolConstants.*;
import static org.nasutekds.server.util.ServerConstants.PROPERTY_SCRIPT_NAME;
import static org.nasutekds.server.util.StaticUtils.*;
/**
* This class provides a program that may be used to determine the differences
* between two LDIF files, generating the output in LDIF change format. There
* are several things to note about the operation of this program:
* <BR>
* <UL>
* <LI>This program is only designed for cases in which both LDIF files to be
* compared will fit entirely in memory at the same time.</LI>
* <LI>This program will only compare live data in the LDIF files and will
* ignore comments and other elements that do not have any real impact on
* the way that the data is interpreted.</LI>
* <LI>The differences will be generated in such a way as to provide the
* maximum amount of information, so that there will be enough information
* for the changes to be reversed (i.e., it will not use the "replace"
* modification type but only the "add" and "delete" types, and contents
* of deleted entries will be included as comments).</LI>
* </UL>
*
*
* Note
* that this is only an option for cases in which both LDIF files can fit in
* memory. Also note that this will only compare live data in the LDIF files
* and will ignore comments and other elements that do not have any real impact
* on the way that the data is interpreted.
*/
public class LDIFDiff
{
/**
* The fully-qualified name of this class.
*/
private static final String CLASS_NAME = "org.nasutekds.server.tools.LDIFDiff";
/**
* Provides the command line arguments to the <CODE>mainDiff</CODE> method
* so that they can be processed.
*
* @param args The command line arguments provided to this program.
*/
public static void main(String[] args)
{
int exitCode = mainDiff(args, false, System.out, System.err);
if (exitCode != 0)
{
System.exit(filterExitCode(exitCode));
}
}
/**
* Parses the provided command line arguments and performs the appropriate
* LDIF diff operation.
*
* @param args The command line arguments provided to this
* program.
* @param serverInitialized Indicates whether the Directory Server has
* already been initialized (and therefore should
* not be initialized a second time).
* @param outStream The output stream to use for standard output, or
* {@code null} if standard output is not needed.
* @param errStream The output stream to use for standard error, or
* {@code null} if standard error is not needed.
*
* @return The return code for this operation. A value of zero indicates
* that all processing completed successfully. A nonzero value
* indicates that some problem occurred during processing.
*/
public static int mainDiff(String[] args, boolean serverInitialized,
OutputStream outStream, OutputStream errStream)
{
PrintStream out;
if (outStream == null)
{
out = NullOutputStream.printStream();
}
else
{
out = new PrintStream(outStream);
}
PrintStream err;
if (errStream == null)
{
err = NullOutputStream.printStream();
}
else
{
err = new PrintStream(errStream);
}
BooleanArgument overwriteExisting;
BooleanArgument showUsage;
BooleanArgument singleValueChanges;
BooleanArgument doCheckSchema;
StringArgument configClass;
StringArgument configFile;
StringArgument outputLDIF;
StringArgument sourceLDIF;
StringArgument targetLDIF;
StringArgument ignoreAttrsFile;
StringArgument ignoreEntriesFile;
Message toolDescription = INFO_LDIFDIFF_TOOL_DESCRIPTION.get();
ArgumentParser argParser = new ArgumentParser(CLASS_NAME, toolDescription,
false);
try
{
sourceLDIF = new StringArgument(
"sourceldif", 's', "sourceLDIF", true,
false, true, INFO_FILE_PLACEHOLDER.get(), null, null,
INFO_LDIFDIFF_DESCRIPTION_SOURCE_LDIF.get());
argParser.addArgument(sourceLDIF);
targetLDIF = new StringArgument(
"targetldif", 't', "targetLDIF", true,
false, true, INFO_FILE_PLACEHOLDER.get(), null, null,
INFO_LDIFDIFF_DESCRIPTION_TARGET_LDIF.get());
argParser.addArgument(targetLDIF);
outputLDIF = new StringArgument(
"outputldif", 'o', "outputLDIF", false,
false, true, INFO_FILE_PLACEHOLDER.get(), null, null,
INFO_LDIFDIFF_DESCRIPTION_OUTPUT_LDIF.get());
argParser.addArgument(outputLDIF);
ignoreAttrsFile = new StringArgument(
"ignoreattrs", 'a', "ignoreAttrs", false,
false, true, INFO_FILE_PLACEHOLDER.get(), null, null,
INFO_LDIFDIFF_DESCRIPTION_IGNORE_ATTRS.get());
argParser.addArgument(ignoreAttrsFile);
ignoreEntriesFile = new StringArgument(
"ignoreentries", 'e', "ignoreEntries", false,
false, true, INFO_FILE_PLACEHOLDER.get(), null, null,
INFO_LDIFDIFF_DESCRIPTION_IGNORE_ENTRIES.get());
argParser.addArgument(ignoreEntriesFile);
overwriteExisting =
new BooleanArgument(
"overwriteexisting", 'O',
"overwriteExisting",
INFO_LDIFDIFF_DESCRIPTION_OVERWRITE_EXISTING.get());
argParser.addArgument(overwriteExisting);
singleValueChanges =
new BooleanArgument(
"singlevaluechanges", 'S', "singleValueChanges",
INFO_LDIFDIFF_DESCRIPTION_SINGLE_VALUE_CHANGES.get());
argParser.addArgument(singleValueChanges);
doCheckSchema =
new BooleanArgument(
"checkschema", null, "checkSchema",
INFO_LDIFDIFF_DESCRIPTION_CHECK_SCHEMA.get());
argParser.addArgument(doCheckSchema);
configFile = new StringArgument("configfile", 'c', "configFile", false,
false, true,
INFO_CONFIGFILE_PLACEHOLDER.get(), null,
null,
INFO_DESCRIPTION_CONFIG_FILE.get());
configFile.setHidden(true);
argParser.addArgument(configFile);
configClass = new StringArgument("configclass", OPTION_SHORT_CONFIG_CLASS,
OPTION_LONG_CONFIG_CLASS, false,
false, true, INFO_CONFIGCLASS_PLACEHOLDER.get(),
ConfigFileHandler.class.getName(), null,
INFO_DESCRIPTION_CONFIG_CLASS.get());
configClass.setHidden(true);
argParser.addArgument(configClass);
showUsage = new BooleanArgument("showusage", OPTION_SHORT_HELP,
OPTION_LONG_HELP,
INFO_DESCRIPTION_USAGE.get());
argParser.addArgument(showUsage);
argParser.setUsageArgument(showUsage);
}
catch (ArgumentException ae)
{
Message message = ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage());
err.println(message);
return 1;
}
// Parse the command-line arguments provided to the program.
try
{
argParser.parseArguments(args);
}
catch (ArgumentException ae)
{
Message message = ERR_ERROR_PARSING_ARGS.get(ae.getMessage());
err.println(message);
err.println(argParser.getUsage());
return LDAPResultCode.CLIENT_SIDE_PARAM_ERROR;
}
// If we should just display usage or version information,
// then print it and exit.
if (argParser.usageOrVersionDisplayed())
{
return 0;
}
if (doCheckSchema.isPresent() && !configFile.isPresent())
{
String scriptName = System.getProperty(PROPERTY_SCRIPT_NAME);
if (scriptName == null)
{
scriptName = "ldif-diff";
}
Message message = WARN_LDIFDIFF_NO_CONFIG_FILE.get(scriptName);
err.println(message);
}
boolean checkSchema = configFile.isPresent() && doCheckSchema.isPresent();
if (! serverInitialized)
{
// Bootstrap the Directory Server configuration for use as a client.
DirectoryServer directoryServer = DirectoryServer.getInstance();
DirectoryServer.bootstrapClient();
// If we're to use the configuration then initialize it, along with the
// schema.
if (checkSchema)
{
try
{
DirectoryServer.initializeJMX();
}
catch (Exception e)
{
Message message = ERR_LDIFDIFF_CANNOT_INITIALIZE_JMX.get(
String.valueOf(configFile.getValue()),
e.getMessage());
err.println(message);
return 1;
}
try
{
directoryServer.initializeConfiguration(configClass.getValue(),
configFile.getValue());
}
catch (Exception e)
{
Message message = ERR_LDIFDIFF_CANNOT_INITIALIZE_CONFIG.get(
String.valueOf(configFile.getValue()),
e.getMessage());
err.println(message);
return 1;
}
try
{
directoryServer.initializeSchema();
}
catch (Exception e)
{
Message message = ERR_LDIFDIFF_CANNOT_INITIALIZE_SCHEMA.get(
String.valueOf(configFile.getValue()),
e.getMessage());
err.println(message);
return 1;
}
}
}
// Read in ignored entries and attributes if any
BufferedReader ignReader = null;
Collection<DN> ignoreEntries = new HashSet<DN>();
Collection<String> ignoreAttrs = new HashSet<String>();
if (ignoreAttrsFile.getValue() != null)
{
try
{
ignReader = new BufferedReader(
new FileReader(ignoreAttrsFile.getValue()));
String line = null;
while ((line = ignReader.readLine()) != null)
{
ignoreAttrs.add(line.toLowerCase());
}
ignReader.close();
}
catch (Exception e)
{
Message message = ERR_LDIFDIFF_CANNOT_READ_FILE_IGNORE_ATTRIBS.get(
ignoreAttrsFile.getValue(),
String.valueOf(e));
err.println(message);
return 1;
}
finally
{
try
{
ignReader.close();
}
catch (Exception e) {}
}
}
if (ignoreEntriesFile.getValue() != null)
{
try
{
ignReader = new BufferedReader(
new FileReader(ignoreEntriesFile.getValue()));
String line = null;
while ((line = ignReader.readLine()) != null)
{
try
{
DN dn = DN.decode(line);
ignoreEntries.add(dn);
}
catch (DirectoryException e)
{
Message message = INFO_LDIFDIFF_CANNOT_PARSE_STRING_AS_DN.get(
line, ignoreEntriesFile.getValue());
err.println(message);
}
}
ignReader.close();
}
catch (Exception e)
{
Message message = ERR_LDIFDIFF_CANNOT_READ_FILE_IGNORE_ENTRIES.get(
ignoreEntriesFile.getValue(),
String.valueOf(e));
err.println(message);
return 1;
}
finally
{
try
{
ignReader.close();
}
catch (Exception e) {}
}
}
// Open the source LDIF file and read it into a tree map.
LDIFReader reader;
LDIFImportConfig importConfig = new LDIFImportConfig(sourceLDIF.getValue());
try
{
reader = new LDIFReader(importConfig);
}
catch (Exception e)
{
Message message = ERR_LDIFDIFF_CANNOT_OPEN_SOURCE_LDIF.get(
sourceLDIF.getValue(),
String.valueOf(e));
err.println(message);
return 1;
}
TreeMap<DN,Entry> sourceMap = new TreeMap<DN,Entry>();
try
{
while (true)
{
Entry entry = reader.readEntry(checkSchema);
if (entry == null)
{
break;
}
if (! ignoreEntries.contains(entry.getDN()))
{
sourceMap.put(entry.getDN(), entry);
}
}
}
catch (Exception e)
{
Message message = ERR_LDIFDIFF_ERROR_READING_SOURCE_LDIF.get(
sourceLDIF.getValue(),
String.valueOf(e));
err.println(message);
return 1;
}
finally
{
try
{
reader.close();
} catch (Exception e) {}
}
// Open the target LDIF file and read it into a tree map.
importConfig = new LDIFImportConfig(targetLDIF.getValue());
try
{
reader = new LDIFReader(importConfig);
}
catch (Exception e)
{
Message message = ERR_LDIFDIFF_CANNOT_OPEN_TARGET_LDIF.get(
targetLDIF.getValue(),
String.valueOf(e));
err.println(message);
return 1;
}
TreeMap<DN,Entry> targetMap = new TreeMap<DN,Entry>();
try
{
while (true)
{
Entry entry = reader.readEntry(checkSchema);
if (entry == null)
{
break;
}
if (! ignoreEntries.contains(entry.getDN()))
{
targetMap.put(entry.getDN(), entry);
}
}
}
catch (Exception e)
{
Message message = ERR_LDIFDIFF_ERROR_READING_TARGET_LDIF.get(
targetLDIF.getValue(),
String.valueOf(e));
err.println(message);
return 1;
}
finally
{
try
{
reader.close();
} catch (Exception e) {}
}
// Open the output writer that we'll use to write the differences.
LDIFWriter writer;
try
{
LDIFExportConfig exportConfig;
if (outputLDIF.isPresent())
{
if (overwriteExisting.isPresent())
{
exportConfig = new LDIFExportConfig(outputLDIF.getValue(),
ExistingFileBehavior.OVERWRITE);
}
else
{
exportConfig = new LDIFExportConfig(outputLDIF.getValue(),
ExistingFileBehavior.APPEND);
}
}
else
{
exportConfig = new LDIFExportConfig(out);
}
writer = new LDIFWriter(exportConfig);
}
catch (Exception e)
{
Message message = ERR_LDIFDIFF_CANNOT_OPEN_OUTPUT.get(String.valueOf(e));
err.println(message);
return 1;
}
try
{
// Check to see if either or both of the source and target maps are empty.
if (sourceMap.isEmpty())
{
if (targetMap.isEmpty())
{
// They're both empty, so there are no differences.
Message message = INFO_LDIFDIFF_NO_DIFFERENCES.get();
writer.writeComment(message, 0);
return 0;
}
else
{
// The target isn't empty, so they're all adds.
Iterator<DN> targetIterator = targetMap.keySet().iterator();
while (targetIterator.hasNext())
{
writeAdd(writer, targetMap.get(targetIterator.next()));
}
return 0;
}
}
else if (targetMap.isEmpty())
{
// The source isn't empty, so they're all deletes.
Iterator<DN> sourceIterator = sourceMap.keySet().iterator();
while (sourceIterator.hasNext())
{
writeDelete(writer, sourceMap.get(sourceIterator.next()));
}
return 0;
}
else
{
// Iterate through all the entries in the source and target maps and
// identify the differences.
Iterator<DN> sourceIterator = sourceMap.keySet().iterator();
Iterator<DN> targetIterator = targetMap.keySet().iterator();
DN sourceDN = sourceIterator.next();
DN targetDN = targetIterator.next();
Entry sourceEntry = sourceMap.get(sourceDN);
Entry targetEntry = targetMap.get(targetDN);
boolean differenceFound = false;
while (true)
{
// Compare the DNs to determine the relative order of the
// entries.
int comparatorValue = sourceDN.compareTo(targetDN);
if (comparatorValue < 0)
{
// The source entry should be before the target entry, which means
// that the source entry has been deleted.
writeDelete(writer, sourceEntry);
differenceFound = true;
if (sourceIterator.hasNext())
{
sourceDN = sourceIterator.next();
sourceEntry = sourceMap.get(sourceDN);
}
else
{
// There are no more source entries, so if there are more target
// entries then they're all adds.
writeAdd(writer, targetEntry);
while (targetIterator.hasNext())
{
targetDN = targetIterator.next();
targetEntry = targetMap.get(targetDN);
writeAdd(writer, targetEntry);
differenceFound = true;
}
break;
}
}
else if (comparatorValue > 0)
{
// The target entry should be before the source entry, which means
// that the target entry has been added.
writeAdd(writer, targetEntry);
differenceFound = true;
if (targetIterator.hasNext())
{
targetDN = targetIterator.next();
targetEntry = targetMap.get(targetDN);
}
else
{
// There are no more target entries so all of the remaining source
// entries are deletes.
writeDelete(writer, sourceEntry);
differenceFound = true;
while (sourceIterator.hasNext())
{
sourceDN = sourceIterator.next();
sourceEntry = sourceMap.get(sourceDN);
writeDelete(writer, sourceEntry);
}
break;
}
}
else
{
// The DNs are the same, so check to see if the entries are the
// same or have been modified.
if (writeModify(writer, sourceEntry, targetEntry, ignoreAttrs,
singleValueChanges.isPresent()))
{
differenceFound = true;
}
if (sourceIterator.hasNext())
{
sourceDN = sourceIterator.next();
sourceEntry = sourceMap.get(sourceDN);
}
else
{
// There are no more source entries, so if there are more target
// entries then they're all adds.
while (targetIterator.hasNext())
{
targetDN = targetIterator.next();
targetEntry = targetMap.get(targetDN);
writeAdd(writer, targetEntry);
differenceFound = true;
}
break;
}
if (targetIterator.hasNext())
{
targetDN = targetIterator.next();
targetEntry = targetMap.get(targetDN);
}
else
{
// There are no more target entries so all of the remaining source
// entries are deletes.
writeDelete(writer, sourceEntry);
differenceFound = true;
while (sourceIterator.hasNext())
{
sourceDN = sourceIterator.next();
sourceEntry = sourceMap.get(sourceDN);
writeDelete(writer, sourceEntry);
}
break;
}
}
}
if (! differenceFound)
{
Message message = INFO_LDIFDIFF_NO_DIFFERENCES.get();
writer.writeComment(message, 0);
}
}
}
catch (IOException e)
{
Message message =
ERR_LDIFDIFF_ERROR_WRITING_OUTPUT.get(String.valueOf(e));
err.println(message);
return 1;
}
finally
{
try
{
writer.close();
} catch (Exception e) {}
}
// If we've gotten to this point, then everything was successful.
return 0;
}
/**
* Writes an add change record to the LDIF writer.
*
* @param writer The writer to which the add record should be written.
* @param entry The entry that has been added.
*
* @throws IOException If a problem occurs while attempting to write the add
* record.
*/
private static void writeAdd(LDIFWriter writer, Entry entry)
throws IOException
{
writer.writeAddChangeRecord(entry);
writer.flush();
}
/**
* Writes a delete change record to the LDIF writer, including a comment
* with the contents of the deleted entry.
*
* @param writer The writer to which the delete record should be written.
* @param entry The entry that has been deleted.
*
* @throws IOException If a problem occurs while attempting to write the
* delete record.
*/
private static void writeDelete(LDIFWriter writer, Entry entry)
throws IOException
{
writer.writeDeleteChangeRecord(entry, true);
writer.flush();
}
/**
* Writes a modify change record to the LDIF writer. Note that this will
* handle all the necessary logic for determining if the entries are actually
* different, and if they are the same then no output will be generated. Also
* note that this will only look at differences between the objectclasses and
* user attributes. It will ignore differences in the DN and operational
* attributes.
*
* @param writer The writer to which the modify record should be
* written.
* @param sourceEntry The source form of the entry.
* @param targetEntry The target form of the entry.
* @param ignoreAttrs Attributes that are ignored while calculating
* the differences.
* @param singleValueChanges Indicates whether each attribute-level change
* should be written in a separate modification
* per attribute value.
*
* @return <CODE>true</CODE> if there were any differences found between the
* source and target entries, or <CODE>false</CODE> if not.
*
* @throws IOException If a problem occurs while attempting to write the
* change record.
*/
private static boolean writeModify(LDIFWriter writer, Entry sourceEntry,
Entry targetEntry, Collection ignoreAttrs,
boolean singleValueChanges)
throws IOException
{
// Create a list to hold the modifications that are found.
LinkedList<Modification> modifications = new LinkedList<Modification>();
// Look at the set of objectclasses for the entries.
LinkedHashSet<ObjectClass> sourceClasses =
new LinkedHashSet<ObjectClass>(
sourceEntry.getObjectClasses().keySet());
LinkedHashSet<ObjectClass> targetClasses =
new LinkedHashSet<ObjectClass>(
targetEntry.getObjectClasses().keySet());
Iterator<ObjectClass> sourceClassIterator = sourceClasses.iterator();
while (sourceClassIterator.hasNext())
{
ObjectClass sourceClass = sourceClassIterator.next();
if (targetClasses.remove(sourceClass))
{
sourceClassIterator.remove();
}
}
if (!sourceClasses.isEmpty())
{
// Whatever is left must have been deleted.
AttributeType attrType = DirectoryServer.getObjectClassAttributeType();
AttributeBuilder builder = new AttributeBuilder(attrType);
for (ObjectClass oc : sourceClasses)
{
builder.add(AttributeValues.create(attrType, oc.getNameOrOID()));
}
modifications.add(new Modification(ModificationType.DELETE, builder
.toAttribute()));
}
if (! targetClasses.isEmpty())
{
// Whatever is left must have been added.
AttributeType attrType = DirectoryServer.getObjectClassAttributeType();
AttributeBuilder builder = new AttributeBuilder(attrType);
for (ObjectClass oc : targetClasses)
{
builder.add(AttributeValues.create(attrType, oc.getNameOrOID()));
}
modifications.add(new Modification(ModificationType.ADD, builder
.toAttribute()));
}
// Look at the user attributes for the entries.
LinkedHashSet<AttributeType> sourceTypes =
new LinkedHashSet<AttributeType>(
sourceEntry.getUserAttributes().keySet());
Iterator<AttributeType> sourceTypeIterator = sourceTypes.iterator();
while (sourceTypeIterator.hasNext())
{
AttributeType type = sourceTypeIterator.next();
List<Attribute> sourceAttrs = sourceEntry.getUserAttribute(type);
List<Attribute> targetAttrs = targetEntry.getUserAttribute(type);
sourceEntry.removeAttribute(type);
if (targetAttrs == null)
{
// The target entry doesn't have this attribute type, so it must have
// been deleted. In order to make the delete reversible, delete each
// value individually.
for (Attribute a : sourceAttrs)
{
modifications.add(new Modification(ModificationType.DELETE, a));
}
}
else
{
// Check the attributes for differences. We'll ignore differences in
// the order of the values since that isn't significant.
targetEntry.removeAttribute(type);
for (Attribute sourceAttr : sourceAttrs)
{
Attribute targetAttr = null;
Iterator<Attribute> attrIterator = targetAttrs.iterator();
while (attrIterator.hasNext())
{
Attribute a = attrIterator.next();
if (a.optionsEqual(sourceAttr.getOptions()))
{
targetAttr = a;
attrIterator.remove();
break;
}
}
if (targetAttr == null)
{
// The attribute doesn't exist in the target list, so it has been
// deleted.
modifications.add(new Modification(ModificationType.DELETE,
sourceAttr));
}
else
{
// Compare the values.
AttributeBuilder addedValuesBuilder = new AttributeBuilder(
targetAttr);
addedValuesBuilder.removeAll(sourceAttr);
Attribute addedValues = addedValuesBuilder.toAttribute();
if (!addedValues.isEmpty())
{
modifications.add(new Modification(ModificationType.ADD,
addedValues));
}
AttributeBuilder deletedValuesBuilder = new AttributeBuilder(
sourceAttr);
deletedValuesBuilder.removeAll(targetAttr);
Attribute deletedValues = deletedValuesBuilder.toAttribute();
if (!deletedValues.isEmpty())
{
modifications.add(new Modification(ModificationType.DELETE,
deletedValues));
}
}
}
// Any remaining target attributes have been added.
for (Attribute targetAttr: targetAttrs)
{
modifications.add(new Modification(ModificationType.ADD, targetAttr));
}
}
}
// Any remaining target attribute types have been added.
for (AttributeType type : targetEntry.getUserAttributes().keySet())
{
List<Attribute> targetAttrs = targetEntry.getUserAttribute(type);
for (Attribute a : targetAttrs)
{
modifications.add(new Modification(ModificationType.ADD, a));
}
}
// Remove ignored attributes
if (! ignoreAttrs.isEmpty())
{
ListIterator<Modification> modIter = modifications.listIterator();
while (modIter.hasNext())
{
String name = modIter.next().getAttribute().getName().toLowerCase();
if (ignoreAttrs.contains(name))
{
modIter.remove();
}
}
}
// Write the modification change record.
if (modifications.isEmpty())
{
return false;
}
else
{
if (singleValueChanges)
{
for (Modification m : modifications)
{
Attribute a = m.getAttribute();
if (a.isEmpty())
{
LinkedList<Modification> attrMods = new LinkedList<Modification>();
attrMods.add(m);
writer.writeModifyChangeRecord(sourceEntry.getDN(), attrMods);
}
else
{
LinkedList<Modification> attrMods = new LinkedList<Modification>();
for (AttributeValue v : a)
{
AttributeBuilder builder = new AttributeBuilder(a, true);
builder.add(v);
Attribute attr = builder.toAttribute();
attrMods.clear();
attrMods.add(new Modification(m.getModificationType(), attr));
writer.writeModifyChangeRecord(sourceEntry.getDN(), attrMods);
}
}
}
}
else
{
writer.writeModifyChangeRecord(sourceEntry.getDN(), modifications);
}
return true;
}
}
}