mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Matthew Swift
09.04.2011 055edab9d632ccb01ab6df488434edf99088c5bb
Fix OPENDJ-381: Implement LDIF diff and patch API support in the SDK

Added LDIF.diff() and LDIF.patch() as well as listener API for handling failed patches.
2 files added
3 files modified
735 ■■■■■ changed files
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AVA.java 15 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIF.java 418 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/RejectedChangeListener.java 233 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/RejectedRecordListener.java 59 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties 10 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AVA.java
@@ -825,6 +825,21 @@
  /**
   * Returns a single valued attribute having the same attribute type and value
   * as this AVA.
   *
   * @return A single valued attribute having the same attribute type and value
   *         as this AVA.
   */
  public Attribute toAttribute()
  {
    AttributeDescription ad = AttributeDescription.create(attributeType);
    return new LinkedAttribute(ad, attributeValue);
  }
  /**
   * {@inheritDoc}
   */
  @Override
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIF.java
New file
@@ -0,0 +1,418 @@
/*
 * 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/opendj3/legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * 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/opendj3/legal-notices/CDDLv1_0.txt.  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 2011 ForgeRock AS
 */
package org.forgerock.opendj.ldif;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import org.forgerock.opendj.ldap.*;
import org.forgerock.opendj.ldap.requests.*;
/**
 * This class contains common utility methods for creating and manipulating
 * readers and writers.
 */
public final class LDIF
{
  /**
   * Compares the content of {@code source} to the content of {@code target} and
   * writes the differences to {@code output}. This method does not close the
   * provided readers and writer.
   * <p>
   * <b>NOTE:</b> this method reads the content of {@code source} and
   * {@code target} into memory before calculating the differences, and is
   * therefore not suited for use in cases where a very large number of entries
   * are to be compared.
   *
   * @param source
   *          The entry reader containing the source entries to be compared.
   * @param target
   *          The entry reader containing the target entries to be compared.
   * @param output
   *          The change record writer to which the differences are to be
   *          written.
   * @throws IOException
   *           If an unexpected IO error occurred.
   */
  public static void diff(final EntryReader source, final EntryReader target,
      final ChangeRecordWriter output) throws IOException
  {
    final SortedMap<DN, Entry> sourceEntries = readEntries(source);
    final SortedMap<DN, Entry> targetEntries = readEntries(target);
    final Iterator<Entry> sourceIterator = sourceEntries.values().iterator();
    final Iterator<Entry> targetIterator = targetEntries.values().iterator();
    Entry sourceEntry = nextEntry(sourceIterator);
    Entry targetEntry = nextEntry(targetIterator);
    while (sourceEntry != null && targetEntry != null)
    {
      final DN sourceDN = sourceEntry.getName();
      final DN targetDN = targetEntry.getName();
      final int cmp = sourceDN.compareTo(targetDN);
      if (cmp == 0)
      {
        // Modify record: entry in both source and target.
        output.writeChangeRecord(Requests.newModifyRequest(sourceEntry,
            targetEntry));
        sourceEntry = nextEntry(sourceIterator);
        targetEntry = nextEntry(targetIterator);
      }
      else if (cmp < 0)
      {
        // Delete record: entry in source but not in target.
        output.writeChangeRecord(Requests.newDeleteRequest(sourceEntry
            .getName()));
        sourceEntry = nextEntry(sourceIterator);
      }
      else
      {
        // Add record: entry in target but not in source.
        output.writeChangeRecord(Requests.newAddRequest(targetEntry));
        targetEntry = nextEntry(targetIterator);
      }
    }
    // Delete remaining source records.
    while (sourceEntry != null)
    {
      output
          .writeChangeRecord(Requests.newDeleteRequest(sourceEntry.getName()));
      sourceEntry = nextEntry(sourceIterator);
    }
    // Add remaining target records.
    while (targetEntry != null)
    {
      output.writeChangeRecord(Requests.newAddRequest(targetEntry));
      targetEntry = nextEntry(targetIterator);
    }
  }
  /**
   * Applies the set of changes contained in {@code patch} to the content of
   * {@code input} and writes the result to {@code output}, while ignoring
   * missing entries, and overwriting existing entries. This method does not
   * close the provided readers and writer.
   * <p>
   * <b>NOTE:</b> this method reads the content of {@code input} into memory
   * before applying the changes, and is therefore not suited for use in cases
   * where a very large number of entries are to be patched.
   *
   * @param input
   *          The entry reader containing the set of entries to be patched.
   * @param patch
   *          The change record reader containing the set of changes to be
   *          applied.
   * @param output
   *          The entry writer to which the updated entries are to be written.
   * @throws IOException
   *           If an unexpected IO error occurred.
   */
  public static void patch(final EntryReader input,
      final ChangeRecordReader patch, final EntryWriter output)
      throws IOException
  {
    patch(input, patch, output, RejectedChangeListener.OVERWRITE);
  }
  /**
   * Applies the set of changes contained in {@code patch} to the content of
   * {@code input} and writes the result to {@code output}. This method does not
   * close the provided readers and writer.
   * <p>
   * <b>NOTE:</b> this method reads the content of {@code input} into memory
   * before applying the changes, and is therefore not suited for use in cases
   * where a very large number of entries are to be patched.
   *
   * @param input
   *          The entry reader containing the set of entries to be patched.
   * @param patch
   *          The change record reader containing the set of changes to be
   *          applied.
   * @param output
   *          The entry writer to which the updated entries are to be written.
   * @param listener
   *          The rejected change listener.
   * @throws IOException
   *           If an unexpected IO error occurred.
   */
  public static void patch(final EntryReader input,
      final ChangeRecordReader patch, final EntryWriter output,
      final RejectedChangeListener listener) throws IOException
  {
    final SortedMap<DN, Entry> entries = readEntries(input);
    while (patch.hasNext())
    {
      final ChangeRecord change = patch.readChangeRecord();
      final DecodeException de = change.accept(
          new ChangeRecordVisitor<DecodeException, Void>()
          {
            @Override
            public DecodeException visitChangeRecord(final Void p,
                final AddRequest change)
            {
              final Entry existingEntry = entries.get(change.getName());
              if (existingEntry != null)
              {
                try
                {
                  final Entry entry = listener.handleDuplicateEntry(change,
                      existingEntry);
                  entries.put(entry.getName(), entry);
                }
                catch (final DecodeException e)
                {
                  return e;
                }
              }
              else
              {
                entries.put(change.getName(), change);
              }
              return null;
            }
            @Override
            public DecodeException visitChangeRecord(final Void p,
                final DeleteRequest change)
            {
              if (!entries.containsKey(change.getName()))
              {
                try
                {
                  listener.handleMissingEntry(change);
                }
                catch (final DecodeException e)
                {
                  return e;
                }
              }
              else
              {
                entries.remove(change.getName());
              }
              return null;
            }
            @Override
            public DecodeException visitChangeRecord(final Void p,
                final ModifyDNRequest change)
            {
              if (!entries.containsKey(change.getName()))
              {
                try
                {
                  listener.handleMissingEntry(change);
                }
                catch (final DecodeException e)
                {
                  return e;
                }
              }
              else
              {
                // Calculate the old and new DN.
                final DN oldDN = change.getName();
                DN newSuperior = change.getNewSuperior();
                if (newSuperior == null)
                {
                  newSuperior = change.getName().parent();
                  if (newSuperior == null)
                  {
                    newSuperior = DN.rootDN();
                  }
                }
                final DN newDN = newSuperior.child(change.getNewRDN());
                // Move the renamed entries into a separate map in order to
                // avoid cases where the renamed subtree overlaps.
                final SortedMap<DN, Entry> renamedEntries = new TreeMap<DN, Entry>();
                final Iterator<Map.Entry<DN, Entry>> i = entries
                    .tailMap(change.getName()).entrySet().iterator();
                while (i.hasNext())
                {
                  final Map.Entry<DN, Entry> e = i.next();
                  i.remove();
                  final DN renamedDN = e.getKey().rename(oldDN, newDN);
                  final Entry entry = e.getValue().setName(renamedDN);
                  renamedEntries.put(renamedDN, entry);
                }
                // Modify the target entry.
                final Entry entry = entries.values().iterator().next();
                if (change.isDeleteOldRDN())
                {
                  for (final AVA ava : oldDN.rdn())
                  {
                    entry.removeAttribute(ava.toAttribute(), null);
                  }
                }
                for (final AVA ava : newDN.rdn())
                {
                  entry.addAttribute(ava.toAttribute());
                }
                // Add the renamed entries.
                for (final Entry renamedEntry : renamedEntries.values())
                {
                  final Entry existingEntry = entries.get(renamedEntry
                      .getName());
                  if (existingEntry != null)
                  {
                    try
                    {
                      final Entry tmp = listener.handleDuplicateEntry(change,
                          existingEntry, renamedEntry);
                      entries.put(tmp.getName(), tmp);
                    }
                    catch (final DecodeException e)
                    {
                      return e;
                    }
                  }
                  else
                  {
                    entries.put(renamedEntry.getName(), renamedEntry);
                  }
                }
              }
              return null;
            }
            @Override
            public DecodeException visitChangeRecord(final Void p,
                final ModifyRequest change)
            {
              if (!entries.containsKey(change.getName()))
              {
                try
                {
                  listener.handleMissingEntry(change);
                }
                catch (final DecodeException e)
                {
                  return e;
                }
              }
              else
              {
                final Entry entry = entries.get(change.getName());
                for (final Modification modification : change
                    .getModifications())
                {
                  final ModificationType modType = modification
                      .getModificationType();
                  if (modType.equals(ModificationType.ADD))
                  {
                    entry.addAttribute(modification.getAttribute(), null);
                  }
                  else if (modType.equals(ModificationType.DELETE))
                  {
                    entry.removeAttribute(modification.getAttribute(), null);
                  }
                  else if (modType.equals(ModificationType.REPLACE))
                  {
                    entry.replaceAttribute(modification.getAttribute());
                  }
                  else
                  {
                    System.err.println("Unable to apply \"" + modType
                        + "\" modification to entry \"" + change.getName()
                        + "\": modification type not supported");
                  }
                }
              }
              return null;
            }
          }, null);
      if (de != null)
      {
        throw de;
      }
    }
    for (final Entry entry : entries.values())
    {
      output.writeEntry(entry);
    }
  }
  private static Entry nextEntry(final Iterator<Entry> i)
  {
    return i.hasNext() ? i.next() : null;
  }
  private static SortedMap<DN, Entry> readEntries(final EntryReader reader)
      throws IOException
  {
    final SortedMap<DN, Entry> entries = new TreeMap<DN, Entry>();
    while (reader.hasNext())
    {
      final Entry entry = reader.readEntry();
      entries.put(entry.getName(), entry);
    }
    return entries;
  }
  // Prevent instantiation.
  private LDIF()
  {
    // Do nothing.
  }
}
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/RejectedChangeListener.java
New file
@@ -0,0 +1,233 @@
/*
 * 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/opendj3/legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * 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/opendj3/legal-notices/CDDLv1_0.txt.  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 2011 ForgeRock AS
 */
package org.forgerock.opendj.ldif;
import static org.forgerock.opendj.ldap.CoreMessages.*;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.DeleteRequest;
import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
/**
 * A listener interface which is notified whenever a change record cannot be
 * applied to an entry. This may occur when an attempt is made to update a
 * non-existent entry, or add an entry which already exists.
 * <p>
 * By default the {@link #FAIL_FAST} listener is used.
 */
public interface RejectedChangeListener
{
  /**
   * A handler which terminates processing by throwing a {@code DecodeException}
   * as soon as a change is rejected.
   */
  public final static RejectedChangeListener FAIL_FAST = new RejectedChangeListener()
  {
    @Override
    public Entry handleDuplicateEntry(final AddRequest change,
        final Entry existingEntry) throws DecodeException
    {
      throw DecodeException.error(REJECTED_CHANGE_FAIL_ADD_DUPE.get(change
          .getName().toString()));
    }
    @Override
    public Entry handleDuplicateEntry(final ModifyDNRequest change,
        final Entry existingEntry, final Entry renamedEntry)
        throws DecodeException
    {
      throw DecodeException.error(REJECTED_CHANGE_FAIL_MODIFYDN_DUPE
          .get(renamedEntry.getName().toString()));
    }
    @Override
    public void handleMissingEntry(final DeleteRequest change)
        throws DecodeException
    {
      throw DecodeException.error(REJECTED_CHANGE_FAIL_DELETE.get(change
          .getName().toString()));
    }
    @Override
    public void handleMissingEntry(final ModifyDNRequest change)
        throws DecodeException
    {
      throw DecodeException.error(REJECTED_CHANGE_FAIL_MODIFYDN.get(change
          .getName().toString()));
    }
    @Override
    public void handleMissingEntry(final ModifyRequest change)
        throws DecodeException
    {
      throw DecodeException.error(REJECTED_CHANGE_FAIL_MODIFY.get(change
          .getName().toString()));
    }
  };
  /**
   * The default handler which ignores changes applied to missing entries and
   * tolerates duplicate entries by overwriting the existing entry with the new
   * entry.
   */
  public final static RejectedChangeListener OVERWRITE = new RejectedChangeListener()
  {
    @Override
    public Entry handleDuplicateEntry(final AddRequest change,
        final Entry existingEntry) throws DecodeException
    {
      // Overwrite existing entries.
      return change;
    }
    @Override
    public Entry handleDuplicateEntry(final ModifyDNRequest change,
        final Entry existingEntry, final Entry renamedEntry)
        throws DecodeException
    {
      // Overwrite existing entries.
      return renamedEntry;
    }
    @Override
    public void handleMissingEntry(final DeleteRequest change)
        throws DecodeException
    {
      // Ignore changes applied to missing entries.
    }
    @Override
    public void handleMissingEntry(final ModifyDNRequest change)
        throws DecodeException
    {
      // Ignore changes applied to missing entries.
    }
    @Override
    public void handleMissingEntry(final ModifyRequest change)
        throws DecodeException
    {
      // Ignore changes applied to missing entries.
    }
  };
  /**
   * Invoked when an attempt was made to add an entry which already exists.
   *
   * @param change
   *          The conflicting add request.
   * @param existingEntry
   *          The pre-existing entry.
   * @return The entry which should be kept.
   * @throws DecodeException
   *           If processing should terminate.
   */
  Entry handleDuplicateEntry(AddRequest change, Entry existingEntry)
      throws DecodeException;
  /**
   * Invoked when an attempt was made to rename an entry which already exists.
   *
   * @param change
   *          The conflicting add request.
   * @param existingEntry
   *          The pre-existing entry.
   * @param renamedEntry
   *          The renamed entry.
   * @return The entry which should be kept.
   * @throws DecodeException
   *           If processing should terminate.
   */
  Entry handleDuplicateEntry(ModifyDNRequest change, Entry existingEntry,
      Entry renamedEntry) throws DecodeException;
  /**
   * Invoked when an attempt was made to delete an entry which does not exist.
   *
   * @param change
   *          The conflicting delete request.
   * @throws DecodeException
   *           If processing should terminate.
   */
  void handleMissingEntry(DeleteRequest change) throws DecodeException;
  /**
   * Invoked when an attempt was made to rename an entry which does not exist.
   *
   * @param change
   *          The conflicting rename request.
   * @throws DecodeException
   *           If processing should terminate.
   */
  void handleMissingEntry(ModifyDNRequest change) throws DecodeException;
  /**
   * Invoked when an attempt was made to modify an entry which does not exist.
   *
   * @param change
   *          The conflicting modify request.
   * @throws DecodeException
   *           If processing should terminate.
   */
  void handleMissingEntry(ModifyRequest change) throws DecodeException;
}
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/RejectedRecordListener.java
@@ -38,6 +38,8 @@
/**
 * A listener interface which is notified whenever records are skipped,
 * malformed, or fail schema validation.
 * <p>
 * By default the {@link #FAIL_FAST} listener is used.
 */
public interface RejectedRecordListener
{
@@ -72,21 +74,67 @@
    @Override
    public void handleSchemaValidationWarning(final long lineNumber,
        final List<String> ldifRecord, final List<LocalizableMessage> reasons)
        throws DecodeException
    {
      // Ignore schema validation warnings.
    }
    @Override
    public void handleSkippedRecord(final long lineNumber,
        final List<String> ldifRecord, final LocalizableMessage reason)
        throws DecodeException
    {
      // Ignore skipped records.
    }
  };
  /**
   * A handler which ignores all rejected record notifications.
   */
  public static final RejectedRecordListener IGNORE_ALL = new RejectedRecordListener()
  {
    @Override
    public void handleMalformedRecord(final long lineNumber,
        final List<String> ldifRecord, final LocalizableMessage reason)
        throws DecodeException
    {
      // Ignore malformed records.
    }
    public void handleSchemaValidationWarning(long lineNumber,
        List<String> ldifRecord, List<LocalizableMessage> reasons)
    @Override
    public void handleSchemaValidationFailure(final long lineNumber,
        final List<String> ldifRecord, final List<LocalizableMessage> reasons)
        throws DecodeException
    {
      // Ignore schema validation failures.
    }
    @Override
    public void handleSchemaValidationWarning(final long lineNumber,
        final List<String> ldifRecord, final List<LocalizableMessage> reasons)
        throws DecodeException
    {
      // Ignore schema validation warnings.
    }
    @Override
    public void handleSkippedRecord(final long lineNumber,
        final List<String> ldifRecord, final LocalizableMessage reason)
        throws DecodeException
    {
      // Ignore skipped records.
    }
  };
@@ -131,11 +179,12 @@
  /**
   * Invoked when a record was not rejected but contained one or more schema validation warnings.
   * Invoked when a record was not rejected but contained one or more schema
   * validation warnings.
   *
   * @param lineNumber
   *          The line number within the source location in which the
   *          record is located, if known, otherwise {@code -1}.
   *          The line number within the source location in which the record is
   *          located, if known, otherwise {@code -1}.
   * @param ldifRecord
   *          An LDIF representation of the record which contained schema
   *          validation warnings.
opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties
@@ -1390,3 +1390,13 @@
 removed from the schema because it is invalid
ERR_CONNECTION_POOL_CLOSING=No connection could be obtained from connection \
 pool "%s" because it is closing
REJECTED_CHANGE_FAIL_ADD_DUPE=The entry "%s" could not be added because there \
 is already an entry with the same name
REJECTED_CHANGE_FAIL_DELETE=The entry "%s" could not be deleted because the \
 entry does not exist
REJECTED_CHANGE_FAIL_MODIFY=The entry "%s" could not be modified because the \
 entry does not exist
REJECTED_CHANGE_FAIL_MODIFYDN=The entry "%s" could not be renamed because the \
 entry does not exist
REJECTED_CHANGE_FAIL_MODIFYDN_DUPE=The entry "%s" could not be renamed because \
 there is already an entry with the same name