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

Matthew Swift
28.50.2013 71178c4013389172080487b47ad407ae211c304b
Fix OPENDJ-354: Implement a RequestHandler which provides an in-memory backend

* added class MemoryBackend
* added Entries.modifyEntry() and included support for increment
* added Connection.deleteSubtree()
* added Request.containsControl() and Response.containsControl()
* aligned attributes APIs in Post/Pre read controls with SearchRequest (i.e. use List)
* refactored server example to use new memory backend
2 files added
18 files modified
1921 ■■■■ changed files
opendj-sdk/opendj3/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java 364 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/ConnectionDecorator.java 10 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java 12 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Connection.java 31 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entries.java 209 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/ErrorResultException.java 1 ●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/FixedConnectionPool.java 5 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java 461 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Modification.java 5 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/controls/PostReadRequestControl.java 55 ●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/controls/PreReadRequestControl.java 55 ●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AbstractRequestImpl.java 52 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableRequest.java 55 ●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/Request.java 13 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/AbstractResponseImpl.java 52 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableResponseImpl.java 52 ●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/Response.java 13 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties 15 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java 6 ●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java 455 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj3/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java
@@ -22,61 +22,28 @@
 *
 *
 *      Copyright 2009-2010 Sun Microsystems, Inc.
 *      Portions copyright 2011-2012 ForgeRock AS
 *      Portions copyright 2011-2013 ForgeRock AS
 */
package org.forgerock.opendj.examples;
import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.NavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.net.ssl.SSLContext;
import org.forgerock.opendj.ldap.CancelledResultException;
import org.forgerock.opendj.ldap.Connections;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.ErrorResultException;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.IntermediateResponseHandler;
import org.forgerock.opendj.ldap.KeyManagers;
import org.forgerock.opendj.ldap.LDAPClientContext;
import org.forgerock.opendj.ldap.LDAPListener;
import org.forgerock.opendj.ldap.LDAPListenerOptions;
import org.forgerock.opendj.ldap.LinkedHashMapEntry;
import org.forgerock.opendj.ldap.Matcher;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.opendj.ldap.ModificationType;
import org.forgerock.opendj.ldap.RequestContext;
import org.forgerock.opendj.ldap.RequestHandler;
import org.forgerock.opendj.ldap.MemoryBackend;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.ResultHandler;
import org.forgerock.opendj.ldap.SSLContextBuilder;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.ServerConnection;
import org.forgerock.opendj.ldap.ServerConnectionFactory;
import org.forgerock.opendj.ldap.TrustManagers;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.CompareRequest;
import org.forgerock.opendj.ldap.requests.DeleteRequest;
import org.forgerock.opendj.ldap.requests.ExtendedRequest;
import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.responses.BindResult;
import org.forgerock.opendj.ldap.responses.CompareResult;
import org.forgerock.opendj.ldap.responses.ExtendedResult;
import org.forgerock.opendj.ldap.responses.Responses;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldif.LDIFEntryReader;
/**
@@ -95,260 +62,6 @@
 * </pre>
 */
public final class Server {
    private static final class MemoryBackend implements RequestHandler<RequestContext> {
        private final ConcurrentSkipListMap<DN, Entry> entries;
        private final ReentrantReadWriteLock entryLock = new ReentrantReadWriteLock();
        private MemoryBackend(final ConcurrentSkipListMap<DN, Entry> entries) {
            this.entries = entries;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void handleAdd(final RequestContext requestContext, final AddRequest request,
                final IntermediateResponseHandler intermediateResponseHandler,
                final ResultHandler<? super Result> resultHandler) {
            // TODO: controls.
            entryLock.writeLock().lock();
            try {
                DN dn = request.getName();
                if (entries.containsKey(dn)) {
                    resultHandler.handleErrorResult(ErrorResultException.newErrorResult(
                            ResultCode.ENTRY_ALREADY_EXISTS, "The entry " + dn.toString()
                                    + " already exists"));
                }
                DN parent = dn.parent();
                if (!entries.containsKey(parent)) {
                    resultHandler.handleErrorResult(ErrorResultException.newErrorResult(
                            ResultCode.NO_SUCH_OBJECT, "The parent entry " + parent.toString()
                                    + " does not exist"));
                } else {
                    entries.put(dn, request);
                    resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
                }
            } finally {
                entryLock.writeLock().unlock();
            }
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void handleBind(final RequestContext requestContext, final int version,
                final BindRequest request,
                final IntermediateResponseHandler intermediateResponseHandler,
                final ResultHandler<? super BindResult> resultHandler) {
            if (request.getAuthenticationType() != ((byte) 0x80)) {
                // TODO: SASL authentication not implemented.
                resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR,
                        "non-SIMPLE authentication not supported: "
                                + request.getAuthenticationType()));
            } else {
                // TODO: always succeed.
                resultHandler.handleResult(Responses.newBindResult(ResultCode.SUCCESS));
            }
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void handleCompare(final RequestContext requestContext,
                final CompareRequest request,
                final IntermediateResponseHandler intermediateResponseHandler,
                final ResultHandler<? super CompareResult> resultHandler) {
            // TODO:
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void handleDelete(final RequestContext requestContext, final DeleteRequest request,
                final IntermediateResponseHandler intermediateResponseHandler,
                final ResultHandler<? super Result> resultHandler) {
            // TODO: controls.
            entryLock.writeLock().lock();
            try {
                // TODO: check for children.
                DN dn = request.getName();
                if (!entries.containsKey(dn)) {
                    resultHandler.handleErrorResult(ErrorResultException.newErrorResult(
                            ResultCode.NO_SUCH_OBJECT, "The entry " + dn.toString()
                                    + " does not exist"));
                } else {
                    entries.remove(dn);
                    resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
                }
            } finally {
                entryLock.writeLock().unlock();
            }
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public <R extends ExtendedResult> void handleExtendedRequest(
                final RequestContext requestContext, final ExtendedRequest<R> request,
                final IntermediateResponseHandler intermediateResponseHandler,
                final ResultHandler<? super R> resultHandler) {
            // TODO: not implemented.
            resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR,
                    "Extended request operation not supported"));
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void handleModify(final RequestContext requestContext, final ModifyRequest request,
                final IntermediateResponseHandler intermediateResponseHandler,
                final ResultHandler<? super Result> resultHandler) {
            // TODO: controls.
            // TODO: read lock is not really enough since concurrent updates may
            // still occur to the same entry.
            entryLock.readLock().lock();
            try {
                DN dn = request.getName();
                Entry entry = entries.get(dn);
                if (entry == null) {
                    resultHandler.handleErrorResult(ErrorResultException.newErrorResult(
                            ResultCode.NO_SUCH_OBJECT, "The entry " + dn.toString()
                                    + " does not exist"));
                }
                Entry newEntry = new LinkedHashMapEntry(entry);
                for (Modification mod : request.getModifications()) {
                    ModificationType modType = mod.getModificationType();
                    if (modType.equals(ModificationType.ADD)) {
                        // TODO: Reject empty attribute and duplicate values.
                        newEntry.addAttribute(mod.getAttribute(), null);
                    } else if (modType.equals(ModificationType.DELETE)) {
                        // TODO: Reject missing values.
                        newEntry.removeAttribute(mod.getAttribute(), null);
                    } else if (modType.equals(ModificationType.REPLACE)) {
                        newEntry.replaceAttribute(mod.getAttribute());
                    } else {
                        resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR,
                                "Modify request contains an unsupported modification type"));
                        return;
                    }
                }
                entries.put(dn, newEntry);
                resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
            } finally {
                entryLock.readLock().unlock();
            }
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void handleModifyDN(final RequestContext requestContext,
                final ModifyDNRequest request,
                final IntermediateResponseHandler intermediateResponseHandler,
                final ResultHandler<? super Result> resultHandler) {
            // TODO: not implemented.
            resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR,
                    "ModifyDN request operation not supported"));
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void handleSearch(final RequestContext requestContext, final SearchRequest request,
                final IntermediateResponseHandler intermediateResponseHandler,
                final SearchResultHandler resultHandler) {
            // TODO: controls, limits, etc.
            entryLock.readLock().lock();
            try {
                DN dn = request.getName();
                Entry baseEntry = entries.get(dn);
                if (baseEntry == null) {
                    resultHandler.handleErrorResult(ErrorResultException.newErrorResult(
                            ResultCode.NO_SUCH_OBJECT, "The entry " + dn.toString()
                                    + " does not exist"));
                    return;
                }
                SearchScope scope = request.getScope();
                Filter filter = request.getFilter();
                Matcher matcher = filter.matcher();
                if (scope.equals(SearchScope.BASE_OBJECT)) {
                    if (matcher.matches(baseEntry).toBoolean()) {
                        sendEntry(request, resultHandler, baseEntry);
                    }
                } else if (scope.equals(SearchScope.SINGLE_LEVEL)) {
                    NavigableMap<DN, Entry> subtree = entries.tailMap(dn, false);
                    for (Entry entry : subtree.values()) {
                        // Check for cancellation.
                        requestContext.checkIfCancelled(false);
                        DN childDN = entry.getName();
                        if (childDN.isChildOf(dn)) {
                            if (!matcher.matches(entry).toBoolean()) {
                                continue;
                            }
                            if (!sendEntry(request, resultHandler, entry)) {
                                // Caller has asked to stop sending results.
                                break;
                            }
                        } else if (!childDN.isSubordinateOrEqualTo(dn)) {
                            // The remaining entries will be out of scope.
                            break;
                        }
                    }
                } else if (scope.equals(SearchScope.WHOLE_SUBTREE)) {
                    NavigableMap<DN, Entry> subtree = entries.tailMap(dn);
                    for (Entry entry : subtree.values()) {
                        // Check for cancellation.
                        requestContext.checkIfCancelled(false);
                        DN childDN = entry.getName();
                        if (childDN.isSubordinateOrEqualTo(dn)) {
                            if (!matcher.matches(entry).toBoolean()) {
                                continue;
                            }
                            if (!sendEntry(request, resultHandler, entry)) {
                                // Caller has asked to stop sending results.
                                break;
                            }
                        } else {
                            // The remaining entries will be out of scope.
                            break;
                        }
                    }
                } else {
                    resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR,
                            "Search request contains an unsupported search scope"));
                    return;
                }
                resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
            } catch (CancelledResultException e) {
                resultHandler.handleErrorResult(e);
            } finally {
                entryLock.readLock().unlock();
            }
        }
        private boolean sendEntry(SearchRequest request, SearchResultHandler resultHandler,
                Entry entry) {
            // TODO: check filter, strip attributes.
            return resultHandler.handleEntry(Responses.newSearchResultEntry(entry));
        }
    }
    /**
     * Main method.
@@ -373,8 +86,14 @@
        final String certNickname = (args.length == 6) ? args[5] : null;
        // Create the memory backend.
        final ConcurrentSkipListMap<DN, Entry> entries = readEntriesFromLDIF(ldifFileName);
        final MemoryBackend backend = new MemoryBackend(entries);
        final MemoryBackend backend;
        try {
            backend = new MemoryBackend(new LDIFEntryReader(new FileInputStream(ldifFileName)));
        } catch (final IOException e) {
            System.err.println(e.getMessage());
            System.exit(ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue());
            return; // Keep compiler quiet.
        }
        // Create a server connection adapter.
        final ServerConnectionFactory<LDAPClientContext, Integer> connectionHandler =
@@ -395,11 +114,13 @@
                                                .toCharArray(), null))).setTrustManager(
                                TrustManagers.trustAll()).getSSLContext();
                ServerConnectionFactory<LDAPClientContext, Integer> sslWrapper =
                final ServerConnectionFactory<LDAPClientContext, Integer> sslWrapper =
                        new ServerConnectionFactory<LDAPClientContext, Integer>() {
                            @Override
                            public ServerConnection<Integer> handleAccept(
                                    LDAPClientContext clientContext) throws ErrorResultException {
                                    final LDAPClientContext clientContext)
                                    throws ErrorResultException {
                                clientContext.enableTLS(sslContext, null, null, false, false);
                                return connectionHandler.handleAccept(clientContext);
                            }
@@ -422,63 +143,6 @@
        }
    }
    /**
     * Reads the entries from the named LDIF file.
     *
     * @param ldifFileName
     *            The name of the LDIF file.
     * @return The entries.
     */
    private static ConcurrentSkipListMap<DN, Entry> readEntriesFromLDIF(final String ldifFileName) {
        final ConcurrentSkipListMap<DN, Entry> entries;
        // Read the LDIF.
        InputStream ldif;
        try {
            ldif = new FileInputStream(ldifFileName);
        } catch (final FileNotFoundException e) {
            System.err.println(e.getMessage());
            System.exit(ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue());
            return null; // Satisfy compiler.
        }
        entries = new ConcurrentSkipListMap<DN, Entry>();
        final LDIFEntryReader reader = new LDIFEntryReader(ldif);
        try {
            while (reader.hasNext()) {
                Entry entry = reader.readEntry();
                entries.put(entry.getName(), entry);
            }
        } catch (final IOException e) {
            System.err.println(e.getMessage());
            System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
            return null; // Satisfy compiler.
        } finally {
            try {
                reader.close();
            } catch (final IOException ignored) {
                // Ignore.
            }
        }
        // Quickly sanity check that every entry (except root entries) have a
        // parent.
        boolean isValid = true;
        for (DN dn : entries.keySet()) {
            if (dn.size() > 1) {
                DN parent = dn.parent();
                if (!entries.containsKey(parent)) {
                    System.err
                            .println("The entry \"" + dn.toString() + "\" does not have a parent");
                    isValid = false;
                }
            }
        }
        if (!isValid) {
            System.exit(1);
        }
        return entries;
    }
    private Server() {
        // Not used.
    }
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/ConnectionDecorator.java
@@ -289,6 +289,16 @@
     * The default implementation is to delegate.
     */
    @Override
    public Result deleteSubtree(final String name) throws ErrorResultException {
        return connection.deleteSubtree(name);
    }
    /**
     * {@inheritDoc}
     * <p>
     * The default implementation is to delegate.
     */
    @Override
    public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request)
            throws ErrorResultException {
        return connection.extendedRequest(request);
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java
@@ -22,7 +22,7 @@
 *
 *
 *      Copyright 2009-2010 Sun Microsystems, Inc.
 *      Portions copyright 2011-2012 ForgeRock AS
 *      Portions copyright 2011-2013 ForgeRock AS
 */
package org.forgerock.opendj.ldap;
@@ -36,6 +36,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.DeleteRequest;
import org.forgerock.opendj.ldap.requests.ExtendedRequest;
@@ -363,6 +364,15 @@
     * {@inheritDoc}
     */
    @Override
    public Result deleteSubtree(final String name) throws ErrorResultException {
        return delete(Requests.newDeleteRequest(name).addControl(
                SubtreeDeleteRequestControl.newControl(true)));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request)
            throws ErrorResultException {
        return extendedRequest(request, null);
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Connection.java
@@ -597,6 +597,37 @@
    Result delete(String name) throws ErrorResultException;
    /**
     * Deletes the named entry and all of its subordinates from the Directory
     * Server.
     * <p>
     * This method is equivalent to the following code:
     *
     * <pre>
     * DeleteRequest request = new DeleteRequest(name).addControl(
     * connection.delete(request);
     * </pre>
     *
     * @param name
     *            The distinguished name of the subtree base entry to be
     *            deleted.
     * @return The result of the operation.
     * @throws ErrorResultException
     *             If the result code indicates that the request failed for some
     *             reason.
     * @throws LocalizedIllegalArgumentException
     *             If {@code name} could not be decoded using the default
     *             schema.
     * @throws UnsupportedOperationException
     *             If this connection does not support delete operations.
     * @throws IllegalStateException
     *             If this connection has already been closed, i.e. if
     *             {@code isClosed() == true}.
     * @throws NullPointerException
     *             If {@code name} was {@code null}.
     */
    Result deleteSubtree(String name) throws ErrorResultException;
    /**
     * Asynchronously deletes an entry from the Directory Server using the
     * provided delete request.
     *
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entries.java
@@ -22,13 +22,21 @@
 *
 *
 *      Copyright 2010 Sun Microsystems, Inc.
 *      Portions copyright 2011-2012 ForgeRock AS
 *      Portions copyright 2011-2013 ForgeRock AS
 */
package org.forgerock.opendj.ldap;
import static org.forgerock.opendj.ldap.AttributeDescription.objectClass;
import static org.forgerock.opendj.ldap.CoreMessages.ERR_ENTRY_DUPLICATE_VALUES;
import static org.forgerock.opendj.ldap.CoreMessages.ERR_ENTRY_INCREMENT_CANNOT_PARSE_AS_INT;
import static org.forgerock.opendj.ldap.CoreMessages.ERR_ENTRY_INCREMENT_INVALID_VALUE_COUNT;
import static org.forgerock.opendj.ldap.CoreMessages.ERR_ENTRY_INCREMENT_NO_SUCH_ATTRIBUTE;
import static org.forgerock.opendj.ldap.CoreMessages.ERR_ENTRY_NO_SUCH_VALUE;
import static org.forgerock.opendj.ldap.CoreMessages.ERR_ENTRY_UNKNOWN_MODIFICATION_TYPE;
import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
@@ -37,6 +45,7 @@
import java.util.Set;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.schema.ObjectClass;
@@ -179,14 +188,16 @@
        /**
         * {@inheritDoc}
         */
        public AttributeParser parseAttribute(AttributeDescription attributeDescription) {
        @Override
        public AttributeParser parseAttribute(final AttributeDescription attributeDescription) {
            return entry.parseAttribute(attributeDescription);
        }
        /**
         * {@inheritDoc}
         */
        public AttributeParser parseAttribute(String attributeDescription) {
        @Override
        public AttributeParser parseAttribute(final String attributeDescription) {
            return entry.parseAttribute(attributeDescription);
        }
@@ -251,6 +262,13 @@
    }
    private static final Comparator<Entry> COMPARATOR = new Comparator<Entry>() {
        @Override
        public int compare(final Entry o1, final Entry o2) {
            return o1.getName().compareTo(o2.getName());
        }
    };
    private static final Function<Attribute, Attribute, Void> UNMODIFIABLE_ATTRIBUTE_FUNCTION =
            new Function<Attribute, Attribute, Void>() {
@@ -261,12 +279,6 @@
            };
    private static final Comparator<Entry> COMPARATOR = new Comparator<Entry>() {
        public int compare(Entry o1, Entry o2) {
            return o1.getName().compareTo(o2.getName());
        }
    };
    /**
     * Returns a {@code Comparator} which can be used to compare entries by name
     * using the natural order for DN comparisons (parent before children).
@@ -576,6 +588,126 @@
    }
    /**
     * Applies the provided modification to an entry. This method implements
     * "permissive" modify semantics, ignoring attempts to add duplicate values
     * or attempts to remove values which do not exist.
     *
     * @param entry
     *            The entry to be modified.
     * @param change
     *            The modification to be applied to the entry.
     * @return A reference to the updated entry.
     * @throws ErrorResultException
     *             If an error occurred while performing the change such as an
     *             attempt to increment a value which is not a number. The entry
     *             will not have been modified.
     */
    public static Entry modifyEntry(final Entry entry, final Modification change)
            throws ErrorResultException {
        return modifyEntry(entry, change, null);
    }
    /**
     * Applies the provided modification to an entry. This method implements
     * "permissive" modify semantics, recording attempts to add duplicate values
     * or attempts to remove values which do not exist in the provided
     * collection if provided.
     *
     * @param entry
     *            The entry to be modified.
     * @param change
     *            The modification to be applied to the entry.
     * @param conflictingValues
     *            A collection into which duplicate or missing values will be
     *            added, or {@code null} if conflicting values should not be
     *            saved.
     * @return A reference to the updated entry.
     * @throws ErrorResultException
     *             If an error occurred while performing the change such as an
     *             attempt to increment a value which is not a number. The entry
     *             will not have been modified.
     */
    public static Entry modifyEntry(final Entry entry, final Modification change,
            final Collection<? super ByteString> conflictingValues) throws ErrorResultException {
        final ModificationType modType = change.getModificationType();
        if (modType.equals(ModificationType.ADD)) {
            entry.addAttribute(change.getAttribute(), conflictingValues);
        } else if (modType.equals(ModificationType.DELETE)) {
            entry.removeAttribute(change.getAttribute(), conflictingValues);
        } else if (modType.equals(ModificationType.REPLACE)) {
            entry.replaceAttribute(change.getAttribute());
        } else if (modType.equals(ModificationType.INCREMENT)) {
            incrementAttribute(entry, change.getAttribute());
        } else {
            throw newErrorResult(ResultCode.UNWILLING_TO_PERFORM,
                    ERR_ENTRY_UNKNOWN_MODIFICATION_TYPE.get(String.valueOf(modType)).toString());
        }
        return entry;
    }
    /**
     * Applies the provided modification request to an entry. This method will
     * utilize "permissive" modify semantics if the request contains the
     * {@link PermissiveModifyRequestControl}.
     *
     * @param entry
     *            The entry to be modified.
     * @param changes
     *            The modification request to be applied to the entry.
     * @return A reference to the updated entry.
     * @throws ErrorResultException
     *             If an error occurred while performing the changes such as an
     *             attempt to add duplicate values, remove values which do not
     *             exist, or increment a value which is not a number. The entry
     *             may have been modified.
     */
    public static Entry modifyEntry(final Entry entry, final ModifyRequest changes)
            throws ErrorResultException {
        final boolean isPermissive = changes.containsControl(PermissiveModifyRequestControl.OID);
        return modifyEntry0(entry, changes.getModifications(), isPermissive);
    }
    /**
     * Applies the provided modifications to an entry using "permissive" modify
     * semantics.
     *
     * @param entry
     *            The entry to be modified.
     * @param changes
     *            The modification request to be applied to the entry.
     * @return A reference to the updated entry.
     * @throws ErrorResultException
     *             If an error occurred while performing the changes such as an
     *             attempt to increment a value which is not a number. The entry
     *             may have been modified.
     */
    public static Entry modifyEntryPermissive(final Entry entry,
            final Collection<Modification> changes) throws ErrorResultException {
        return modifyEntry0(entry, changes, true);
    }
    /**
     * Applies the provided modifications to an entry using "strict" modify
     * semantics. Attempts to add duplicate values or attempts to remove values
     * which do not exist will cause the update to fail.
     *
     * @param entry
     *            The entry to be modified.
     * @param changes
     *            The modification request to be applied to the entry.
     * @return A reference to the updated entry.
     * @throws ErrorResultException
     *             If an error occurred while performing the changes such as an
     *             attempt to add duplicate values, remove values which do not
     *             exist, or increment a value which is not a number. The entry
     *             may have been modified.
     */
    public static Entry modifyEntryStrict(final Entry entry, final Collection<Modification> changes)
            throws ErrorResultException {
        return modifyEntry0(entry, changes, false);
    }
    /**
     * Returns a read-only view of {@code entry} and its attributes. Query
     * operations on the returned entry and its attributes "read-through" to the
     * underlying entry or attribute, and attempts to modify the returned entry
@@ -596,6 +728,65 @@
        }
    }
    private static void incrementAttribute(final Entry entry, final Attribute change)
            throws ErrorResultException {
        // First parse the change.
        final AttributeDescription deltaAd = change.getAttributeDescription();
        if (change.size() != 1) {
            throw newErrorResult(ResultCode.CONSTRAINT_VIOLATION,
                    ERR_ENTRY_INCREMENT_INVALID_VALUE_COUNT.get(deltaAd.toString()).toString());
        }
        final long delta;
        try {
            delta = change.parse().asLong();
        } catch (final Exception e) {
            throw newErrorResult(ResultCode.CONSTRAINT_VIOLATION,
                    ERR_ENTRY_INCREMENT_CANNOT_PARSE_AS_INT.get(deltaAd.toString()).toString());
        }
        // Now apply the increment to the attribute.
        final Attribute oldAttribute = entry.getAttribute(deltaAd);
        if (oldAttribute == null) {
            throw newErrorResult(ResultCode.NO_SUCH_ATTRIBUTE,
                    ERR_ENTRY_INCREMENT_NO_SUCH_ATTRIBUTE.get(deltaAd.toString()).toString());
        }
        // Re-use existing attribute description in case it differs in case, etc.
        final Attribute newAttribute = new LinkedAttribute(oldAttribute.getAttributeDescription());
        try {
            for (final Long value : oldAttribute.parse().asSetOfLong()) {
                newAttribute.add(value + delta);
            }
        } catch (final Exception e) {
            throw newErrorResult(ResultCode.CONSTRAINT_VIOLATION,
                    ERR_ENTRY_INCREMENT_CANNOT_PARSE_AS_INT.get(deltaAd.toString()).toString());
        }
        entry.replaceAttribute(newAttribute);
    }
    private static Entry modifyEntry0(final Entry entry, final Collection<Modification> changes,
            final boolean isPermissive) throws ErrorResultException {
        final Collection<ByteString> conflictingValues =
                isPermissive ? null : new ArrayList<ByteString>(0);
        for (final Modification change : changes) {
            modifyEntry(entry, change, conflictingValues);
            if (!isPermissive && !conflictingValues.isEmpty()) {
                if (change.getModificationType().equals(ModificationType.ADD)) {
                    // Duplicate values.
                    throw newErrorResult(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS,
                            ERR_ENTRY_DUPLICATE_VALUES.get(
                                    change.getAttribute().getAttributeDescriptionAsString())
                                    .toString());
                } else {
                    // Missing values.
                    throw newErrorResult(ResultCode.NO_SUCH_ATTRIBUTE, ERR_ENTRY_NO_SUCH_VALUE.get(
                            change.getAttribute().getAttributeDescriptionAsString()).toString());
                }
            }
        }
        return entry;
    }
    // Prevent instantiation.
    private Entries() {
        // Nothing to do.
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/ErrorResultException.java
@@ -162,6 +162,7 @@
                || rc == ResultCode.CLIENT_SIDE_ENCODING_ERROR) {
            return new ConnectionException(result);
        } else if (rc == ResultCode.ATTRIBUTE_OR_VALUE_EXISTS
                || rc == ResultCode.NO_SUCH_ATTRIBUTE
                || rc == ResultCode.CONSTRAINT_VIOLATION || rc == ResultCode.ENTRY_ALREADY_EXISTS
                || rc == ResultCode.INVALID_ATTRIBUTE_SYNTAX || rc == ResultCode.INVALID_DN_SYNTAX
                || rc == ResultCode.NAMING_VIOLATION || rc == ResultCode.NOT_ALLOWED_ON_NONLEAF
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/FixedConnectionPool.java
@@ -309,6 +309,11 @@
        }
        @Override
        public Result deleteSubtree(final String name) throws ErrorResultException {
            return checkState().deleteSubtree(name);
        }
        @Override
        public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request)
                throws ErrorResultException {
            return checkState().extendedRequest(request);
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java
New file
@@ -0,0 +1,461 @@
/*
 * 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 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 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 2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap;
import static org.forgerock.opendj.ldap.Attributes.singletonAttribute;
import static org.forgerock.opendj.ldap.Entries.modifyEntry;
import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
import static org.forgerock.opendj.ldap.responses.Responses.newBindResult;
import static org.forgerock.opendj.ldap.responses.Responses.newCompareResult;
import static org.forgerock.opendj.ldap.responses.Responses.newResult;
import static org.forgerock.opendj.ldap.responses.Responses.newSearchResultEntry;
import java.io.IOException;
import java.util.Collection;
import java.util.NavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
import org.forgerock.opendj.ldap.controls.PostReadRequestControl;
import org.forgerock.opendj.ldap.controls.PostReadResponseControl;
import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.CompareRequest;
import org.forgerock.opendj.ldap.requests.DeleteRequest;
import org.forgerock.opendj.ldap.requests.ExtendedRequest;
import org.forgerock.opendj.ldap.requests.GenericBindRequest;
import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.Request;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.requests.SimpleBindRequest;
import org.forgerock.opendj.ldap.responses.BindResult;
import org.forgerock.opendj.ldap.responses.CompareResult;
import org.forgerock.opendj.ldap.responses.ExtendedResult;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.ldif.EntryReader;
/**
 * A simple in memory back-end which can be used for testing. It is not intended
 * for production use due to various limitations. The back-end implementations
 * supports the following:
 * <ul>
 * <li>add, bind (simple), compare, delete, modify, and search operations, but
 * not modifyDN nor extended operations
 * <li>assertion, pre-, and post- read controls, subtree delete control, and
 * permissive modify control
 * <li>thread safety - supports concurrent operations
 * </ul>
 * It does not support the following:
 * <ul>
 * <li>high performance
 * <li>secure password storage
 * <li>schema checking
 * <li>persistence
 * <li>indexing
 * </ul>
 * This class can be used in conjunction with the factories defined in
 * {@link Connections} to create simpler servers as well as mock LDAP
 * connections. For example, to create a mock LDAP connection factory:
 *
 * <pre>
 * MemoryBackend backend = new MemoryBackend();
 * Connection connection = newInternalConnectionFactory(newServerConnectionFactory(backend), null)
 *         .getConnection();
 * </pre>
 */
public final class MemoryBackend implements RequestHandler<RequestContext> {
    private final DecodeOptions decodeOptions;
    private final ConcurrentSkipListMap<DN, Entry> entries = new ConcurrentSkipListMap<DN, Entry>();
    private final ReentrantReadWriteLock entryLock = new ReentrantReadWriteLock();
    private final Schema schema;
    /**
     * Creates a new empty memory backend which will use the default schema.
     */
    public MemoryBackend() {
        this(Schema.getDefaultSchema());
    }
    /**
     * Creates a new memory backend which will use the default schema, and will
     * contain the entries read from the provided entry reader.
     *
     * @param reader
     *            The entry reader.
     * @throws IOException
     *             If an unexpected IO error occurred while reading the entries.
     */
    public MemoryBackend(final EntryReader reader) throws IOException {
        this(Schema.getDefaultSchema(), reader);
    }
    /**
     * Creates a new empty memory backend which will use the provided schema.
     *
     * @param schema
     *            The schema to use for decoding filters, etc.
     */
    public MemoryBackend(final Schema schema) {
        this.schema = schema;
        this.decodeOptions = new DecodeOptions().setSchema(schema);
    }
    /**
     * Creates a new memory backend which will use the provided schema, and will
     * contain the entries read from the provided entry reader.
     *
     * @param schema
     *            The schema to use for decoding filters, etc.
     * @param reader
     *            The entry reader.
     * @throws IOException
     *             If an unexpected IO error occurred while reading the entries.
     */
    public MemoryBackend(final Schema schema, final EntryReader reader) throws IOException {
        this.schema = schema;
        this.decodeOptions = new DecodeOptions().setSchema(schema);
        if (reader != null) {
            try {
                while (reader.hasNext()) {
                    final Entry entry = reader.readEntry();
                    final DN dn = entry.getName();
                    if (entries.containsKey(dn)) {
                        throw new ErrorResultIOException(newErrorResult(
                                ResultCode.ENTRY_ALREADY_EXISTS, "Attempted to add the entry '"
                                        + dn.toString() + "' multiple times"));
                    } else {
                        entries.put(dn, entry);
                    }
                }
            } finally {
                reader.close();
            }
        }
    }
    @Override
    public void handleAdd(final RequestContext requestContext, final AddRequest request,
            final IntermediateResponseHandler intermediateResponseHandler,
            final ResultHandler<? super Result> resultHandler) {
        entryLock.writeLock().lock();
        try {
            final DN dn = request.getName();
            final DN parent = dn.parent();
            if (entries.containsKey(dn)) {
                throw newErrorResult(ResultCode.ENTRY_ALREADY_EXISTS, "The entry '" + dn.toString()
                        + "' already exists");
            } else if (!entries.containsKey(parent)) {
                noSuchObject(parent);
            } else {
                entries.put(dn, request);
                resultHandler.handleResult(getResult(request, null, request));
            }
        } catch (final ErrorResultException e) {
            resultHandler.handleErrorResult(e);
        } finally {
            entryLock.writeLock().unlock();
        }
    }
    @Override
    public void handleBind(final RequestContext requestContext, final int version,
            final BindRequest request,
            final IntermediateResponseHandler intermediateResponseHandler,
            final ResultHandler<? super BindResult> resultHandler) {
        entryLock.readLock().lock();
        try {
            final DN username = DN.valueOf(request.getName(), schema);
            final byte[] password;
            if (request instanceof SimpleBindRequest) {
                password = ((SimpleBindRequest) request).getPassword();
            } else if (request instanceof GenericBindRequest
                    && request.getAuthenticationType() == ((byte) 0x80)) {
                password = ((GenericBindRequest) request).getAuthenticationValue();
            } else {
                throw newErrorResult(ResultCode.PROTOCOL_ERROR,
                        "non-SIMPLE authentication not supported: "
                                + request.getAuthenticationType());
            }
            final Entry entry = getRequiredEntry(null, username);
            if (entry.containsAttribute("userPassword", password)) {
                resultHandler.handleResult(getBindResult(request, entry, entry));
            } else {
                throw newErrorResult(ResultCode.INVALID_CREDENTIALS, "Wrong password");
            }
        } catch (final LocalizedIllegalArgumentException e) {
            resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR, e));
        } catch (final EntryNotFoundException e) {
            /*
             * Usually you would not include a diagnostic message, but we'll add
             * one here because the memory back-end is not intended for
             * production use.
             */
            resultHandler.handleErrorResult(newErrorResult(ResultCode.INVALID_CREDENTIALS,
                    "Unknown user"));
        } catch (final ErrorResultException e) {
            resultHandler.handleErrorResult(e);
        } finally {
            entryLock.readLock().unlock();
        }
    }
    @Override
    public void handleCompare(final RequestContext requestContext, final CompareRequest request,
            final IntermediateResponseHandler intermediateResponseHandler,
            final ResultHandler<? super CompareResult> resultHandler) {
        entryLock.readLock().lock();
        try {
            final DN dn = request.getName();
            final Entry entry = getRequiredEntry(request, dn);
            final Attribute assertion =
                    singletonAttribute(request.getAttributeDescription(), request
                            .getAssertionValue());
            resultHandler.handleResult(getCompareResult(request, entry, entry.containsAttribute(
                    assertion, null)));
        } catch (final ErrorResultException e) {
            resultHandler.handleErrorResult(e);
        } finally {
            entryLock.readLock().unlock();
        }
    }
    @Override
    public void handleDelete(final RequestContext requestContext, final DeleteRequest request,
            final IntermediateResponseHandler intermediateResponseHandler,
            final ResultHandler<? super Result> resultHandler) {
        entryLock.writeLock().lock();
        try {
            final DN dn = request.getName();
            final Entry entry = getRequiredEntry(request, dn);
            if (request.getControl(SubtreeDeleteRequestControl.DECODER, decodeOptions) != null) {
                // Subtree delete.
                entries.subMap(dn, dn.child(RDN.maxValue())).clear();
            } else {
                // Must be leaf.
                final DN next = entries.higherKey(dn);
                if (next == null || !next.isChildOf(dn)) {
                    entries.remove(dn);
                } else {
                    throw newErrorResult(ResultCode.NOT_ALLOWED_ON_NONLEAF);
                }
            }
            resultHandler.handleResult(getResult(request, entry, null));
        } catch (final DecodeException e) {
            resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR, e));
        } catch (final ErrorResultException e) {
            resultHandler.handleErrorResult(e);
        } finally {
            entryLock.writeLock().unlock();
        }
    }
    @Override
    public <R extends ExtendedResult> void handleExtendedRequest(
            final RequestContext requestContext, final ExtendedRequest<R> request,
            final IntermediateResponseHandler intermediateResponseHandler,
            final ResultHandler<? super R> resultHandler) {
        resultHandler.handleErrorResult(newErrorResult(ResultCode.UNWILLING_TO_PERFORM,
                "Extended request operation not supported"));
    }
    @Override
    public void handleModify(final RequestContext requestContext, final ModifyRequest request,
            final IntermediateResponseHandler intermediateResponseHandler,
            final ResultHandler<? super Result> resultHandler) {
        entryLock.writeLock().lock();
        try {
            final DN dn = request.getName();
            final Entry entry = getRequiredEntry(request, dn);
            final Entry newEntry = new LinkedHashMapEntry(entry);
            entries.put(dn, modifyEntry(newEntry, request));
            resultHandler.handleResult(getResult(request, entry, newEntry));
        } catch (final ErrorResultException e) {
            resultHandler.handleErrorResult(e);
        } finally {
            entryLock.writeLock().unlock();
        }
    }
    @Override
    public void handleModifyDN(final RequestContext requestContext, final ModifyDNRequest request,
            final IntermediateResponseHandler intermediateResponseHandler,
            final ResultHandler<? super Result> resultHandler) {
        resultHandler.handleErrorResult(newErrorResult(ResultCode.UNWILLING_TO_PERFORM,
                "ModifyDN request operation not supported"));
    }
    @Override
    public void handleSearch(final RequestContext requestContext, final SearchRequest request,
            final IntermediateResponseHandler intermediateResponseHandler,
            final SearchResultHandler resultHandler) {
        entryLock.readLock().lock();
        try {
            final DN dn = request.getName();
            final Entry baseEntry = getRequiredEntry(request, dn);
            final SearchScope scope = request.getScope();
            final Filter filter = request.getFilter();
            final Matcher matcher = filter.matcher(schema);
            if (scope.equals(SearchScope.BASE_OBJECT)) {
                if (matcher.matches(baseEntry).toBoolean()) {
                    sendEntry(request, resultHandler, baseEntry);
                }
            } else if (scope.equals(SearchScope.SINGLE_LEVEL)) {
                final NavigableMap<DN, Entry> subtree =
                        entries.subMap(dn, dn.child(RDN.maxValue()));
                for (final Entry entry : subtree.values()) {
                    // Check for cancellation.
                    requestContext.checkIfCancelled(false);
                    final DN childDN = entry.getName();
                    if (childDN.isChildOf(dn)) {
                        if (matcher.matches(entry).toBoolean()
                                && !sendEntry(request, resultHandler, entry)) {
                            // Caller has asked to stop sending results.
                            break;
                        }
                    }
                }
            } else if (scope.equals(SearchScope.WHOLE_SUBTREE)) {
                final NavigableMap<DN, Entry> subtree =
                        entries.subMap(dn, dn.child(RDN.maxValue()));
                for (final Entry entry : subtree.values()) {
                    // Check for cancellation.
                    requestContext.checkIfCancelled(false);
                    if (matcher.matches(entry).toBoolean()
                            && !sendEntry(request, resultHandler, entry)) {
                        // Caller has asked to stop sending results.
                        break;
                    }
                }
            } else {
                throw newErrorResult(ResultCode.PROTOCOL_ERROR,
                        "Search request contains an unsupported search scope");
            }
            resultHandler.handleResult(newResult(ResultCode.SUCCESS));
        } catch (final ErrorResultException e) {
            resultHandler.handleErrorResult(e);
        } finally {
            entryLock.readLock().unlock();
        }
    }
    private <R extends Result> R addResultControls(final Request request, final Entry before,
            final Entry after, final R result) throws ErrorResultException {
        try {
            // Add pre-read response control if requested.
            final PreReadRequestControl preRead =
                    request.getControl(PreReadRequestControl.DECODER, decodeOptions);
            if (preRead != null) {
                if (preRead.isCritical() && before == null) {
                    throw newErrorResult(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
                } else {
                    result.addControl(PreReadResponseControl.newControl(filter(before, preRead
                            .getAttributes())));
                }
            }
            // Add post-read response control if requested.
            final PostReadRequestControl postRead =
                    request.getControl(PostReadRequestControl.DECODER, decodeOptions);
            if (postRead != null) {
                if (postRead.isCritical() && after == null) {
                    throw newErrorResult(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
                } else {
                    result.addControl(PostReadResponseControl.newControl(filter(after, postRead
                            .getAttributes())));
                }
            }
            return result;
        } catch (final DecodeException e) {
            throw newErrorResult(ResultCode.PROTOCOL_ERROR, e);
        }
    }
    private Entry filter(final Entry entry, final Collection<String> attributes) {
        // FIXME: attribute filtering not supported yet.
        return entry;
    }
    private BindResult getBindResult(final BindRequest request, final Entry before,
            final Entry after) throws ErrorResultException {
        return addResultControls(request, before, after, newBindResult(ResultCode.SUCCESS));
    }
    private CompareResult getCompareResult(final CompareRequest request, final Entry entry,
            final boolean compareResult) throws ErrorResultException {
        return addResultControls(
                request,
                entry,
                entry,
                newCompareResult(compareResult ? ResultCode.COMPARE_TRUE : ResultCode.COMPARE_FALSE));
    }
    private Entry getRequiredEntry(final Request request, final DN dn) throws ErrorResultException {
        final Entry entry = entries.get(dn);
        if (entry == null) {
            noSuchObject(dn);
        } else if (request != null) {
            AssertionRequestControl control;
            try {
                control = request.getControl(AssertionRequestControl.DECODER, decodeOptions);
            } catch (final DecodeException e) {
                throw newErrorResult(ResultCode.PROTOCOL_ERROR, e);
            }
            if (control != null) {
                final Filter filter = control.getFilter();
                final Matcher matcher = filter.matcher(schema);
                if (!matcher.matches(entry).toBoolean()) {
                    throw newErrorResult(ResultCode.ASSERTION_FAILED, "The filter '"
                            + filter.toString() + "' did not match the entry '"
                            + entry.getName().toString() + "'");
                }
            }
        }
        return entry;
    }
    private Result getResult(final Request request, final Entry before, final Entry after)
            throws ErrorResultException {
        return addResultControls(request, before, after, newResult(ResultCode.SUCCESS));
    }
    private void noSuchObject(final DN dn) throws ErrorResultException {
        throw newErrorResult(ResultCode.NO_SUCH_OBJECT, "The entry '" + dn.toString()
                + "' does not exist");
    }
    private boolean sendEntry(final SearchRequest request, final SearchResultHandler resultHandler,
            final Entry entry) {
        return resultHandler
                .handleEntry(newSearchResultEntry(filter(entry, request.getAttributes())));
    }
}
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Modification.java
@@ -33,7 +33,6 @@
 */
public final class Modification {
    private final ModificationType modificationType;
    private final Attribute attribute;
    /**
@@ -44,7 +43,8 @@
     * fully immutable:
     *
     * <pre>
     * Modification change = new Modification(modificationType, Types.unmodifiableAttribute(attribute));
     * Modification change = new Modification(modificationType, Attributes
     *         .unmodifiableAttribute(attribute));
     * </pre>
     *
     * @param modificationType
@@ -54,7 +54,6 @@
     */
    public Modification(final ModificationType modificationType, final Attribute attribute) {
        Validator.ensureNotNull(modificationType, attribute);
        this.modificationType = modificationType;
        this.attribute = attribute;
    }
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/controls/PostReadRequestControl.java
@@ -22,21 +22,24 @@
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 *      Portions copyright 2012 ForgeRock AS.
 *      Portions copyright 2012-2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.controls;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;
import static org.forgerock.opendj.ldap.CoreMessages.ERR_POSTREADREQ_CANNOT_DECODE_VALUE;
import static org.forgerock.opendj.ldap.CoreMessages.ERR_POSTREADREQ_NO_CONTROL_VALUE;
import static org.forgerock.opendj.ldap.CoreMessages.ERR_POSTREAD_CONTROL_BAD_OID;
import java.io.IOException;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.List;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.asn1.ASN1;
@@ -88,16 +91,16 @@
     */
    public static final String OID = "1.3.6.1.1.13.2";
    // The set of raw attributes to return in the entry.
    private final Set<String> attributes;
    // The list of raw attributes to return in the entry.
    private final List<String> attributes;
    private final boolean isCritical;
    private static final PostReadRequestControl CRITICAL_EMPTY_INSTANCE =
            new PostReadRequestControl(true, Collections.<String> emptySet());
            new PostReadRequestControl(true, Collections.<String> emptyList());
    private static final PostReadRequestControl NONCRITICAL_EMPTY_INSTANCE =
            new PostReadRequestControl(false, Collections.<String> emptySet());
            new PostReadRequestControl(false, Collections.<String> emptyList());
    /**
     * A decoder which can be used for decoding the post-read request control.
@@ -126,23 +129,23 @@
                    }
                    final ASN1Reader reader = ASN1.getReader(control.getValue());
                    Set<String> attributes;
                    List<String> attributes;
                    try {
                        reader.readStartSequence();
                        if (reader.hasNextElement()) {
                            final String firstAttribute = reader.readOctetStringAsString();
                            if (reader.hasNextElement()) {
                                attributes = new LinkedHashSet<String>();
                                attributes = new ArrayList<String>();
                                attributes.add(firstAttribute);
                                do {
                                    attributes.add(reader.readOctetStringAsString());
                                } while (reader.hasNextElement());
                                attributes = Collections.unmodifiableSet(attributes);
                                attributes = unmodifiableList(attributes);
                            } else {
                                attributes = Collections.singleton(firstAttribute);
                                attributes = singletonList(firstAttribute);
                            }
                        } else {
                            attributes = Collections.emptySet();
                            attributes = emptyList();
                        }
                        reader.readEndSequence();
                    } catch (final Exception ae) {
@@ -190,11 +193,11 @@
        if (attributes.isEmpty()) {
            return isCritical ? CRITICAL_EMPTY_INSTANCE : NONCRITICAL_EMPTY_INSTANCE;
        } else if (attributes.size() == 1) {
            return new PostReadRequestControl(isCritical, Collections.singleton(attributes
                    .iterator().next()));
            return new PostReadRequestControl(isCritical, singletonList(attributes.iterator()
                    .next()));
        } else {
            final Set<String> attributeSet = new LinkedHashSet<String>(attributes);
            return new PostReadRequestControl(isCritical, Collections.unmodifiableSet(attributeSet));
            return new PostReadRequestControl(isCritical, unmodifiableList(new ArrayList<String>(
                    attributes)));
        }
    }
@@ -221,28 +224,28 @@
        if (attributes.length == 0) {
            return isCritical ? CRITICAL_EMPTY_INSTANCE : NONCRITICAL_EMPTY_INSTANCE;
        } else if (attributes.length == 1) {
            return new PostReadRequestControl(isCritical, Collections.singleton(attributes[0]));
            return new PostReadRequestControl(isCritical, singletonList(attributes[0]));
        } else {
            final Set<String> attributeSet = new LinkedHashSet<String>(Arrays.asList(attributes));
            return new PostReadRequestControl(isCritical, Collections.unmodifiableSet(attributeSet));
            return new PostReadRequestControl(isCritical, unmodifiableList(new ArrayList<String>(
                    asList(attributes))));
        }
    }
    private PostReadRequestControl(final boolean isCritical, final Set<String> attributes) {
    private PostReadRequestControl(final boolean isCritical, final List<String> attributes) {
        this.isCritical = isCritical;
        this.attributes = attributes;
    }
    /**
     * Returns an unmodifiable set containing the names of attributes to be
     * Returns an unmodifiable list containing the names of attributes to be
     * included with the response control. Attributes that are sub-types of
     * listed attributes are implicitly included. The returned set may be empty,
     * indicating that all user attributes should be returned.
     * listed attributes are implicitly included. The returned list may be
     * empty, indicating that all user attributes should be returned.
     *
     * @return An unmodifiable set containing the names of attributes to be
     * @return An unmodifiable list containing the names of attributes to be
     *         included with the response control.
     */
    public Set<String> getAttributes() {
    public List<String> getAttributes() {
        return attributes;
    }
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/controls/PreReadRequestControl.java
@@ -22,21 +22,24 @@
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 *      Portions copyright 2012 ForgeRock AS.
 *      Portions copyright 2012-2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.controls;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;
import static org.forgerock.opendj.ldap.CoreMessages.ERR_PREREADREQ_CANNOT_DECODE_VALUE;
import static org.forgerock.opendj.ldap.CoreMessages.ERR_PREREADREQ_NO_CONTROL_VALUE;
import static org.forgerock.opendj.ldap.CoreMessages.ERR_PREREAD_CONTROL_BAD_OID;
import java.io.IOException;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.List;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.asn1.ASN1;
@@ -87,16 +90,16 @@
     */
    public static final String OID = "1.3.6.1.1.13.1";
    // The set of raw attributes to return in the entry.
    private final Set<String> attributes;
    // The list of raw attributes to return in the entry.
    private final List<String> attributes;
    private final boolean isCritical;
    private static final PreReadRequestControl CRITICAL_EMPTY_INSTANCE = new PreReadRequestControl(
            true, Collections.<String> emptySet());
            true, Collections.<String> emptyList());
    private static final PreReadRequestControl NONCRITICAL_EMPTY_INSTANCE =
            new PreReadRequestControl(false, Collections.<String> emptySet());
            new PreReadRequestControl(false, Collections.<String> emptyList());
    /**
     * A decoder which can be used for decoding the pre-read request control.
@@ -125,23 +128,23 @@
                    }
                    final ASN1Reader reader = ASN1.getReader(control.getValue());
                    Set<String> attributes;
                    List<String> attributes;
                    try {
                        reader.readStartSequence();
                        if (reader.hasNextElement()) {
                            final String firstAttribute = reader.readOctetStringAsString();
                            if (reader.hasNextElement()) {
                                attributes = new LinkedHashSet<String>();
                                attributes = new ArrayList<String>();
                                attributes.add(firstAttribute);
                                do {
                                    attributes.add(reader.readOctetStringAsString());
                                } while (reader.hasNextElement());
                                attributes = Collections.unmodifiableSet(attributes);
                                attributes = unmodifiableList(attributes);
                            } else {
                                attributes = Collections.singleton(firstAttribute);
                                attributes = singletonList(firstAttribute);
                            }
                        } else {
                            attributes = Collections.emptySet();
                            attributes = emptyList();
                        }
                        reader.readEndSequence();
                    } catch (final Exception ae) {
@@ -189,11 +192,11 @@
        if (attributes.isEmpty()) {
            return isCritical ? CRITICAL_EMPTY_INSTANCE : NONCRITICAL_EMPTY_INSTANCE;
        } else if (attributes.size() == 1) {
            return new PreReadRequestControl(isCritical, Collections.singleton(attributes
                    .iterator().next()));
            return new PreReadRequestControl(isCritical,
                    singletonList(attributes.iterator().next()));
        } else {
            final Set<String> attributeSet = new LinkedHashSet<String>(attributes);
            return new PreReadRequestControl(isCritical, Collections.unmodifiableSet(attributeSet));
            return new PreReadRequestControl(isCritical, unmodifiableList(new ArrayList<String>(
                    attributes)));
        }
    }
@@ -220,28 +223,28 @@
        if (attributes.length == 0) {
            return isCritical ? CRITICAL_EMPTY_INSTANCE : NONCRITICAL_EMPTY_INSTANCE;
        } else if (attributes.length == 1) {
            return new PreReadRequestControl(isCritical, Collections.singleton(attributes[0]));
            return new PreReadRequestControl(isCritical, singletonList(attributes[0]));
        } else {
            final Set<String> attributeSet = new LinkedHashSet<String>(Arrays.asList(attributes));
            return new PreReadRequestControl(isCritical, Collections.unmodifiableSet(attributeSet));
            return new PreReadRequestControl(isCritical, unmodifiableList(new ArrayList<String>(
                    asList(attributes))));
        }
    }
    private PreReadRequestControl(final boolean isCritical, final Set<String> attributes) {
    private PreReadRequestControl(final boolean isCritical, final List<String> attributes) {
        this.isCritical = isCritical;
        this.attributes = attributes;
    }
    /**
     * Returns an unmodifiable set containing the names of attributes to be
     * Returns an unmodifiable list containing the names of attributes to be
     * included with the response control. Attributes that are sub-types of
     * listed attributes are implicitly included. The returned set may be empty,
     * indicating that all user attributes should be returned.
     * listed attributes are implicitly included. The returned list may be
     * empty, indicating that all user attributes should be returned.
     *
     * @return An unmodifiable set containing the names of attributes to be
     * @return An unmodifiable list containing the names of attributes to be
     *         included with the response control.
     */
    public Set<String> getAttributes() {
    public List<String> getAttributes() {
        return attributes;
    }
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AbstractRequestImpl.java
@@ -22,7 +22,7 @@
 *
 *
 *      Copyright 2010 Sun Microsystems, Inc.
 *      Portions copyright 2012 ForgeRock AS.
 *      Portions copyright 2012-2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.requests;
@@ -45,6 +45,20 @@
 *            The type of request.
 */
abstract class AbstractRequestImpl<R extends Request> implements Request {
    // Used by unmodifiable implementations as well.
    static Control getControl(final List<Control> controls, final String oid) {
        // Avoid creating an iterator if possible.
        if (!controls.isEmpty()) {
            for (final Control control : controls) {
                if (control.getOID().equals(oid)) {
                    return control;
                }
            }
        }
        return null;
    }
    private final List<Control> controls = new LinkedList<Control>();
    /**
@@ -63,9 +77,9 @@
     * @throws NullPointerException
     *             If {@code request} was {@code null} .
     */
    AbstractRequestImpl(Request request) {
    AbstractRequestImpl(final Request request) {
        Validator.ensureNotNull(request);
        for (Control control : request.getControls()) {
        for (final Control control : request.getControls()) {
            // Create defensive copy.
            controls.add(GenericControl.newControl(control));
        }
@@ -74,6 +88,7 @@
    /**
     * {@inheritDoc}
     */
    @Override
    public final R addControl(final Control control) {
        Validator.ensureNotNull(control);
        controls.add(control);
@@ -83,27 +98,26 @@
    /**
     * {@inheritDoc}
     */
    public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
            final DecodeOptions options) throws DecodeException {
        Validator.ensureNotNull(decoder, options);
        // Avoid creating an iterator if possible.
        if (controls.isEmpty()) {
            return null;
        }
        for (final Control control : controls) {
            if (control.getOID().equals(decoder.getOID())) {
                return decoder.decodeControl(control, options);
            }
        }
        return null;
    @Override
    public boolean containsControl(final String oid) {
        return getControl(controls, oid) != null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
            final DecodeOptions options) throws DecodeException {
        Validator.ensureNotNull(decoder, options);
        final Control control = getControl(controls, decoder.getOID());
        return control != null ? decoder.decodeControl(control, options) : null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public final List<Control> getControls() {
        return controls;
    }
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableRequest.java
@@ -22,7 +22,7 @@
 *
 *
 *      Copyright 2010 Sun Microsystems, Inc.
 *      Portions copyright 2012 ForgeRock AS.
 *      Portions copyright 2012-2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.requests;
@@ -73,38 +73,38 @@
     * {@inheritDoc}
     */
    @Override
    public boolean containsControl(final String oid) {
        return impl.containsControl(oid);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
            final DecodeOptions options) throws DecodeException {
        Validator.ensureNotNull(decoder, options);
        final List<Control> controls = impl.getControls();
        // Avoid creating an iterator if possible.
        if (controls.isEmpty()) {
        final Control control = AbstractRequestImpl.getControl(controls, decoder.getOID());
        if (control != null) {
            // Got a match. Return a defensive copy only if necessary.
            final C decodedControl = decoder.decodeControl(control, options);
            if (decodedControl != control) {
                // This was not the original control so return it
                // immediately.
                return decodedControl;
            } else if (decodedControl instanceof GenericControl) {
                // Generic controls are immutable, so return it immediately.
                return decodedControl;
            } else {
                // Re-decode to get defensive copy.
                final GenericControl genericControl = GenericControl.newControl(control);
                return decoder.decodeControl(genericControl, options);
            }
        } else {
            return null;
        }
        for (final Control control : controls) {
            if (control.getOID().equals(decoder.getOID())) {
                // Got a match. Return a defensive copy only if necessary.
                final C decodedControl = decoder.decodeControl(control, options);
                if (decodedControl != control) {
                    // This was not the original control so return it
                    // immediately.
                    return decodedControl;
                } else if (decodedControl instanceof GenericControl) {
                    // Generic controls are immutable, so return it immediately.
                    return decodedControl;
                } else {
                    // Re-decode to get defensive copy.
                    final GenericControl genericControl = GenericControl.newControl(control);
                    return decoder.decodeControl(genericControl, options);
                }
            }
        }
        return null;
    }
    /**
@@ -113,8 +113,7 @@
    @Override
    public final List<Control> getControls() {
        // We need to make all controls unmodifiable as well, which implies
        // making
        // defensive copies where necessary.
        // making defensive copies where necessary.
        final Function<Control, Control, Void> function = new Function<Control, Control, Void>() {
            @Override
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/Request.java
@@ -22,7 +22,7 @@
 *
 *
 *      Copyright 2009-2010 Sun Microsystems, Inc.
 *      Portions copyright 2012 ForgeRock AS.
 *      Portions copyright 2012-2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.requests;
@@ -54,6 +54,17 @@
    Request addControl(Control control);
    /**
     * Returns {@code true} if this request contains the specified request
     * control.
     *
     * @param oid
     *            The numeric OID of the request control.
     * @return {@code true} if this request contains the specified request
     *         control.
     */
    boolean containsControl(String oid);
    /**
     * Decodes and returns the first control in this request having an OID
     * corresponding to the provided control decoder.
     *
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/AbstractResponseImpl.java
@@ -22,7 +22,7 @@
 *
 *
 *      Copyright 2010 Sun Microsystems, Inc.
 *      Portions copyright 2012 ForgeRock AS.
 *      Portions copyright 2012-2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.responses;
@@ -45,6 +45,19 @@
 *            The type of response.
 */
abstract class AbstractResponseImpl<S extends Response> implements Response {
    // Used by unmodifiable implementations as well.
    static Control getControl(final List<Control> controls, final String oid) {
        // Avoid creating an iterator if possible.
        if (!controls.isEmpty()) {
            for (final Control control : controls) {
                if (control.getOID().equals(oid)) {
                    return control;
                }
            }
        }
        return null;
    }
    private final List<Control> controls = new LinkedList<Control>();
    /**
@@ -63,9 +76,9 @@
     * @throws NullPointerException
     *             If {@code response} was {@code null} .
     */
    AbstractResponseImpl(Response response) {
    AbstractResponseImpl(final Response response) {
        Validator.ensureNotNull(response);
        for (Control control : response.getControls()) {
        for (final Control control : response.getControls()) {
            // Create defensive copy.
            controls.add(GenericControl.newControl(control));
        }
@@ -74,6 +87,7 @@
    /**
     * {@inheritDoc}
     */
    @Override
    public final S addControl(final Control control) {
        Validator.ensureNotNull(control);
        controls.add(control);
@@ -83,27 +97,26 @@
    /**
     * {@inheritDoc}
     */
    public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
            final DecodeOptions options) throws DecodeException {
        Validator.ensureNotNull(decoder, options);
        // Avoid creating an iterator if possible.
        if (controls.isEmpty()) {
            return null;
        }
        for (final Control control : controls) {
            if (control.getOID().equals(decoder.getOID())) {
                return decoder.decodeControl(control, options);
            }
        }
        return null;
    @Override
    public boolean containsControl(final String oid) {
        return getControl(controls, oid) != null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
            final DecodeOptions options) throws DecodeException {
        Validator.ensureNotNull(decoder, options);
        final Control control = getControl(controls, decoder.getOID());
        return control != null ? decoder.decodeControl(control, options) : null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public final List<Control> getControls() {
        return controls;
    }
@@ -112,5 +125,4 @@
    public abstract String toString();
    abstract S getThis();
}
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableResponseImpl.java
@@ -22,7 +22,7 @@
 *
 *
 *      Copyright 2010 Sun Microsystems, Inc.
 *      Portions copyright 2012 ForgeRock AS.
 *      Portions copyright 2012-2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.responses;
@@ -75,38 +75,38 @@
     * {@inheritDoc}
     */
    @Override
    public boolean containsControl(final String oid) {
        return impl.containsControl(oid);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
            final DecodeOptions options) throws DecodeException {
        Validator.ensureNotNull(decoder, options);
        final List<Control> controls = impl.getControls();
        // Avoid creating an iterator if possible.
        if (controls.isEmpty()) {
        final Control control = AbstractResponseImpl.getControl(controls, decoder.getOID());
        if (control != null) {
            // Got a match. Return a defensive copy only if necessary.
            final C decodedControl = decoder.decodeControl(control, options);
            if (decodedControl != control) {
                // This was not the original control so return it
                // immediately.
                return decodedControl;
            } else if (decodedControl instanceof GenericControl) {
                // Generic controls are immutable, so return it immediately.
                return decodedControl;
            } else {
                // Re-decode to get defensive copy.
                final GenericControl genericControl = GenericControl.newControl(control);
                return decoder.decodeControl(genericControl, options);
            }
        } else {
            return null;
        }
        for (final Control control : controls) {
            if (control.getOID().equals(decoder.getOID())) {
                // Got a match. Return a defensive copy only if necessary.
                final C decodedControl = decoder.decodeControl(control, options);
                if (decodedControl != control) {
                    // This was not the original control so return it
                    // immediately.
                    return decodedControl;
                } else if (decodedControl instanceof GenericControl) {
                    // Generic controls are immutable, so return it immediately.
                    return decodedControl;
                } else {
                    // Re-decode to get defensive copy.
                    final GenericControl genericControl = GenericControl.newControl(control);
                    return decoder.decodeControl(genericControl, options);
                }
            }
        }
        return null;
    }
    /**
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/Response.java
@@ -22,7 +22,7 @@
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 *      Portions copyright 2012 ForgeRock AS.
 *      Portions copyright 2012-2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.responses;
@@ -54,6 +54,17 @@
    Response addControl(Control control);
    /**
     * Returns {@code true} if this response contains the specified response
     * control.
     *
     * @param oid
     *            The numeric OID of the response control.
     * @return {@code true} if this response contains the specified response
     *         control.
     */
    boolean containsControl(String oid);
    /**
     * Decodes and returns the first control in this response having an OID
     * corresponding to the provided control decoder.
     *
opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties
@@ -22,7 +22,7 @@
#
#
#      Copyright 2010 Sun Microsystems, Inc.
#      Portions copyright 2011-2012 ForgeRock AS
#      Portions copyright 2011-2013 ForgeRock AS
#
ERR_ATTR_SYNTAX_UNKNOWN_APPROXIMATE_MATCHING_RULE=Unable to retrieve \
 approximate matching rule %s used as the default for the %s attribute syntax. \
@@ -1405,3 +1405,16 @@
ERR_LDIF_MALFORMED_CONTROL=Unable to parse LDIF change record starting at line %d \
 with distinguished name "%s" because it contained a malformed control \
 "%s"
ERR_ENTRY_UNKNOWN_MODIFICATION_TYPE=Unsupported modification type '%s'
ERR_ENTRY_DUPLICATE_VALUES=Unable to add one or more values to attribute \
 '%s' because at least one of the values already exists
ERR_ENTRY_NO_SUCH_VALUE=Unable to remove one or more values from attribute \
 '%s' because at least one of the attributes does not exist in the entry
ERR_ENTRY_INCREMENT_NO_SUCH_ATTRIBUTE=Unable to increment the value of attribute \
 '%s' because that attribute does not exist in the entry
ERR_ENTRY_INCREMENT_INVALID_VALUE_COUNT=Unable to increment the value of \
 attribute '%s' because the provided modification did not have exactly \
 one value to use as the increment
ERR_ENTRY_INCREMENT_CANNOT_PARSE_AS_INT=Unable to increment the value of \
 attribute '%s' because either the current value or the increment could \
 not be parsed as an integer
opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java
@@ -22,7 +22,7 @@
 *
 *
 *      Copyright 2010 Sun Microsystems, Inc.
 *      Portions copyright 2011-2012 ForgeRock AS
 *      Portions copyright 2011-2013 ForgeRock AS
 */
package org.forgerock.opendj.ldap;
@@ -103,6 +103,10 @@
            return request.addControl(cntrl);
        }
        public boolean containsControl(final String oid) {
            return request.containsControl(oid);
        }
        public <C extends Control> C getControl(final ControlDecoder<C> decoder,
                final DecodeOptions options) throws DecodeException {
            return request.getControl(decoder, options);
opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java
New file
@@ -0,0 +1,455 @@
/*
 * 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 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 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 2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap;
import static org.fest.assertions.Assertions.assertThat;
import static org.forgerock.opendj.ldap.Connections.newInternalConnectionFactory;
import static org.forgerock.opendj.ldap.Connections.newServerConnectionFactory;
import static org.forgerock.opendj.ldap.requests.Requests.newAddRequest;
import static org.forgerock.opendj.ldap.requests.Requests.newDeleteRequest;
import static org.forgerock.opendj.ldap.requests.Requests.newModifyRequest;
import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
import static org.forgerock.opendj.ldif.LDIFEntryReader.valueOfLDIFEntry;
import java.io.IOException;
import java.util.Arrays;
import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl;
import org.forgerock.opendj.ldap.controls.PostReadRequestControl;
import org.forgerock.opendj.ldap.controls.PostReadResponseControl;
import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
import org.forgerock.opendj.ldif.ConnectionEntryReader;
import org.forgerock.opendj.ldif.LDIFEntryReader;
import org.testng.annotations.Test;
/**
 * Memory backend tests.
 */
@SuppressWarnings("javadoc")
public class MemoryBackendTestCase extends SdkTestCase {
    @Test
    public void testAdd() throws Exception {
        final Connection connection = getConnection();
        final Entry newDomain =
                valueOfLDIFEntry("dn: dc=new domain,dc=com", "objectClass: domain",
                        "objectClass: top", "dc: new domain");
        connection.add(newDomain);
        assertThat(connection.readEntry("dc=new domain,dc=com")).isEqualTo(newDomain);
    }
    @Test(expectedExceptions = ConstraintViolationException.class)
    public void testAddAlreadyExists() throws Exception {
        final Connection connection = getConnection();
        connection.add(valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
                "objectClass: top", "dc: example"));
    }
    @Test(expectedExceptions = EntryNotFoundException.class)
    public void testAddNoParent() throws Exception {
        final Connection connection = getConnection();
        connection.add(valueOfLDIFEntry("dn: dc=new domain,dc=missing,dc=com",
                "objectClass: domain", "objectClass: top", "dc: new domain"));
    }
    @Test
    public void testAddPostRead() throws Exception {
        final Connection connection = getConnection();
        final Entry newDomain =
                valueOfLDIFEntry("dn: dc=new domain,dc=com", "objectClass: domain",
                        "objectClass: top", "dc: new domain");
        assertThat(
                connection.add(
                        newAddRequest(newDomain)
                                .addControl(PostReadRequestControl.newControl(true))).getControl(
                        PostReadResponseControl.DECODER, new DecodeOptions()).getEntry())
                .isEqualTo(newDomain);
    }
    @Test(expectedExceptions = ErrorResultException.class)
    public void testAddPreRead() throws Exception {
        final Connection connection = getConnection();
        final Entry newDomain =
                valueOfLDIFEntry("dn: dc=new domain,dc=com", "objectClass: domain",
                        "objectClass: top", "dc: new domain");
        connection.add(newAddRequest(newDomain).addControl(PreReadRequestControl.newControl(true)));
    }
    @Test
    public void testCompareFalse() throws Exception {
        final Connection connection = getConnection();
        assertThat(connection.compare("dc=example,dc=com", "objectclass", "person").matched())
                .isFalse();
    }
    @Test(expectedExceptions = EntryNotFoundException.class)
    public void testCompareNoSuchObject() throws Exception {
        final Connection connection = getConnection();
        connection.compare("uid=missing,ou=people,dc=example,dc=com", "uid", "missing");
    }
    @Test
    public void testCompareTrue() throws Exception {
        final Connection connection = getConnection();
        assertThat(connection.compare("dc=example,dc=com", "objectclass", "domain").matched())
                .isTrue();
    }
    @Test(expectedExceptions = AssertionFailureException.class)
    public void testDeleteAssertionFalse() throws Exception {
        final Connection connection = getConnection();
        connection.delete(newDeleteRequest("dc=xxx,dc=com").addControl(
                AssertionRequestControl.newControl(true, Filter.valueOf("(objectclass=person)"))));
    }
    @Test
    public void testDeleteAssertionTrue() throws Exception {
        final Connection connection = getConnection();
        connection.delete(newDeleteRequest("dc=xxx,dc=com").addControl(
                AssertionRequestControl.newControl(true, Filter.valueOf("(objectclass=domain)"))));
    }
    @Test(expectedExceptions = EntryNotFoundException.class)
    public void testDeleteNoSuchObject() throws Exception {
        final Connection connection = getConnection();
        connection.delete("uid=missing,ou=people,dc=example,dc=com");
    }
    @Test
    public void testDeleteOnLeaf() throws Exception {
        final Connection connection = getConnection();
        connection.delete("uid=test1,ou=people,dc=example,dc=com");
        try {
            connection.readEntry("dc=example,dc=com");
        } catch (final EntryNotFoundException expected) {
            // Do nothing.
        }
    }
    @Test(expectedExceptions = ConstraintViolationException.class)
    public void testDeleteOnNonLeaf() throws Exception {
        final Connection connection = getConnection();
        try {
            connection.delete("dc=example,dc=com");
        } finally {
            assertThat(connection.readEntry("dc=example,dc=com")).isNotNull();
        }
    }
    @Test(expectedExceptions = ErrorResultException.class)
    public void testDeletePostRead() throws Exception {
        final Connection connection = getConnection();
        connection.delete(newDeleteRequest("dc=xxx,dc=com").addControl(
                PostReadRequestControl.newControl(true)));
    }
    @Test
    public void testDeletePreRead() throws Exception {
        final Connection connection = getConnection();
        assertThat(
                connection.delete(
                        newDeleteRequest("dc=xxx,dc=com").addControl(
                                PreReadRequestControl.newControl(true))).getControl(
                        PreReadResponseControl.DECODER, new DecodeOptions()).getEntry()).isEqualTo(
                valueOfLDIFEntry("dn: dc=xxx,dc=com", "objectClass: domain", "objectClass: top",
                        "dc: xxx"));
    }
    @Test
    public void testDeleteSubtree() throws Exception {
        final Connection connection = getConnection();
        connection.deleteSubtree("dc=example,dc=com");
        for (final String name : Arrays.asList("dc=example,dc=com", "ou=people,dc=example,dc=com",
                "uid=test1,ou=people,dc=example,dc=com", "uid=test2,ou=people,dc=example,dc=com")) {
            try {
                connection.readEntry(name);
            } catch (final EntryNotFoundException expected) {
                // Do nothing.
            }
        }
        assertThat(connection.readEntry("dc=xxx,dc=com")).isNotNull();
    }
    @Test
    public void testModify() throws Exception {
        final Connection connection = getConnection();
        connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: description",
                "description: test description");
        assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
                        "objectClass: top", "dc: example", "description: test description"));
    }
    @Test(expectedExceptions = AssertionFailureException.class)
    public void testModifyAssertionFalse() throws Exception {
        final Connection connection = getConnection();
        connection.modify(newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
                "add: description", "description: test description").addControl(
                AssertionRequestControl.newControl(true, Filter.valueOf("(objectclass=person)"))));
    }
    @Test
    public void testModifyAssertionTrue() throws Exception {
        final Connection connection = getConnection();
        connection.modify(newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
                "add: description", "description: test description").addControl(
                AssertionRequestControl.newControl(true, Filter.valueOf("(objectclass=domain)"))));
    }
    @Test
    public void testModifyBindPostRead() throws Exception {
        final Connection connection = getConnection();
        assertThat(
                connection.modify(
                        newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
                                "add: description", "description: test description").addControl(
                                PostReadRequestControl.newControl(true))).getControl(
                        PostReadResponseControl.DECODER, new DecodeOptions()).getEntry())
                .isEqualTo(
                        valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
                                "objectClass: top", "dc: example", "description: test description"));
    }
    @Test
    public void testModifyIncrement() throws Exception {
        final Connection connection = getConnection();
        connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: integer",
                "integer: 100", "-", "increment: integer", "integer: 10");
        assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
                        "objectClass: top", "dc: example", "integer: 110"));
    }
    @Test(expectedExceptions = ConstraintViolationException.class)
    public void testModifyIncrementBadDelta() throws Exception {
        final Connection connection = getConnection();
        connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: integer",
                "integer: 100", "-", "increment: integer", "integer: nan");
    }
    @Test(expectedExceptions = ConstraintViolationException.class)
    public void testModifyIncrementBadValue() throws Exception {
        final Connection connection = getConnection();
        connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: integer",
                "integer: nan", "-", "increment: integer", "integer: 10");
    }
    @Test(expectedExceptions = EntryNotFoundException.class)
    public void testModifyNoSuchObject() throws Exception {
        final Connection connection = getConnection();
        connection.modify("dn: dc=missing,dc=com", "changetype: modify", "add: description",
                "description: test description");
    }
    @Test
    public void testModifyPermissiveWithDuplicateValues() throws Exception {
        final Connection connection = getConnection();
        connection.modify(newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
                "add: dc", "dc: example").addControl(
                PermissiveModifyRequestControl.newControl(true)));
        assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
                        "objectClass: top", "dc: example"));
    }
    @Test
    public void testModifyPermissiveWithMissingValues() throws Exception {
        final Connection connection = getConnection();
        connection.modify(newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
                "delete: dc", "dc: xxx")
                .addControl(PermissiveModifyRequestControl.newControl(true)));
        assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
                        "objectClass: top", "dc: example"));
    }
    @Test
    public void testModifyPreRead() throws Exception {
        final Connection connection = getConnection();
        assertThat(
                connection.modify(
                        newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
                                "add: description", "description: test description").addControl(
                                PreReadRequestControl.newControl(true))).getControl(
                        PreReadResponseControl.DECODER, new DecodeOptions()).getEntry()).isEqualTo(
                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
                        "objectClass: top", "dc: example"));
    }
    @Test(expectedExceptions = ConstraintViolationException.class)
    public void testModifyStrictWithDuplicateValues() throws Exception {
        final Connection connection = getConnection();
        connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: dc", "dc: example");
    }
    @Test(expectedExceptions = ConstraintViolationException.class)
    public void testModifyStrictWithMissingValues() throws Exception {
        final Connection connection = getConnection();
        connection.modify("dn: dc=example,dc=com", "changetype: modify", "delete: dc", "dc: xxx");
    }
    @Test
    public void testSearchBase() throws Exception {
        final Connection connection = getConnection();
        assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
                        "objectClass: top", "dc: example"));
    }
    @Test(expectedExceptions = EntryNotFoundException.class)
    public void testSearchBaseNoSuchObject() throws Exception {
        final Connection connection = getConnection();
        connection.readEntry("dc=missing,dc=com");
    }
    @Test
    public void testSearchOneLevel() throws Exception {
        final Connection connection = getConnection();
        final ConnectionEntryReader reader =
                connection.search("dc=com", SearchScope.SINGLE_LEVEL, "(objectClass=*)");
        assertThat(reader.readEntry()).isEqualTo(
                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
                        "objectClass: top", "dc: example"));
        assertThat(reader.readEntry()).isEqualTo(
                valueOfLDIFEntry("dn: dc=xxx,dc=com", "objectClass: domain", "objectClass: top",
                        "dc: xxx"));
        assertThat(reader.hasNext()).isFalse();
    }
    @Test
    public void testSearchSubtree() throws Exception {
        final Connection connection = getConnection();
        assertThat(
                connection.searchSingleEntry("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
                        "(uid=test1)")).isEqualTo(getUser1Entry());
    }
    @Test(expectedExceptions = EntryNotFoundException.class)
    public void testSearchSubtreeNotFound() throws Exception {
        final Connection connection = getConnection();
        connection.searchSingleEntry("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
                "(uid=missing)");
    }
    @Test
    public void testSimpleBind() throws Exception {
        final Connection connection = getConnection();
        connection.bind("uid=test1,ou=people,dc=example,dc=com", "password".toCharArray());
    }
    @Test(expectedExceptions = AuthenticationException.class)
    public void testSimpleBindBadPassword() throws Exception {
        final Connection connection = getConnection();
        connection.bind("uid=test1,ou=people,dc=example,dc=com", "bad".toCharArray());
    }
    @Test(expectedExceptions = AuthenticationException.class)
    public void testSimpleBindNoSuchUser() throws Exception {
        final Connection connection = getConnection();
        connection.bind("uid=missing,ou=people,dc=example,dc=com", "password".toCharArray());
    }
    @Test
    public void testSimpleBindPostRead() throws Exception {
        final Connection connection = getConnection();
        assertThat(
                connection.bind(
                        newSimpleBindRequest("uid=test1,ou=people,dc=example,dc=com",
                                "password".toCharArray()).addControl(
                                PostReadRequestControl.newControl(true))).getControl(
                        PostReadResponseControl.DECODER, new DecodeOptions()).getEntry())
                .isEqualTo(getUser1Entry());
    }
    @Test
    public void testSimpleBindPreRead() throws Exception {
        final Connection connection = getConnection();
        assertThat(
                connection.bind(
                        newSimpleBindRequest("uid=test1,ou=people,dc=example,dc=com",
                                "password".toCharArray()).addControl(
                                PreReadRequestControl.newControl(true))).getControl(
                        PreReadResponseControl.DECODER, new DecodeOptions()).getEntry()).isEqualTo(
                getUser1Entry());
    }
    private Connection getConnection() throws IOException, ErrorResultException {
        // @formatter:off
        final MemoryBackend backend =
                new MemoryBackend(new LDIFEntryReader(
                        "dn: dc=com",
                        "objectClass: domain",
                        "objectClass: top",
                        "dc: com",
                        "",
                        "dn: dc=example,dc=com",
                        "objectClass: domain",
                        "objectClass: top",
                        "dc: example",
                        "",
                        "dn: ou=People,dc=example,dc=com",
                        "objectClass: organizationalunit",
                        "objectClass: top",
                        "ou: People",
                        "",
                        "dn: uid=test1,ou=People,dc=example,dc=com",
                        "objectClass: top",
                        "objectClass: person",
                        "uid: test1",
                        "userpassword: password",
                        "cn: test user 1",
                        "sn: user 1",
                        "",
                        "dn: uid=test2,ou=People,dc=example,dc=com",
                        "objectClass: top",
                        "objectClass: person",
                        "uid: test2",
                        "userpassword: password",
                        "cn: test user 2",
                        "sn: user 2",
                        "",
                        "dn: dc=xxx,dc=com",
                        "objectClass: domain",
                        "objectClass: top",
                        "dc: xxx"
                ));
        // @formatter:on
        final Connection connection =
                newInternalConnectionFactory(newServerConnectionFactory(backend), null)
                        .getConnection();
        return connection;
    }
    private Entry getUser1Entry() {
        return valueOfLDIFEntry("dn: uid=test1,ou=People,dc=example,dc=com", "objectClass: top",
                "objectClass: person", "uid: test1", "userpassword: password", "cn: test user 1",
                "sn: user 1");
    }
}