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

Gaetan Boismal
04.00.2015 1df4f51adf614210ca4a9b9728327090ec5ea264
OPENDJ-1666 PR-19 CREST-3.0.0 Migration

Design changes overview:
* Make the code compiling against CREST-3.0.0
* Implement rest2ldap servlet and Http connection handler as a CHF filter/handler chain
* Move all of the functionality out of the opendj-rest2ldap-servlet module and into the opendj-rest2ldap module, since it doesn't require Servlet - Servlet integration is provided by CHF.
* Create an Ldap Http Handler which is shared by both rest2ldap servlet and httpConnectionHandler
* Remove Rest2LdapContextFactory because the authn filter and handler can communicate directly via Context objects rather than using Servlet "attributes".
* Rename Context to RequestState

Detailed changes performed:
Files which has been mostly modified:
* org.opends.server.protocols.http.CollectClientsConnectionFilter
* org.forgerock.opendj.rest2ldap.LDAPCollectionResourceProvider
* org.forgerock.opendj.rest2ldap.ReferenceAttributeMapper

Other changes are mostly minor, they just ensure that code compile against CREST-3.0.0.
4 files deleted
4 files added
2 files renamed
25 files modified
5204 ■■■■■ changed files
opendj-rest2ldap-servlet/pom.xml 192 ●●●● patch | view | raw | blame | history
opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPAuthnFilter.java 485 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java 160 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPContextFactory.java 176 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/package-info.java 20 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap-servlet/src/main/webapp/META-INF/services/org.forgerock.http.HttpApplication 16 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json patch | view | raw | blame | history
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml 61 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/pom.xml 6 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java 269 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java 128 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java 45 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/FilterType.java 10 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java 384 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java 55 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java 1125 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java 26 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java 153 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java 188 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java 64 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java 27 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java 167 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java 45 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java 126 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java 267 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java 20 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/pom.xml 24 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/assembly/opendj-archive-component.xml 1 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/CollectClientConnectionsFilter.java 518 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPClientConnection.java 64 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java 129 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LdapHttpApplication.java 177 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/protocol.properties 1 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java 72 ●●●● patch | view | raw | blame | history
pom.xml 3 ●●●● patch | view | raw | blame | history
opendj-rest2ldap-servlet/pom.xml
@@ -12,187 +12,91 @@
  ! Header, with the fields enclosed by brackets [] replaced by your own identifying
  ! information: "Portions Copyright [year] [name of copyright owner]".
  !
  ! Copyright 2013 ForgeRock AS.
  ! Copyright 2013-2015 ForgeRock AS.
  !
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>opendj-project</artifactId>
        <groupId>org.forgerock.opendj</groupId>
        <version>3.0.0-SNAPSHOT</version>
    </parent>
    <artifactId>opendj-rest2ldap-servlet</artifactId>
    <name>OpenDJ Commons REST LDAP Gateway</name>
    <description>
        Provides integration between the OpenDJ Commons REST Adapter and Servlet APIs.
    </description>
    <packaging>bundle</packaging>
  <packaging>war</packaging>
    <properties>
        <jacksonVersion>1.9.2</jacksonVersion>
    <!-- When released, with the 'binary.license.url' property set,
         this artifact will contain an additional binary license -->
    <include.binary.license>${project.build.directory}/${project.build.finalName}/WEB-INF/legal-notices</include.binary.license>
        <checkstyleHeaderLocation>org/forgerock/checkstyle/default-java-header</checkstyleHeaderLocation>
    </properties>
    <dependencies>
        <dependency>
      <groupId>org.forgerock.http</groupId>
      <artifactId>chf-http-servlet</artifactId>
      <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <dependency>
            <groupId>org.forgerock.opendj</groupId>
            <artifactId>opendj-rest2ldap</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.forgerock.opendj</groupId>
            <artifactId>opendj-grizzly</artifactId>
            <version>${project.version}</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <!-- Required for compilation -->
            <groupId>org.forgerock.commons</groupId>
            <artifactId>json-resource-servlet</artifactId>
            <version>${forgerockRestVersion}</version>
        </dependency>
        <dependency>
            <!-- Required for runtime via WAR overlay -->
            <groupId>org.forgerock.commons</groupId>
            <artifactId>json-resource-servlet</artifactId>
            <version>${forgerockRestVersion}</version>
            <type>war</type>
            <classifier>servlet</classifier>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-jdk14</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>jackson-core-asl</artifactId>
            <version>${jacksonVersion}</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>jackson-mapper-asl</artifactId>
            <version>${jacksonVersion}</version>
        </dependency>
    </dependencies>
    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <extensions>true</extensions>
            </plugin>
            <!--  include opendj-grizzly and its dependencies in war -->
            <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-dependency-plugin</artifactId>
              <executions>
                <execution>
                  <id>copy-grizzly</id>
                  <phase>package</phase>
                  <goals>
                    <goal>copy-dependencies</goal>
                  </goals>
                  <configuration>
                    <includeArtifactIds>opendj-grizzly</includeArtifactIds>
                    <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/lib</outputDirectory>
                    <overWriteReleases>false</overWriteReleases>
                    <overWriteSnapshots>false</overWriteSnapshots>
                    <overWriteIfNewer>true</overWriteIfNewer>
                  </configuration>
                </execution>
              </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <executions>
                    <execution>
                        <id>war</id>
                        <phase>package</phase>
                        <goals>
                            <goal>war</goal>
                        </goals>
                        <configuration>
                            <classifier>servlet</classifier>
                            <overlays>
                                <overlay>
                                    <groupId>org.forgerock.commons</groupId>
                                    <artifactId>json-resource-servlet</artifactId>
                                    <classifier>servlet</classifier>
                                    <includes>
                                        <include>WEB-INF/*.xml</include>
                                    </includes>
                                </overlay>
                            </overlays>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.eclipse.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>9.0.4.v20130625</version>
            </plugin>
        </plugins>
        <pluginManagement>
          <plugins>
            <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.-->
            <plugin>
              <groupId>org.eclipse.m2e</groupId>
              <artifactId>lifecycle-mapping</artifactId>
              <version>1.0.0</version>
        <version>9.2.11.v20150529</version>
              <configuration>
                <lifecycleMappingMetadata>
                  <pluginExecutions>
                    <pluginExecution>
                      <pluginExecutionFilter>
          <scanIntervalSeconds>10</scanIntervalSeconds>
          <webAppConfig>
            <contextPath>/</contextPath>
          </webAppConfig>
        </configuration>
      </plugin>
      <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-dependency-plugin</artifactId>
                        <versionRange>[2.6,)</versionRange>
                        <goals>
                          <goal>copy-dependencies</goal>
                        </goals>
                      </pluginExecutionFilter>
                      <action>
                        <ignore />
                      </action>
                    </pluginExecution>
                  </pluginExecutions>
                </lifecycleMappingMetadata>
        <artifactId>maven-war-plugin</artifactId>
        <configuration>
          <webResources>
            <resource>
              <targetPath>WEB-INF/legal-notices</targetPath>
              <directory>../legal-notices</directory>
              <excludes>
                <!-- The web-app does not include the documentation -->
                <exclude>CC-BY-NC-ND.txt</exclude>
              </excludes>
            </resource>
            <resource>
              <targetPath>/</targetPath>
              <directory>src/main/webapp/</directory>
              <excludes>
                <exclude>web.xml</exclude>
              </excludes>
            </resource>
          </webResources>
              </configuration>
            </plugin>
          </plugins>
        </pluginManagement>
    </build>
    <reporting>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-project-info-reports-plugin</artifactId>
                <reportSets>
                    <reportSet>
                        <reports>
                            <report>dependencies</report>
                        </reports>
                    </reportSet>
                </reportSets>
            </plugin>
        </plugins>
    </reporting>
</project>
opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPAuthnFilter.java
File was deleted
opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java
File was deleted
opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPContextFactory.java
File was deleted
opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/package-info.java
File was deleted
opendj-rest2ldap-servlet/src/main/webapp/META-INF/services/org.forgerock.http.HttpApplication
New file
@@ -0,0 +1,16 @@
#
# The contents of this file are subject to the terms of the Common Development and
# Distribution License (the License). You may not use this file except in compliance with the
# License.
#
# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
# specific language governing permission and limitations under the License.
#
# When distributing Covered Software, include this CDDL Header Notice in each file and include
# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
# Header, with the fields enclosed by brackets [] replaced by your own identifying
# information: "Portions copyright [year] [name of copyright owner]".
#
# Copyright 2015 ForgeRock AS.
#
org.forgerock.opendj.rest2ldap.Rest2LDAPHttpApplication
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml
@@ -1,49 +1,34 @@
<!--
  ! The contents of this file are subject to the terms of the Common Development and
  ! Distribution License (the License). You may not use this file except in compliance with the
  ! License.
  !
  ! You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
  ! specific language governing permission and limitations under the License.
  !
  ! When distributing Covered Software, include this CDDL Header Notice in each file and include
  ! the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
  ! Header, with the fields enclosed by brackets [] replaced by your own identifying
  ! information: "Portions Copyright [year] [name of copyright owner]".
  !
  ! Copyright 2015 ForgeRock AS.
  !
-->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    version="2.5">
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">
    <display-name>Json Resource Servlet</display-name>
    <display-name>OpenDJ REST LDAP Gateway</display-name>
    <servlet>
        <servlet-name>OpenDJ Commons REST LDAP Gateway</servlet-name>
        <servlet-class>org.forgerock.json.resource.servlet.HttpServlet</servlet-class>
        <init-param>
            <param-name>connection-factory-class</param-name>
            <param-value>org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPConnectionFactoryProvider</param-value>
        </init-param>
        <init-param>
            <param-name>config-file</param-name>
            <param-value>/opendj-rest2ldap-servlet.json</param-value>
        </init-param>
         <init-param>
            <param-name>context-factory-class</param-name>
            <param-value>org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory</param-value>
        </init-param>
        <servlet-name>OpenDJ REST LDAP Gateway</servlet-name>
        <servlet-class>org.forgerock.http.servlet.HttpFrameworkServlet</servlet-class>
        <async-supported>true</async-supported>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>OpenDJ Commons REST LDAP Gateway</servlet-name>
        <servlet-name>OpenDJ REST LDAP Gateway</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
    <filter>
        <filter-name>OpenDJ Commons REST LDAP Authentication Filter</filter-name>
        <filter-class>org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPAuthnFilter</filter-class>
        <init-param>
            <param-name>config-file</param-name>
            <param-value>/opendj-rest2ldap-servlet.json</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>OpenDJ Commons REST LDAP Authentication Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>
opendj-rest2ldap/pom.xml
@@ -12,7 +12,7 @@
  ! Header, with the fields enclosed by brackets [] replaced by your own identifying
  ! information: "Portions Copyright [year] [name of copyright owner]".
  !
  ! Copyright 2012 ForgeRock AS.
  ! Copyright 2012-2015 ForgeRock AS.
  !
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
@@ -39,12 +39,12 @@
        </dependency>
        <dependency>
            <groupId>org.forgerock.commons</groupId>
            <artifactId>json-fluent</artifactId>
            <artifactId>json-resource</artifactId>
            <version>${forgerockRestVersion}</version>
        </dependency>
        <dependency>
            <groupId>org.forgerock.commons</groupId>
            <artifactId>json-resource</artifactId>
          <artifactId>json-resource-http</artifactId>
            <version>${forgerockRestVersion}</version>
        </dependency>
        <dependency>
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
@@ -21,7 +21,6 @@
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.isNullOrEmpty;
import static org.forgerock.opendj.rest2ldap.Utils.transform;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
import java.util.ArrayList;
@@ -29,13 +28,12 @@
import java.util.List;
import java.util.Set;
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.NotSupportedException;
import org.forgerock.json.resource.PatchOperation;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.Entry;
@@ -43,14 +41,14 @@
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.opendj.ldap.ModificationType;
import org.forgerock.util.Function;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.Promises;
/**
 * An abstract LDAP attribute mapper which provides a simple mapping from a JSON
 * value to a single LDAP attribute.
 */
abstract class AbstractLDAPAttributeMapper<T extends AbstractLDAPAttributeMapper<T>> extends
        AttributeMapper {
abstract class AbstractLDAPAttributeMapper<T extends AbstractLDAPAttributeMapper<T>> extends AttributeMapper {
    List<Object> defaultJSONValues = emptyList();
    final AttributeDescription ldapAttributeName;
    private boolean isRequired;
@@ -101,25 +99,45 @@
    }
    @Override
    void create(final Context c, final JsonPointer path, final JsonValue v,
            final ResultHandler<List<Attribute>> h) {
        getNewLDAPAttributes(c, path, v, createAttributeHandler(path, h));
    Promise<List<Attribute>, ResourceException> create(
            final RequestState requestState, final JsonPointer path, final JsonValue v) {
        return getNewLDAPAttributes(requestState, path, v).then(
            new Function<Attribute, List<Attribute>, ResourceException>() {
                @Override
                public List<Attribute> apply(Attribute newLDAPAttribute) throws ResourceException {
                    if (!writabilityPolicy.canCreate(ldapAttributeName)) {
                        if (!newLDAPAttribute.isEmpty() && !writabilityPolicy.discardWrites()) {
                            throw new BadRequestException(i18n("The request cannot be processed because it attempts "
                                    + "to create the read-only field '%s'", path));
                        }
                        return Collections.emptyList();
                    } else if (newLDAPAttribute.isEmpty()) {
                        if (isRequired) {
                            throw new BadRequestException(i18n("The request cannot be processed because it attempts "
                                    + "to remove the required field '%s'", path));
                        }
                        return Collections.emptyList();
                    }
                    return singletonList(newLDAPAttribute);
                }
            });
    }
    @Override
    void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
            final Set<String> ldapAttributes) {
    void getLDAPAttributes(final RequestState requestState, final JsonPointer path,
            final JsonPointer subPath, final Set<String> ldapAttributes) {
        ldapAttributes.add(ldapAttributeName.toString());
    }
    abstract void getNewLDAPAttributes(Context c, JsonPointer path, List<Object> newValues,
            ResultHandler<Attribute> h);
    abstract Promise<Attribute, ResourceException> getNewLDAPAttributes(
                RequestState requestState, JsonPointer path, List<Object> newValues);
    abstract T getThis();
    @Override
    void patch(final Context c, final JsonPointer path, final PatchOperation operation,
            final ResultHandler<List<Modification>> h) {
    Promise<List<Modification>, ResourceException> patch(
                final RequestState requestState, final JsonPointer path, final PatchOperation operation) {
        try {
            final JsonPointer field = operation.getField();
            final JsonValue v = operation.getValue();
@@ -235,33 +253,95 @@
            if (newValues.isEmpty()) {
                // Deleting the attribute.
                if (isRequired) {
                    h.handleError(new BadRequestException(i18n(
                            "The request cannot be processed because it attempts to remove "
                                    + "the required field '%s'", path)));
                    return Promises.<List<Modification>, ResourceException> newExceptionPromise(
                            new BadRequestException(i18n(
                                "The request cannot be processed because it attempts to remove the required field '%s'",
                                path)));
                } else {
                    h.handleResult(singletonList(new Modification(modType,
                            emptyAttribute(ldapAttributeName))));
                    return Promises.newResultPromise(
                        singletonList(new Modification(modType, emptyAttribute(ldapAttributeName))));
                }
            } else {
                getNewLDAPAttributes(c, path, newValues, transform(
                        new Function<Attribute, List<Modification>, NeverThrowsException>() {
                return getNewLDAPAttributes(requestState, path, newValues)
                        .then(new Function<Attribute, List<Modification>, ResourceException>() {
                            @Override
                            public List<Modification> apply(final Attribute value) {
                                return singletonList(new Modification(modType, value));
                            }
                        }, h));
                        });
            }
        } catch (final RuntimeException e) {
            h.handleError(asResourceException(e));
            return Promises.newExceptionPromise(asResourceException(e));
        } catch (final ResourceException e) {
            h.handleError(e);
            return Promises.newExceptionPromise(e);
        }
    }
    @Override
    void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
            final ResultHandler<List<Modification>> h) {
        getNewLDAPAttributes(c, path, v, updateAttributeHandler(path, e, h));
    Promise<List<Modification>, ResourceException> update(
            final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) {
        return getNewLDAPAttributes(requestState, path, v).then(
            new Function<Attribute, List<Modification>, ResourceException>() {
                @Override
                public List<Modification> apply(final Attribute newLDAPAttribute) throws ResourceException {
                    // Get the existing LDAP attribute.
                    final Attribute tmp = e.getAttribute(ldapAttributeName);
                    final Attribute oldLDAPAttribute = tmp != null ? tmp : emptyAttribute(ldapAttributeName);
                    /*
                     * If the attribute is read-only then handle the following cases:
                     * 1) new values are provided and they are the same as the existing values
                     * 2) no new values are provided.
                     */
                    if (!writabilityPolicy.canWrite(ldapAttributeName)) {
                        if (newLDAPAttribute.isEmpty()
                                || newLDAPAttribute.equals(oldLDAPAttribute)
                                || writabilityPolicy.discardWrites()) {
                            // No change.
                            return Collections.emptyList();
                        }
                        throw new BadRequestException(i18n(
                            "The request cannot be processed because it attempts to modify the read-only field '%s'",
                            path));
                    }
                    if (oldLDAPAttribute.isEmpty() && newLDAPAttribute.isEmpty()) {
                        // No change.
                        return Collections.emptyList();
                    } else if (oldLDAPAttribute.isEmpty()) {
                        // The attribute is being added.
                        return singletonList(new Modification(ModificationType.REPLACE, newLDAPAttribute));
                    } else if (newLDAPAttribute.isEmpty()) {
                        // The attribute is being deleted - this is not allowed if the attribute is required.
                        if (isRequired) {
                            throw new BadRequestException(i18n(
                                "The request cannot be processed because it attempts to remove the required field '%s'",
                                path));
                        }
                        return singletonList(new Modification(ModificationType.REPLACE, newLDAPAttribute));
                    } else {
                        /*
                         * We could do a replace, but try to save bandwidth and send diffs instead.
                         * Perform deletes first in case we don't have an appropriate normalizer:
                         * permissive add(x) followed by delete(x) is destructive, whereas
                         * delete(x) followed by add(x) is idempotent when adding/removing the same value.
                         */
                        final List<Modification> modifications = new ArrayList<>(2);
                        final Attribute deletedValues = new LinkedAttribute(oldLDAPAttribute);
                        deletedValues.removeAll(newLDAPAttribute);
                        if (!deletedValues.isEmpty()) {
                            modifications.add(new Modification(ModificationType.DELETE, deletedValues));
                        }
                        final Attribute addedValues = new LinkedAttribute(newLDAPAttribute);
                        addedValues.removeAll(oldLDAPAttribute);
                        if (!addedValues.isEmpty()) {
                            modifications.add(new Modification(ModificationType.ADD, addedValues));
                        }
                        return modifications;
                    }
                }
            });
    }
    private List<Object> asList(final JsonValue v, final List<Object> defaultValues) {
@@ -290,142 +370,21 @@
        }
    }
    private ResultHandler<Attribute> createAttributeHandler(final JsonPointer path,
            final ResultHandler<List<Attribute>> h) {
        return new ResultHandler<Attribute>() {
            @Override
            public void handleError(final ResourceException error) {
                h.handleError(error);
            }
            @Override
            public void handleResult(final Attribute newLDAPAttribute) {
                if (!writabilityPolicy.canCreate(ldapAttributeName)) {
                    if (newLDAPAttribute.isEmpty() || writabilityPolicy.discardWrites()) {
                        h.handleResult(Collections.<Attribute> emptyList());
                    } else {
                        h.handleError(new BadRequestException(i18n(
                                "The request cannot be processed because it attempts to create "
                                        + "the read-only field '%s'", path)));
                    }
                } else if (newLDAPAttribute.isEmpty()) {
                    if (isRequired) {
                        h.handleError(new BadRequestException(i18n(
                                "The request cannot be processed because it attempts to remove "
                                        + "the required field '%s'", path)));
                        return;
                    } else {
                        h.handleResult(Collections.<Attribute> emptyList());
                    }
                } else {
                    h.handleResult(singletonList(newLDAPAttribute));
                }
            }
        };
    }
    private void getNewLDAPAttributes(final Context c, final JsonPointer path, final JsonValue v,
            final ResultHandler<Attribute> attributeHandler) {
    private Promise<Attribute, ResourceException> getNewLDAPAttributes(
            final RequestState requestState, final JsonPointer path, final JsonValue v) {
        try {
            // Ensure that the value is of the correct type.
            checkSchema(path, v);
            final List<Object> newValues = asList(v, defaultJSONValues);
            if (newValues.isEmpty()) {
                // Skip sub-class implementation if there are no values.
                attributeHandler.handleResult(emptyAttribute(ldapAttributeName));
                return Promises.newResultPromise(emptyAttribute(ldapAttributeName));
            } else {
                getNewLDAPAttributes(c, path, newValues, attributeHandler);
                return getNewLDAPAttributes(requestState, path, newValues);
            }
        } catch (final Exception ex) {
            attributeHandler.handleError(asResourceException(ex));
            return Promises.newExceptionPromise(asResourceException(ex));
        }
    }
    private ResultHandler<Attribute> updateAttributeHandler(final JsonPointer path, final Entry e,
            final ResultHandler<List<Modification>> h) {
        // Get the existing LDAP attribute.
        final Attribute tmp = e.getAttribute(ldapAttributeName);
        final Attribute oldLDAPAttribute = tmp != null ? tmp : emptyAttribute(ldapAttributeName);
        return new ResultHandler<Attribute>() {
            @Override
            public void handleError(final ResourceException error) {
                h.handleError(error);
            }
            @Override
            public void handleResult(final Attribute newLDAPAttribute) {
                /*
                 * If the attribute is read-only then handle the following
                 * cases:
                 *
                 * 1) new values are provided and they are the same as the
                 * existing values
                 *
                 * 2) no new values are provided.
                 */
                if (!writabilityPolicy.canWrite(ldapAttributeName)) {
                    if (newLDAPAttribute.isEmpty() || newLDAPAttribute.equals(oldLDAPAttribute)
                            || writabilityPolicy.discardWrites()) {
                        // No change.
                        h.handleResult(Collections.<Modification> emptyList());
                    } else {
                        h.handleError(new BadRequestException(i18n(
                                "The request cannot be processed because it attempts to modify "
                                        + "the read-only field '%s'", path)));
                    }
                } else {
                    // Compute the changes to the attribute.
                    final List<Modification> modifications;
                    if (oldLDAPAttribute.isEmpty() && newLDAPAttribute.isEmpty()) {
                        // No change.
                        modifications = Collections.<Modification> emptyList();
                    } else if (oldLDAPAttribute.isEmpty()) {
                        // The attribute is being added.
                        modifications =
                                singletonList(new Modification(ModificationType.REPLACE,
                                        newLDAPAttribute));
                    } else if (newLDAPAttribute.isEmpty()) {
                        /*
                         * The attribute is being deleted - this is not allowed
                         * if the attribute is required.
                         */
                        if (isRequired) {
                            h.handleError(new BadRequestException(i18n(
                                    "The request cannot be processed because it attempts to remove "
                                            + "the required field '%s'", path)));
                            return;
                        } else {
                            modifications =
                                    singletonList(new Modification(ModificationType.REPLACE,
                                            newLDAPAttribute));
                        }
                    } else {
                        /*
                         * We could do a replace, but try to save bandwidth and
                         * send diffs instead. Perform deletes first in case we
                         * don't have an appropriate normalizer: permissive
                         * add(x) followed by delete(x) is destructive, whereas
                         * delete(x) followed by add(x) is idempotent when
                         * adding/removing the same value.
                         */
                        modifications = new ArrayList<>(2);
                        final Attribute deletedValues = new LinkedAttribute(oldLDAPAttribute);
                        deletedValues.removeAll(newLDAPAttribute);
                        if (!deletedValues.isEmpty()) {
                            modifications.add(new Modification(ModificationType.DELETE,
                                    deletedValues));
                        }
                        final Attribute addedValues = new LinkedAttribute(newLDAPAttribute);
                        addedValues.removeAll(oldLDAPAttribute);
                        if (!addedValues.isEmpty()) {
                            modifications.add(new Modification(ModificationType.ADD, addedValues));
                        }
                    }
                    h.handleResult(modifications);
                }
            }
        };
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
@@ -11,26 +11,24 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2012-2013 ForgeRock AS.
 * Copyright 2012-2015 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
import java.util.List;
import java.util.Set;
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.PatchOperation;
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.util.promise.Promise;
/**
 * An attribute mapper is responsible for converting JSON values to and from
 * LDAP attributes.
 */
/** An attribute mapper is responsible for converting JSON values to and from LDAP attributes. */
public abstract class AttributeMapper {
    /*
     * This interface is an abstract class so that methods can be made package
@@ -42,19 +40,18 @@
    }
    /**
     * Maps a JSON value to one or more LDAP attributes, invoking a completion
     * handler once the transformation has completed. This method is invoked
     * when a REST resource is created using a create request.
     * Maps a JSON value to one or more LDAP attributes, returning a promise
     * once the transformation has completed. This method is invoked when a REST
     * resource is created using a create request.
     * <p>
     * If the JSON value corresponding to this mapper is not present in the
     * resource then this method will be invoked with a value of {@code null}.
     * It is the responsibility of the mapper implementation to take appropriate
     * action in this case, perhaps by substituting default LDAP values, or by
     * rejecting the update by invoking the result handler's
     * {@link ResultHandler#handleError handleError} method.
     * returning a failed promise with an appropriate {@link ResourceException}.
     *
     * @param c
     *            The context.
     * @param requestState
     *            The request state.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            attribute mapper. This may be used when constructing error
@@ -63,10 +60,10 @@
     *            The JSON value to be converted to LDAP attributes, which may
     *            be {@code null} indicating that the JSON value was not present
     *            in the resource.
     * @param h
     *            The result handler.
     * @return A {@link Promise} containing the result of the operation.
     */
    abstract void create(Context c, JsonPointer path, JsonValue v, ResultHandler<List<Attribute>> h);
    abstract Promise<List<Attribute>, ResourceException> create(
            RequestState requestState, JsonPointer path, JsonValue v);
    /**
     * Adds the names of the LDAP attributes required by this attribute mapper
@@ -75,8 +72,8 @@
     * Implementations should only add the names of attributes found in the LDAP
     * entry directly associated with the resource.
     *
     * @param c
     *            The context.
     * @param requestState
     *            The request state.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            attribute mapper. This may be used when constructing error
@@ -89,21 +86,20 @@
     *            The set into which the required LDAP attribute names should be
     *            put.
     */
    abstract void getLDAPAttributes(Context c, JsonPointer path, JsonPointer subPath,
            Set<String> ldapAttributes);
    abstract void getLDAPAttributes(
            RequestState requestState, JsonPointer path, JsonPointer subPath, Set<String> ldapAttributes);
    /**
     * Transforms the provided REST comparison filter parameters to an LDAP
     * filter representation, invoking a completion handler once the
     * transformation has completed.
     * filter representation, returning a promise once the transformation has
     * completed.
     * <p>
     * If an error occurred while constructing the LDAP filter, then the result
     * handler's {@link ResultHandler#handleError handleError} method must be
     * invoked with an appropriate exception indicating the problem which
     * occurred.
     * If an error occurred while constructing the LDAP filter, then a failed
     * promise must be returned with an appropriate {@link ResourceException}
     * indicating the problem which occurred.
     *
     * @param c
     *            The context.
     * @param requestState
     *            The request state.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            attribute mapper. This may be used when constructing error
@@ -121,19 +117,18 @@
     * @param valueAssertion
     *            The value assertion, or {@code null} if {@code type} is
     *            {@link FilterType#PRESENT}.
     * @param h
     *            The result handler.
     * @return A {@link Promise} containing the result of the operation.
     */
    abstract void getLDAPFilter(Context c, JsonPointer path, JsonPointer subPath, FilterType type,
            String operator, Object valueAssertion, ResultHandler<Filter> h);
    abstract Promise<Filter, ResourceException> getLDAPFilter(RequestState requestState, JsonPointer path,
            JsonPointer subPath, FilterType type, String operator, Object valueAssertion);
    /**
     * Maps a JSON patch operation to one or more LDAP modifications, invoking a
     * completion handler once the transformation has completed. This method is
     * invoked when a REST resource is modified using a patch request.
     * Maps a JSON patch operation to one or more LDAP modifications, returning
     * a promise once the transformation has completed. This method is invoked
     * when a REST resource is modified using a patch request.
     *
     * @param c
     *            The context.
     * @param requestState
     *            The request state.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            attribute mapper. This may be used when constructing error
@@ -143,67 +138,60 @@
     *            modifications. The targeted JSON field will be relative to
     *            this attribute mapper, or root if all attributes associated
     *            with this mapper have been targeted.
     * @param h
     *            The result handler.
     * @return A {@link Promise} containing the result of the operation.
     */
    abstract void patch(Context c, JsonPointer path, PatchOperation operation,
            ResultHandler<List<Modification>> h);
    abstract Promise<List<Modification>, ResourceException> patch(
            RequestState requestState, JsonPointer path, PatchOperation operation);
    /**
     * Maps one or more LDAP attributes to their JSON representation, invoking a
     * completion handler once the transformation has completed.
     * Maps one or more LDAP attributes to their JSON representation, returning
     * a promise once the transformation has completed.
     * <p>
     * This method is invoked whenever an LDAP entry is converted to a REST
     * resource, i.e. when responding to read, query, create, put, or patch
     * requests.
     * <p>
     * If the LDAP attributes are not present in the entry, perhaps because they
     * are optional, then implementations should invoke the result handler's
     * {@link ResultHandler#handleResult handleResult} method with a result of
     * {@code null}. If the LDAP attributes cannot be mapped for any other
     * reason, perhaps because they are required but missing, or they contain
     * unexpected content, then the result handler's
     * {@link ResultHandler#handleError handleError} method must be invoked with
     * an appropriate exception indicating the problem which occurred.
     * are optional, then implementations should return a successful promise
     * with a result of {@code null}. If the LDAP attributes cannot be mapped
     * for any other reason, perhaps because they are required but missing, or
     * they contain unexpected content, then a failed promise must be returned
     * with an appropriate exception indicating the problem which occurred.
     *
     * @param c
     *            The context.
     * @param requestState
     *            The request state.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            attribute mapper. This may be used when constructing error
     *            messages.
     * @param e
     *            The LDAP entry to be converted to JSON.
     * @param h
     *            The result handler.
     * @return A {@link Promise} containing the result of the operation.
     */
    abstract void read(Context c, JsonPointer path, Entry e, ResultHandler<JsonValue> h);
    abstract Promise<JsonValue, ResourceException> read(RequestState requestState, JsonPointer path, Entry e);
    /**
     * Maps a JSON value to one or more LDAP modifications, invoking a
     * completion handler once the transformation has completed. This method is
     * invoked when a REST resource is modified using an update request.
     * Maps a JSON value to one or more LDAP modifications, returning a promise
     * once the transformation has completed. This method is invoked when a REST
     * resource is modified using an update request.
     * <p>
     * If the JSON value corresponding to this mapper is not present in the
     * resource then this method will be invoked with a value of {@code null}.
     * It is the responsibility of the mapper implementation to take appropriate
     * action in this case, perhaps by substituting default LDAP values, or by
     * rejecting the update by invoking the result handler's
     * {@link ResultHandler#handleError handleError} method.
     * returning a failed promise with an appropriate {@link ResourceException}.
     *
     * @param c
     *            The context.
     * @param requestState
     *            The request state.
     * @param v
     *            The JSON value to be converted to LDAP attributes, which may
     *            be {@code null} indicating that the JSON value was not present
     *            in the resource.
     * @param h
     *            The result handler.
     * @return A {@link Promise} containing the result of the operation.
     */
    abstract void update(Context c, JsonPointer path, Entry e, JsonValue v,
            ResultHandler<List<Modification>> h);
    abstract Promise<List<Modification>, ResourceException> update(
            RequestState requestState, JsonPointer path, Entry e, JsonValue v);
    // TODO: methods for obtaining schema information (e.g. name, description,
    // type information).
    // TODO: methods for obtaining schema information (e.g. name, description, type information).
    // TODO: methods for creating sort controls.
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
@@ -11,18 +11,14 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2013 ForgeRock AS.
 * Copyright 2013-2015 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.*;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.Context;
import org.forgerock.json.resource.InternalServerErrorException;
import org.forgerock.json.resource.PersistenceConfig;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.http.Context;
import org.forgerock.http.context.AbstractContext;
import org.forgerock.opendj.ldap.Connection;
/**
@@ -32,7 +28,7 @@
 * servlet filter. It is the responsibility of the component which acquired the
 * connection to release once processing has completed.
 */
public final class AuthenticatedConnectionContext extends Context {
public final class AuthenticatedConnectionContext extends AbstractContext {
    /*
     * TODO: this context does not support persistence because there is no
     * obvious way to restore the connection. We could just persist the context
@@ -53,7 +49,7 @@
     *            re-used for subsequent LDAP operations.
     */
    public AuthenticatedConnectionContext(final Context parent, final Connection connection) {
        super(ensureNotNull(parent));
        super(ensureNotNull(parent), "authenticated connection");
        this.connection = connection;
    }
@@ -69,38 +65,13 @@
     *            The cached pre-authenticated LDAP connection which should be
     *            re-used for subsequent LDAP operations.
     */
    public AuthenticatedConnectionContext(final String id, final Context parent,
    AuthenticatedConnectionContext(final String id, final Context parent,
            final Connection connection) {
        super(id, ensureNotNull(parent));
        super(id, "authenticated connection", ensureNotNull(parent));
        this.connection = connection;
    }
    /**
     * Restore from JSON representation.
     *
     * @param savedContext
     *            The JSON representation from which this context's attributes
     *            should be parsed.
     * @param config
     *            The persistence configuration.
     * @throws ResourceException
     *             If the JSON representation could not be parsed.
     */
    AuthenticatedConnectionContext(final JsonValue savedContext, final PersistenceConfig config)
            throws ResourceException {
        super(savedContext, config);
        throw new InternalServerErrorException(i18n("Cached LDAP connections cannot be restored"));
    }
    /** {@inheritDoc} */
    @Override
    protected void saveToJson(final JsonValue savedContext, final PersistenceConfig config)
            throws ResourceException {
        super.saveToJson(savedContext, config);
        throw new InternalServerErrorException(i18n("Cached LDAP connections cannot be persisted"));
    }
    /**
     * Returns the cached pre-authenticated LDAP connection which should be
     * re-used for subsequent LDAP operations.
     *
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/FilterType.java
@@ -11,15 +11,13 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2013 ForgeRock AS.
 * Copyright 2013-2015 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
import org.forgerock.json.resource.QueryFilter;
import org.forgerock.util.query.QueryFilter;
/**
 * An enumeration of the commons REST query comparison filter types.
 */
/** An enumeration of the commons REST query comparison filter types. */
enum FilterType {
    /**
@@ -83,5 +81,5 @@
     *
     * @see QueryFilter#startsWith
     */
    STARTS_WITH;
    STARTS_WITH
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java
New file
@@ -0,0 +1,384 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2013-2015 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.json.resource.SecurityContext.AUTHZID_DN;
import static org.forgerock.json.resource.SecurityContext.AUTHZID_ID;
import static org.forgerock.opendj.ldap.Connections.uncloseable;
import static org.forgerock.opendj.ldap.LdapException.newLdapException;
import static org.forgerock.opendj.ldap.requests.Requests.newPlainSASLBindRequest;
import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
import static org.forgerock.util.Utils.closeSilently;
import java.io.Closeable;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.http.Context;
import org.forgerock.http.Handler;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.http.protocol.Status;
import org.forgerock.json.JsonValue;
import org.forgerock.json.JsonValueException;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.SecurityContext;
import org.forgerock.opendj.ldap.AuthenticationException;
import org.forgerock.opendj.ldap.AuthorizationException;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.EntryNotFoundException;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.responses.BindResult;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.util.AsyncFunction;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.Promises;
/** An LDAP based HTTP authentication filter. */
final class HttpAuthenticationFilter implements org.forgerock.http.Filter, Closeable {
    /** Indicates how authentication should be performed. */
    private enum AuthenticationMethod {
        SASL_PLAIN,
        SEARCH_SIMPLE,
        SIMPLE
    }
    private final Schema schema = Schema.getDefaultSchema();
    private final String altAuthenticationPasswordHeader;
    private final String altAuthenticationUsernameHeader;
    private final AuthenticationMethod authenticationMethod;
    private final ConnectionFactory bindLDAPConnectionFactory;
    private final boolean reuseAuthenticatedConnection;
    private final String saslAuthzIdTemplate;
    private final DN searchBaseDN;
    private final String searchFilterTemplate;
    private final ConnectionFactory searchLDAPConnectionFactory;
    private final SearchScope searchScope;
    private final boolean supportAltAuthentication;
    private final boolean supportHTTPBasicAuthentication;
    HttpAuthenticationFilter(final JsonValue configuration) {
        // Parse the authentication configuration.
        final JsonValue authnConfig = configuration.get("authenticationFilter");
        supportHTTPBasicAuthentication = authnConfig.get("supportHTTPBasicAuthentication").required().asBoolean();
        // Alternative HTTP authentication.
        supportAltAuthentication = authnConfig.get("supportAltAuthentication").required().asBoolean();
        if (supportAltAuthentication) {
            altAuthenticationUsernameHeader = authnConfig.get("altAuthenticationUsernameHeader").required().asString();
            altAuthenticationPasswordHeader = authnConfig.get("altAuthenticationPasswordHeader").required().asString();
        } else {
            altAuthenticationUsernameHeader = null;
            altAuthenticationPasswordHeader = null;
        }
        // Should the authenticated connection should be cached for use by subsequent LDAP operations?
        reuseAuthenticatedConnection = authnConfig.get("reuseAuthenticatedConnection").required().asBoolean();
        // Parse the authentication method and associated parameters.
        authenticationMethod = parseAuthenticationMethod(authnConfig);
        switch (authenticationMethod) {
        case SASL_PLAIN:
            saslAuthzIdTemplate = authnConfig.get("saslAuthzIdTemplate").required().asString();
            searchBaseDN = null;
            searchScope = null;
            searchFilterTemplate = null;
            searchLDAPConnectionFactory = null;
            break;
        case SEARCH_SIMPLE:
            searchBaseDN = DN.valueOf(authnConfig.get("searchBaseDN").required().asString(), schema);
            searchScope = parseSearchScope(authnConfig);
            searchFilterTemplate = authnConfig.get("searchFilterTemplate").required().asString();
            // Parse the LDAP connection factory to be used for searches.
            final String ldapFactoryName = authnConfig.get("searchLDAPConnectionFactory").required().asString();
            searchLDAPConnectionFactory = Rest2LDAP
                .configureConnectionFactory(configuration.get("ldapConnectionFactories").required(), ldapFactoryName);
            saslAuthzIdTemplate = null;
            break;
        case SIMPLE:
        default:
            saslAuthzIdTemplate = null;
            searchBaseDN = null;
            searchScope = null;
            searchFilterTemplate = null;
            searchLDAPConnectionFactory = null;
            break;
        }
        // Parse the LDAP connection factory to be used for binds.
        final String ldapFactoryName = authnConfig.get("bindLDAPConnectionFactory").required().asString();
        bindLDAPConnectionFactory = Rest2LDAP.configureConnectionFactory(
            configuration.get("ldapConnectionFactories").required(), ldapFactoryName);
    }
    private static AuthenticationMethod parseAuthenticationMethod(final JsonValue configuration) {
        if (configuration.isDefined("method")) {
            final String method = configuration.get("method").asString();
            if ("simple".equalsIgnoreCase(method)) {
                return AuthenticationMethod.SIMPLE;
            } else if ("sasl-plain".equalsIgnoreCase(method)) {
                return AuthenticationMethod.SASL_PLAIN;
            } else if ("search-simple".equalsIgnoreCase(method)) {
                return AuthenticationMethod.SEARCH_SIMPLE;
            } else {
                throw new JsonValueException(configuration,
                    "Illegal authentication method: must be either 'simple', 'sasl-plain', or 'search-simple'");
            }
        } else {
            return AuthenticationMethod.SEARCH_SIMPLE;
        }
    }
    private static SearchScope parseSearchScope(final JsonValue configuration) {
        if (configuration.isDefined("searchScope")) {
            final String scope = configuration.get("searchScope").asString();
            if ("sub".equalsIgnoreCase(scope)) {
                return SearchScope.WHOLE_SUBTREE;
            } else if ("one".equalsIgnoreCase(scope)) {
                return SearchScope.SINGLE_LEVEL;
            } else {
                throw new JsonValueException(configuration, "Illegal search scope: must be either 'sub' or 'one'");
            }
        } else {
            return SearchScope.WHOLE_SUBTREE;
        }
    }
    @Override
    public Promise<Response, NeverThrowsException> filter(final Context context, final Request request,
                                                          final Handler next) {
        // Store the authenticated connection so that it can be re-used by the handler if needed.
        // However, make sure that it is closed on completion.
        try {
            final String headerUsername =
                supportAltAuthentication ? request.getHeaders().getFirst(altAuthenticationUsernameHeader) : null;
            final String headerPassword =
                supportAltAuthentication ? request.getHeaders().getFirst(altAuthenticationPasswordHeader) : null;
            final String headerAuthorization =
                supportHTTPBasicAuthentication ? request.getHeaders().getFirst("Authorization") : null;
            final String username;
            final char[] password;
            if (headerUsername != null) {
                if (headerPassword == null || headerUsername.isEmpty() || headerPassword.isEmpty()) {
                    throw ResourceException.getException(401);
                }
                username = headerUsername;
                password = headerPassword.toCharArray();
            } else if (headerAuthorization != null) {
                final StringTokenizer st = new StringTokenizer(headerAuthorization);
                final String method = st.nextToken();
                if (method == null || !"BASIC".equalsIgnoreCase(method)) {
                    throw ResourceException.getException(401);
                }
                final String b64Credentials = st.nextToken();
                if (b64Credentials == null) {
                    throw ResourceException.getException(401);
                }
                final String credentials = ByteString.valueOfBase64(b64Credentials).toString();
                final String[] usernameAndPassword = credentials.split(":");
                if (usernameAndPassword.length != 2) {
                    throw ResourceException.getException(401);
                }
                username = usernameAndPassword[0];
                password = usernameAndPassword[1].toCharArray();
            } else {
                throw ResourceException.getException(401);
            }
            // If we've got here then we have a username and password.
            switch (authenticationMethod) {
            case SIMPLE: {
                final Map<String, Object> authzid;
                authzid = new LinkedHashMap<>(2);
                authzid.put(AUTHZID_DN, username);
                authzid.put(AUTHZID_ID, username);
                return doBind(
                    context, request, next, Requests.newSimpleBindRequest(username, password), username, authzid);
            }
            case SASL_PLAIN: {
                final Map<String, Object> authzid;
                final String bindId;
                if (saslAuthzIdTemplate.startsWith("dn:")) {
                    final String bindDN = DN.format(saslAuthzIdTemplate.substring(3), schema, username).toString();
                    bindId = "dn:" + bindDN;
                    authzid = new LinkedHashMap<>(2);
                    authzid.put(AUTHZID_DN, bindDN);
                    authzid.put(AUTHZID_ID, username);
                } else {
                    bindId = String.format(saslAuthzIdTemplate, username);
                    authzid = Collections.singletonMap(AUTHZID_ID, (Object) username);
                }
                return doBind(context, request, next, newPlainSASLBindRequest(bindId, password), username, authzid);
            }
            default: // SEARCH_SIMPLE
                final AtomicReference<Connection> savedConnection = new AtomicReference<>();
                return searchLDAPConnectionFactory.getConnectionAsync()
                    .thenAsync(doSearchForUser(username, savedConnection))
                    .thenAsync(doBindAfterSearch(context, request, next, username, password, savedConnection),
                        returnErrorAfterFailedSearch(savedConnection));
            }
        } catch (final Throwable t) {
            return asErrorResponse(t);
        }
    }
    private AsyncFunction<Connection, SearchResultEntry, LdapException> doSearchForUser(
            final String username, final AtomicReference<Connection> savedConnection) {
        return new AsyncFunction<Connection, SearchResultEntry, LdapException>() {
            @Override
            public Promise<SearchResultEntry, LdapException> apply(final Connection connection) throws LdapException {
                savedConnection.set(connection);
                final Filter filter = Filter.format(searchFilterTemplate, username);
                return connection.searchSingleEntryAsync(newSearchRequest(searchBaseDN, searchScope, filter, "1.1"));
            }
        };
    }
    private AsyncFunction<SearchResultEntry, Response, NeverThrowsException> doBindAfterSearch(
            final Context context, final Request request, final Handler next, final String username,
            final char[] password, final AtomicReference<Connection> savedConnection) {
        return new AsyncFunction<SearchResultEntry, Response, NeverThrowsException>() {
            @Override
            public Promise<Response, NeverThrowsException> apply(final SearchResultEntry entry) {
                closeConnection(savedConnection);
                final String bindDN = entry.getName().toString();
                final Map<String, Object> authzid = new LinkedHashMap<>(2);
                authzid.put(AUTHZID_DN, bindDN);
                authzid.put(AUTHZID_ID, username);
                return doBind(context, request, next, newSimpleBindRequest(bindDN, password), username, authzid);
            }
        };
    }
    /**
     * Get a bind connection and then perform the bind operation, setting the cached connection and authorization
     * credentials on completion.
     */
    private Promise<Response, NeverThrowsException> doBind(
            final Context context, final Request request, final Handler next, final BindRequest bindRequest,
            final String authcid, final Map<String, Object> authzid) {
        final AtomicReference<Connection> savedConnection = new AtomicReference<>();
        return bindLDAPConnectionFactory.getConnectionAsync()
            .thenAsync(new AsyncFunction<Connection, BindResult, LdapException>() {
                @Override
                public Promise<BindResult, LdapException> apply(final Connection connection) throws LdapException {
                    savedConnection.set(connection);
                    return connection.bindAsync(bindRequest);
                }
            })
            .thenAsync(doChain(context, request, next, authcid, authzid, savedConnection),
                       returnErrorAfterFailedBind())
            .thenFinally(new Runnable() {
                @Override
                public void run() {
                    closeConnection(savedConnection);
                }
            });
    }
    private AsyncFunction<BindResult, Response, NeverThrowsException> doChain(
            final Context context, final Request request, final Handler next, final String authcid,
            final Map<String, Object> authzid, final AtomicReference<Connection> savedConnection) {
        return new AsyncFunction<BindResult, Response, NeverThrowsException>() {
            @Override
            public Promise<Response, NeverThrowsException> apply(final BindResult result) {
                // Pass through the authentication ID and authorization principals.
                Context forwardedContext = new SecurityContext(context, authcid, authzid);
                // Cache the pre-authenticated connection and prevent downstream
                // components from closing it since this filter will close it.
                if (reuseAuthenticatedConnection) {
                    forwardedContext = new AuthenticatedConnectionContext(
                            forwardedContext, uncloseable(savedConnection.get()));
                }
                return next.handle(forwardedContext, request);
            }
        };
    }
    private AsyncFunction<LdapException, Response, NeverThrowsException> returnErrorAfterFailedSearch(
            final AtomicReference<Connection> savedConnection) {
        return new AsyncFunction<LdapException, Response, NeverThrowsException>() {
            @Override
            public Promise<Response, NeverThrowsException> apply(final LdapException e) {
                if (closeConnection(savedConnection)) {
                    // The search error should not be passed as-is back to the user.
                    if (e instanceof EntryNotFoundException || e instanceof MultipleEntriesFoundException) {
                        return asErrorResponse(newLdapException(ResultCode.INVALID_CREDENTIALS, e));
                    } else if (e instanceof AuthenticationException || e instanceof AuthorizationException) {
                        return asErrorResponse(newLdapException(ResultCode.CLIENT_SIDE_LOCAL_ERROR, e));
                    } else {
                        return asErrorResponse(e);
                    }
                } else {
                    return asErrorResponse(e);
                }
            }
        };
    }
    private AsyncFunction<LdapException, Response, NeverThrowsException> returnErrorAfterFailedBind() {
        return new AsyncFunction<LdapException, Response, NeverThrowsException>() {
            @Override
            public Promise<Response, NeverThrowsException> apply(final LdapException e) {
                return asErrorResponse(e);
            }
        };
    }
    private Promise<Response, NeverThrowsException> asErrorResponse(final Throwable t) {
        final ResourceException e = asResourceException(t);
        final Response response =
            new Response().setStatus(Status.valueOf(e.getCode())).setEntity(e.toJsonValue().getObject());
        return Promises.newResultPromise(response);
    }
    private boolean closeConnection(final AtomicReference<Connection> savedConnection) {
        final Connection connection = savedConnection.get();
        if (connection != null) {
            connection.close();
            return true;
        }
        return false;
    }
    @Override
    public void close() {
        closeSilently(searchLDAPConnectionFactory, bindLDAPConnectionFactory);
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
@@ -11,7 +11,7 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2012-2013 ForgeRock AS.
 * Copyright 2012-2015 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
@@ -26,15 +26,17 @@
import java.util.List;
import java.util.Set;
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.PatchOperation;
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.Promises;
/**
 * An attribute mapper which maps a single JSON attribute to a fixed value.
@@ -52,27 +54,25 @@
    }
    @Override
    void create(final Context c, final JsonPointer path, final JsonValue v,
            final ResultHandler<List<Attribute>> h) {
    Promise<List<Attribute>, ResourceException> create(
            final RequestState requestState, final JsonPointer path, final JsonValue v) {
        if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) {
            h.handleError(new BadRequestException(i18n(
                    "The request cannot be processed because it attempts to create "
                            + "the read-only field '%s'", path)));
            return Promises.<List<Attribute>, ResourceException> newExceptionPromise(new BadRequestException(i18n(
                    "The request cannot be processed because it attempts to create the read-only field '%s'", path)));
        } else {
            h.handleResult(Collections.<Attribute> emptyList());
            return Promises.newResultPromise(Collections.<Attribute> emptyList());
        }
    }
    @Override
    void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
    void getLDAPAttributes(final RequestState requestState, final JsonPointer path, final JsonPointer subPath,
            final Set<String> ldapAttributes) {
        // Nothing to do.
    }
    @Override
    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
            final FilterType type, final String operator, final Object valueAssertion,
            final ResultHandler<Filter> h) {
    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
        final Filter filter;
        final JsonValue subValue = value.get(subPath);
        if (subValue == null) {
@@ -105,32 +105,29 @@
            // This attribute mapper is a candidate but it does not match.
            filter = alwaysFalse();
        }
        h.handleResult(filter);
        return Promises.newResultPromise(filter);
    }
    @Override
    void patch(final Context c, final JsonPointer path, final PatchOperation operation,
            final ResultHandler<List<Modification>> h) {
        h.handleError(new BadRequestException(i18n(
                "The request cannot be processed because it attempts to patch "
                        + "the read-only field '%s'", path)));
    Promise<List<Modification>, ResourceException> patch(final RequestState requestState, final JsonPointer path,
            final PatchOperation operation) {
        return Promises.<List<Modification>, ResourceException> newExceptionPromise(new BadRequestException(i18n(
                "The request cannot be processed because it attempts to patch the read-only field '%s'", path)));
    }
    @Override
    void read(final Context c, final JsonPointer path, final Entry e,
            final ResultHandler<JsonValue> h) {
        h.handleResult(value.copy());
    Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) {
        return Promises.newResultPromise(value.copy());
    }
    @Override
    void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
            final ResultHandler<List<Modification>> h) {
    Promise<List<Modification>, ResourceException> update(
            final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) {
        if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) {
            h.handleError(new BadRequestException(i18n(
                    "The request cannot be processed because it attempts to modify "
                            + "the read-only field '%s'", path)));
            return Promises.<List<Modification>, ResourceException> newExceptionPromise(new BadRequestException(i18n(
                    "The request cannot be processed because it attempts to modify the read-only field '%s'", path)));
        } else {
            h.handleResult(Collections.<Modification> emptyList());
            return Promises.newResultPromise(Collections.<Modification> emptyList());
        }
    }
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -15,6 +15,18 @@
 */
package org.forgerock.opendj.rest2ldap;
import static java.util.Arrays.asList;
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.ldap.Filter.alwaysTrue;
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.newSearchRequest;
import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.toFilter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -22,10 +34,13 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.http.Context;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.CollectionResourceProvider;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
@@ -33,27 +48,25 @@
import org.forgerock.json.resource.PatchOperation;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.PreconditionFailedException;
import org.forgerock.json.resource.QueryFilter;
import org.forgerock.json.resource.QueryFilterVisitor;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.QueryResult;
import org.forgerock.json.resource.QueryResultHandler;
import org.forgerock.json.resource.QueryResourceHandler;
import org.forgerock.json.resource.QueryResponse;
import org.forgerock.json.resource.ReadRequest;
import org.forgerock.json.resource.Resource;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.json.resource.ServerContext;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.Responses;
import org.forgerock.json.resource.UncategorizedException;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.DecodeOptions;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.SearchScope;
@@ -72,45 +85,21 @@
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
import org.forgerock.opendj.ldap.responses.SearchResultReference;
import org.forgerock.opendj.ldif.ChangeRecord;
import org.forgerock.util.promise.ExceptionHandler;
import org.forgerock.util.AsyncFunction;
import org.forgerock.util.Function;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.ExceptionHandler;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.PromiseImpl;
import org.forgerock.util.promise.Promises;
import static java.util.Arrays.*;
import static org.forgerock.opendj.ldap.Filter.*;
import static org.forgerock.opendj.ldap.requests.Requests.*;
import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.*;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
import static org.forgerock.opendj.rest2ldap.Utils.*;
import org.forgerock.util.promise.ResultHandler;
import org.forgerock.util.query.QueryFilter;
import org.forgerock.util.query.QueryFilterVisitor;
/**
 * A {@code CollectionResourceProvider} implementation which maps a JSON
 * resource collection to LDAP entries beneath a base DN.
 */
final class LDAPCollectionResourceProvider implements CollectionResourceProvider {
    private static class ResultHandlerFromPromise<T> implements ResultHandler<T> {
        private final PromiseImpl<T, ResourceException> promise;
        ResultHandlerFromPromise() {
            promise = PromiseImpl.create();
        }
        @Override
        public void handleError(ResourceException error) {
            promise.handleException(error);
        }
        @Override
        public void handleResult(T result) {
            promise.handleResult(result);
        }
    }
    /** Dummy exception used for signalling search success. */
    private static final ResourceException SUCCESS = new UncategorizedException(0, null, null);
@@ -136,144 +125,122 @@
    }
    @Override
    public void actionCollection(final ServerContext context, final ActionRequest request,
            final ResultHandler<JsonValue> handler) {
        handler.handleError(new NotSupportedException("Not yet implemented"));
    public Promise<ActionResponse, ResourceException> actionCollection(
            final Context context, final ActionRequest request) {
        return Promises.<ActionResponse, ResourceException> newExceptionPromise(
                                                            new NotSupportedException("Not yet implemented"));
    }
    @Override
    public void actionInstance(final ServerContext context, final String resourceId,
            final ActionRequest request, final ResultHandler<JsonValue> handler) {
        handler.handleError(new NotSupportedException("Not yet implemented"));
    public Promise<ActionResponse, ResourceException> actionInstance(
            final Context context, final String resourceId, final ActionRequest request) {
        return Promises.<ActionResponse, ResourceException> newExceptionPromise(
                                                            new NotSupportedException("Not yet implemented"));
    }
    @Override
    public void createInstance(final ServerContext context, final CreateRequest request,
            final ResultHandler<Resource> handler) {
        final Context c = wrap(context);
        final ResultHandler<Resource> h = wrap(c, handler);
    public Promise<ResourceResponse, ResourceException> createInstance(
            final Context context, final CreateRequest request) {
        final RequestState requestState = wrap(context);
        // Get the connection, then determine entry content, then perform add.
        c.run(h, new Runnable() {
        return requestState.getConnection().thenAsync(
            new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
            @Override
            public void run() {
                public Promise<ResourceResponse, ResourceException> apply(final Connection connection)
                        throws ResourceException {
                // Calculate entry content.
                attributeMapper.create(c, new JsonPointer(), request.getContent(),
                    new ResultHandler<List<Attribute>>() {
                    return attributeMapper.create(requestState, new JsonPointer(), request.getContent())
                            .thenAsync(new AsyncFunction<List<Attribute>, ResourceResponse, ResourceException>() {
                        @Override
                        public void handleError(final ResourceException error) {
                            h.handleError(error);
                        }
                        @Override
                        public void handleResult(final List<Attribute> result) {
                                public Promise<ResourceResponse, ResourceException> apply(
                                        final List<Attribute> attributes) {
                            // Perform add operation.
                            final AddRequest addRequest = newAddRequest(DN.rootDN());
                            for (final Attribute attribute : additionalLDAPAttributes) {
                                addRequest.addAttribute(attribute);
                            }
                            for (final Attribute attribute : result) {
                                    for (final Attribute attribute : attributes) {
                                addRequest.addAttribute(attribute);
                            }
                            try {
                                nameStrategy.setResourceId(c, getBaseDN(c), request.getNewResourceId(), addRequest);
                                        nameStrategy.setResourceId(requestState, getBaseDN(requestState),
                                                request.getNewResourceId(), addRequest);
                            } catch (final ResourceException e) {
                                h.handleError(e);
                                return;
                                        return Promises.newExceptionPromise(e);
                            }
                            if (config.readOnUpdatePolicy() == CONTROLS) {
                                final String[] attributes = getLDAPAttributes(c, request.getFields());
                                addRequest.addControl(PostReadRequestControl.newControl(false, attributes));
                                        addRequest.addControl(PostReadRequestControl.newControl(
                                                false, getLDAPAttributes(requestState, request.getFields())));
                            }
                            c.getConnection().applyChangeAsync(addRequest)
                                        .thenOnResult(postUpdateResultHandler(c, h))
                                        .thenOnException(postUpdateExceptionHandler(h));
                                    return connection.applyChangeAsync(addRequest)
                                                     .thenAsync(postUpdateResultAsyncFunction(requestState),
                                                                ldapExceptionToResourceException());
                        }
                    });
            }
        });
            }).thenFinally(close(requestState));
    }
    @Override
    public void deleteInstance(final ServerContext context, final String resourceId,
            final DeleteRequest request, final ResultHandler<Resource> handler) {
        final Context c = wrap(context);
        final ResultHandler<Resource> h = wrap(c, handler);
        // Get connection, search if needed, then delete.
        c.run(h, doUpdate(c, resourceId, request.getRevision(), new ResultHandler<DN>() {
    public Promise<ResourceResponse, ResourceException> deleteInstance(
            final Context context, final String resourceId, final DeleteRequest request) {
        final RequestState requestState = wrap(context);
        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
        return requestState.getConnection()
                .thenOnResult(saveConnection(connectionHolder))
                .thenAsync(doUpdateFunction(requestState, resourceId, request.getRevision()))
                .thenAsync(new AsyncFunction<DN, ResourceResponse, ResourceException>() {
            @Override
            public void handleError(final ResourceException error) {
                h.handleError(error);
            }
            @Override
            public void handleResult(final DN dn) {
                    public Promise<ResourceResponse, ResourceException> apply(DN dn) throws ResourceException {
                try {
                    final ChangeRecord deleteRequest = newDeleteRequest(dn);
                    if (config.readOnUpdatePolicy() == CONTROLS) {
                        final String[] attributes = getLDAPAttributes(c, request.getFields());
                                final String[] attributes = getLDAPAttributes(requestState, request.getFields());
                        deleteRequest.addControl(PreReadRequestControl.newControl(false, attributes));
                    }
                    if (config.useSubtreeDelete()) {
                        deleteRequest.addControl(SubtreeDeleteRequestControl.newControl(true));
                    }
                    addAssertionControl(deleteRequest, request.getRevision());
                    c.getConnection().applyChangeAsync(deleteRequest).thenOnResult(postUpdateResultHandler(c, h))
                                                                     .thenOnException(postUpdateExceptionHandler(h));
                            return connectionHolder.get().applyChangeAsync(deleteRequest)
                                                         .thenAsync(postUpdateResultAsyncFunction(requestState),
                                                                    ldapExceptionToResourceException());
                } catch (final Exception e) {
                    h.handleError(asResourceException(e));
                            return Promises.newExceptionPromise((asResourceException(e)));
                }
            }
        }));
                }).thenFinally(close(requestState));
    }
    @Override
    public void patchInstance(final ServerContext context, final String resourceId, final PatchRequest request,
            final ResultHandler<Resource> handler) {
        final Context c = wrap(context);
        final ResultHandler<Resource> h = wrap(c, handler);
    public Promise<ResourceResponse, ResourceException> patchInstance(
            final Context context, final String resourceId, final PatchRequest request) {
        final RequestState requestState = wrap(context);
        if (request.getPatchOperations().isEmpty()) {
            /*
             * This patch is a no-op so just read the entry and check its version.
             */
            c.run(h, new Runnable() {
                @Override
                public void run() {
                    final String[] attributes = getLDAPAttributes(c, request.getFields());
                    final SearchRequest searchRequest = nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId)
                            .addAttribute(attributes);
                    c.getConnection().searchSingleEntryAsync(searchRequest)
                            .thenOnResult(postEmptyPatchResultHandler(c, request, h))
                            .thenOnException(postEmptyPatchExceptionHandler(h));
                }
            });
        } else {
            /*
             * Get the connection, search if needed, then determine modifications, then perform modify.
             */
            c.run(h, doUpdate(c, resourceId, request.getRevision(), new ResultHandler<DN>() {
                @Override
                public void handleError(final ResourceException error) {
                    h.handleError(error);
            return emptyPatchInstance(requestState, resourceId, request);
                }
        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
        return requestState.getConnection()
                .thenOnResult(saveConnection(connectionHolder))
                .thenAsync(doUpdateFunction(requestState, resourceId, request.getRevision()))
                .thenAsync(new AsyncFunction<DN, ResourceResponse, ResourceException>() {
                @Override
                public void handleResult(final DN dn) {
                    public Promise<ResourceResponse, ResourceException> apply(final DN dn) throws ResourceException {
                    // Convert the patch operations to LDAP modifications.
                    List<Promise<List<Modification>, ResourceException>> promises =
                            new ArrayList<>(request.getPatchOperations().size());
                    for (final PatchOperation operation : request.getPatchOperations()) {
                        final ResultHandlerFromPromise<List<Modification>> handler = new ResultHandlerFromPromise<>();
                        attributeMapper.patch(c, new JsonPointer(), operation, handler);
                        promises.add(handler.promise);
                            promises.add(attributeMapper.patch(requestState, new JsonPointer(), operation));
                    }
                    Promises.when(promises).thenOnResult(
                        new org.forgerock.util.promise.ResultHandler<List<List<Modification>>>() {
                        return Promises.when(promises).thenAsync(
                                new AsyncFunction<List<List<Modification>>, ResourceResponse, ResourceException>() {
                            @Override
                            public void handleResult(final List<List<Modification>> result) {
                                    public Promise<ResourceResponse, ResourceException> apply(
                                            final List<List<Modification>> result) {
                                // The patch operations have been converted successfully.
                                try {
                                    final ModifyRequest modifyRequest = newModifyRequest(dn);
@@ -285,14 +252,14 @@
                                        }
                                    }
                                    final List<String> attributes = asList(getLDAPAttributes(c, request.getFields()));
                                            final List<String> attributes =
                                                    asList(getLDAPAttributes(requestState, request.getFields()));
                                    if (modifyRequest.getModifications().isEmpty()) {
                                        /*
                                         * This patch is a no-op so just read the entry and check its version.
                                         */
                                        c.getConnection().readEntryAsync(dn, attributes)
                                                .thenOnResult(postEmptyPatchResultHandler(c, request, h))
                                                .thenOnException(postEmptyPatchExceptionHandler(h));
                                                // This patch is a no-op so just read the entry and check its version.
                                                return connectionHolder.get()
                                                        .readEntryAsync(dn, attributes)
                                                        .thenAsync(postEmptyPatchAsyncFunction(requestState, request),
                                                                   ldapExceptionToResourceException());
                                    } else {
                                        // Add controls and perform the modify request.
                                        if (config.readOnUpdatePolicy() == CONTROLS) {
@@ -300,46 +267,247 @@
                                                PostReadRequestControl.newControl(false, attributes));
                                        }
                                        if (config.usePermissiveModify()) {
                                            modifyRequest.addControl(PermissiveModifyRequestControl.newControl(true));
                                                    modifyRequest.addControl(
                                                            PermissiveModifyRequestControl.newControl(true));
                                        }
                                        addAssertionControl(modifyRequest, request.getRevision());
                                        c.getConnection().applyChangeAsync(modifyRequest)
                                                .thenOnResult(postUpdateResultHandler(c, h))
                                                .thenOnException(postUpdateExceptionHandler(h));
                                                return connectionHolder.get()
                                                        .applyChangeAsync(modifyRequest)
                                                        .thenAsync(postUpdateResultAsyncFunction(requestState),
                                                                   ldapExceptionToResourceException());
                                    }
                                } catch (final Exception e) {
                                    h.handleError(asResourceException(e));
                                            return Promises.newExceptionPromise(asResourceException(e));
                                }
                            }
                        }).thenOnException(new ExceptionHandler<ResourceException>() {
                            @Override
                            public void handleException(ResourceException exception) {
                                h.handleError(asResourceException(exception));
                            }
                        });
                }
            }));
                }).thenFinally(close(requestState));
        }
    /** Just read the entry and check its version. */
    private Promise<ResourceResponse, ResourceException> emptyPatchInstance(
            final RequestState requestState, final String resourceId, final PatchRequest request) {
        return requestState.getConnection()
                .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(final Connection connection)
                            throws ResourceException {
                        final String[] attributes = getLDAPAttributes(requestState, request.getFields());
                        final SearchRequest searchRequest =
                                nameStrategy.createSearchRequest(requestState, getBaseDN(requestState), resourceId)
                                            .addAttribute(attributes);
                        return connection.searchSingleEntryAsync(searchRequest)
                                         .thenAsync(postEmptyPatchAsyncFunction(requestState, request),
                                                    ldapExceptionToResourceException());
                    }
                });
    }
    private AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException> postEmptyPatchAsyncFunction(
            final RequestState requestState, final PatchRequest request) {
        return new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
            @Override
            public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry)
                    throws ResourceException {
                try {
                    // Fail if there is a version mismatch.
                    ensureMVCCVersionMatches(entry, request.getRevision());
                    return adaptEntry(requestState, entry);
                } catch (final Exception e) {
                    return Promises.newExceptionPromise(asResourceException(e));
                }
            }
        };
    }
    @Override
    public void queryCollection(final ServerContext context, final QueryRequest request,
            final QueryResultHandler handler) {
        final Context c = wrap(context);
        final QueryResultHandler h = wrap(c, handler);
    public Promise<QueryResponse, ResourceException> queryCollection(
            final Context context, final QueryRequest request, final QueryResourceHandler resourceHandler) {
        final RequestState requestState = wrap(context);
        /*
         * Get the connection, then calculate the search filter, then perform the search.
         */
        c.run(h, new Runnable() {
        return requestState.getConnection()
                .thenAsync(new AsyncFunction<Connection, QueryResponse, ResourceException>() {
            @Override
            public void run() {
                    public Promise<QueryResponse, ResourceException> apply(final Connection connection)
                            throws ResourceException {
                // Calculate the filter (this may require the connection).
                getLDAPFilter(c, request.getQueryFilter(), new ResultHandler<Filter>() {
                        return getLDAPFilter(requestState, request.getQueryFilter())
                                            .thenAsync(runQuery(request, resourceHandler, requestState, connection));
                    }
                })
                .thenFinally(close(requestState));
    }
    private Promise<Filter, ResourceException> getLDAPFilter(
            final RequestState requestState, final QueryFilter<JsonPointer> queryFilter) {
        final QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer> visitor =
                new QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer>() {
                    @Override
                    public Promise<Filter, ResourceException> visitAndFilter(final Void unused,
                            final List<QueryFilter<JsonPointer>> subFilters) {
                        final List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size());
                        for (final QueryFilter<JsonPointer> subFilter : subFilters) {
                            promises.add(subFilter.accept(this, unused));
                        }
                        return Promises.when(promises).then(new Function<List<Filter>, Filter, ResourceException>() {
                            @Override
                            public Filter apply(final List<Filter> value) {
                                // Check for unmapped filter components and optimize.
                                final Iterator<Filter> i = value.iterator();
                                while (i.hasNext()) {
                                    final Filter f = i.next();
                                    if (f == alwaysFalse()) {
                                        return alwaysFalse();
                                    } else if (f == alwaysTrue()) {
                                        i.remove();
                                    }
                                }
                                switch (value.size()) {
                                case 0:
                                    return alwaysTrue();
                                case 1:
                                    return value.get(0);
                                default:
                                    return Filter.and(value);
                                }
                            }
                        });
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitBooleanLiteralFilter(
                            final Void unused, final boolean value) {
                        return Promises.newResultPromise(toFilter(value));
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitContainsFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(
                                requestState, new JsonPointer(), field, FilterType.CONTAINS, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitEqualsFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(
                                requestState, new JsonPointer(), field, FilterType.EQUAL_TO, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitExtendedMatchFilter(final Void unused,
                            final JsonPointer field, final String operator, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(
                                requestState, new JsonPointer(), field, FilterType.EXTENDED, operator, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitGreaterThanFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(
                                requestState, new JsonPointer(), field, FilterType.GREATER_THAN, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitGreaterThanOrEqualToFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(requestState, new JsonPointer(), field,
                                FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitLessThanFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(
                                requestState, new JsonPointer(), field, FilterType.LESS_THAN, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitLessThanOrEqualToFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(requestState, new JsonPointer(), field,
                                FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitNotFilter(
                            final Void unused, final QueryFilter<JsonPointer> subFilter) {
                        return subFilter.accept(this, unused).then(new Function<Filter, Filter, ResourceException>() {
                            @Override
                            public Filter apply(final Filter value) {
                                if (value == null || value == alwaysFalse()) {
                                    return alwaysTrue();
                                } else if (value == alwaysTrue()) {
                                    return alwaysFalse();
                                } else {
                                    return Filter.not(value);
                                }
                            }
                        });
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitOrFilter(final Void unused,
                            final List<QueryFilter<JsonPointer>> subFilters) {
                        final List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size());
                        for (final QueryFilter<JsonPointer> subFilter : subFilters) {
                            promises.add(subFilter.accept(this, unused));
                        }
                        return Promises.when(promises).then(new Function<List<Filter>, Filter, ResourceException>() {
                            @Override
                            public Filter apply(final List<Filter> value) {
                                // Check for unmapped filter components and optimize.
                                final Iterator<Filter> i = value.iterator();
                                while (i.hasNext()) {
                                    final Filter f = i.next();
                                    if (f == alwaysFalse()) {
                                        i.remove();
                                    } else if (f == alwaysTrue()) {
                                        return alwaysTrue();
                                    }
                                }
                                switch (value.size()) {
                                case 0:
                                    return alwaysFalse();
                                case 1:
                                    return value.get(0);
                                default:
                                    return Filter.or(value);
                                }
                            }
                        });
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitPresentFilter(
                            final Void unused, final JsonPointer field) {
                        return attributeMapper.getLDAPFilter(
                                requestState, new JsonPointer(), field, FilterType.PRESENT, null, null);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitStartsWithFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(
                                requestState, new JsonPointer(), field, FilterType.STARTS_WITH, null, valueAssertion);
                    }
                };
        // Note that the returned LDAP filter may be null if it could not be mapped by any attribute mappers.
        return queryFilter.accept(visitor, null);
    }
    private AsyncFunction<Filter, QueryResponse, ResourceException> runQuery(final QueryRequest request,
            final QueryResourceHandler resourceHandler, final RequestState requestState, final Connection connection) {
        return new AsyncFunction<Filter, QueryResponse, ResourceException>() {
                    /**
                     * The following fields are guarded by sequenceLock. In
                     * addition, the sequenceLock ensures that we send one JSON
                     * resource at a time back to the client.
             * The following fields are guarded by sequenceLock. In addition,
             * the sequenceLock ensures that we send one JSON resource at a time
             * back to the client.
                     */
                    private final Object sequenceLock = new Object();
                    private String cookie;
@@ -349,29 +517,21 @@
                    private int totalResourceCount;
                    @Override
                    public void handleError(final ResourceException error) {
                        h.handleError(error);
                    }
                    @Override
                    public void handleResult(final Filter ldapFilter) {
                        /*
                         * Avoid performing a search if the filter could not be mapped or if it will never match.
                         */
            public Promise<QueryResponse, ResourceException> apply(final Filter ldapFilter) {
                        if (ldapFilter == null || ldapFilter == alwaysFalse()) {
                            h.handleResult(new QueryResult());
                        } else {
                    // Avoid performing a search if the filter could not be mapped or if it will never match.
                    return Promises.newResultPromise(Responses.newQueryResponse());
                }
                final PromiseImpl<QueryResponse, ResourceException> promise = PromiseImpl.create();
                            // Perform the search.
                            final String[] attributes = getLDAPAttributes(c, request.getFields());
                            final Filter searchFilter =
                                    ldapFilter == Filter.alwaysTrue() ? Filter.objectClassPresent() : ldapFilter;
                            final SearchRequest searchRequest =
                                    newSearchRequest(getBaseDN(c), SearchScope.SINGLE_LEVEL, searchFilter, attributes);
                final String[] attributes = getLDAPAttributes(requestState, request.getFields());
                final Filter searchFilter = ldapFilter == Filter.alwaysTrue() ? Filter.objectClassPresent()
                                                                              : ldapFilter;
                final SearchRequest searchRequest = newSearchRequest(
                        getBaseDN(requestState), SearchScope.SINGLE_LEVEL, searchFilter, attributes);
                            /*
                             * Add the page results control. We can support the page offset by
                             * reading the next offset pages, or offset x page size resources.
                             */
                // Add the page results control. We can support the page offset by
                // reading the next offset pages, or offset x page size resources.
                            final int pageResultStartIndex;
                            final int pageSize = request.getPageSize();
                            if (request.getPageSize() > 0) {
@@ -383,10 +543,8 @@
                                    pageResultStartIndex = 0;
                                    pageResultEndIndex = pageSize;
                                }
                                final ByteString cookie =
                                        request.getPagedResultsCookie() != null ? ByteString
                                                .valueOfBase64(request.getPagedResultsCookie())
                                                : ByteString.empty();
                    final ByteString cookie = request.getPagedResultsCookie() != null
                            ? ByteString.valueOfBase64(request.getPagedResultsCookie()) : ByteString.empty();
                                final SimplePagedResultsControl control =
                                        SimplePagedResultsControl.newControl(true, pageResultEndIndex, cookie);
                                searchRequest.addControl(control);
@@ -394,13 +552,11 @@
                                pageResultStartIndex = 0;
                            }
                            c.getConnection().searchAsync(searchRequest, new SearchResultHandler() {
                connection.searchAsync(searchRequest, new SearchResultHandler() {
                                @Override
                                public boolean handleEntry(final SearchResultEntry entry) {
                                    /*
                                     * Search result entries will be returned before the search result/error so the
                                     * only reason pendingResult will be non-null is if a mapping error has occurred.
                                     */
                        // Search result entries will be returned before the search result/error so the only reason
                        // pendingResult will be non-null is if a mapping error has occurred.
                                    synchronized (sequenceLock) {
                                        if (pendingResult != null) {
                                            return false;
@@ -413,37 +569,38 @@
                                    }
                                    /*
                                     * FIXME: secondary asynchronous searches will complete in a non-deterministic
                                     * order and may cause the JSON resources to be returned in a different order to the
                                     * order in which the primary LDAP search results were received. This is benign at
                                     * the moment, but will need resolving when we implement server side sorting. A
                                     * possible fix will be to use a queue of pending resources (promises?). However,
                                     * the queue cannot be unbounded in case it grows very large, but it cannot be
                                     * bounded either since that could cause a deadlock between rest2ldap and the LDAP
                                     * server (imagine the case where the server has a single worker thread which is
                                     * occupied processing the primary search).
                                     * The best solution is probably to process the primary search results in batches
                                     * using the paged results control.
                         * FIXME: secondary asynchronous searches will complete in a non-deterministic order and
                         * may cause the JSON resources to be returned in a different order to the order in which
                         * the primary LDAP search results were received. This is benign at the moment, but will
                         * need resolving when we implement server side sorting. A possible fix will be to use a
                         * queue of pending resources (promises?). However, the queue cannot be unbounded in case
                         * it grows very large, but it cannot be bounded either since that could cause a deadlock
                         * between rest2ldap and the LDAP server (imagine the case where the server has a single
                         * worker thread which is occupied processing the primary search).
                         * The best solution is probably to process the primary search results in batches using
                         * the paged results control.
                                     */
                                    final String id = nameStrategy.getResourceId(c, entry);
                        final String id = nameStrategy.getResourceId(requestState, entry);
                                    final String revision = getRevisionFromEntry(entry);
                                    attributeMapper.read(c, new JsonPointer(), entry, new ResultHandler<JsonValue>() {
                                        @Override
                                        public void handleError(final ResourceException e) {
                                            synchronized (sequenceLock) {
                                                pendingResourceCount--;
                                                completeIfNecessary(e);
                                            }
                                        }
                        attributeMapper.read(requestState, new JsonPointer(), entry)
                                       .thenOnResult(new ResultHandler<JsonValue>() {
                                        @Override
                                        public void handleResult(final JsonValue result) {
                                            synchronized (sequenceLock) {
                                                pendingResourceCount--;
                                                if (!resultSent) {
                                                    h.handleResource(new Resource(id, revision, result));
                                                       resourceHandler.handleResource(
                                                               Responses.newResourceResponse(id, revision, result));
                                                }
                                                completeIfNecessary();
                                                   completeIfNecessary(promise);
                                               }
                                           }
                                       }).thenOnException(new ExceptionHandler<ResourceException>() {
                                           @Override
                                           public void handleException(ResourceException exception) {
                                               synchronized (sequenceLock) {
                                                   pendingResourceCount--;
                                                   completeIfNecessary(exception, promise);
                                            }
                                        }
                                    });
@@ -452,20 +609,19 @@
                                @Override
                                public boolean handleReference(final SearchResultReference reference) {
                                    // TODO: should this be classed as an error since rest2ldap
                                    // assumes entries are all colocated?
                        // TODO: should this be classed as an error since
                        // rest2ldap assumes entries are all colocated?
                                    return true;
                                }
                            }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Result>() {
                }).thenOnResult(new ResultHandler<Result>() {
                                @Override
                                public void handleResult(Result result) {
                                    synchronized (sequenceLock) {
                                        if (request.getPageSize() > 0) {
                                            try {
                                                final SimplePagedResultsControl control =
                                                    result.getControl(SimplePagedResultsControl.DECODER,
                                                        DECODE_OPTIONS);
                                            result.getControl(SimplePagedResultsControl.DECODER, DECODE_OPTIONS);
                                                if (control != null && !control.getCookie().isEmpty()) {
                                                    cookie = control.getCookie().toBase64String();
                                                }
@@ -473,108 +629,99 @@
                                                // FIXME: need some logging.
                                            }
                                        }
                                        completeIfNecessary(SUCCESS);
                            completeIfNecessary(SUCCESS, promise);
                                    }
                                }
                            }).thenOnException(new ExceptionHandler<LdapException>() {
                                @Override
                                public void handleException(LdapException exception) {
                                    synchronized (sequenceLock) {
                                        completeIfNecessary(asResourceException(exception));
                            completeIfNecessary(asResourceException(exception), promise);
                                    }
                                }
                            });
                        }
                return promise;
                    }
                    /**
                     * This method must be invoked with the sequenceLock held.
                     */
                    private void completeIfNecessary(final ResourceException e) {
            /** This method must be invoked with the sequenceLock held. */
            private void completeIfNecessary(
                    final ResourceException e, final PromiseImpl<QueryResponse, ResourceException> handler) {
                        if (pendingResult == null) {
                            pendingResult = e;
                        }
                        completeIfNecessary();
                completeIfNecessary(handler);
                    }
                    /**
                     * Close out the query result set if there are no more
                     * pending resources and the LDAP result has been received.
             * Close out the query result set if there are no more pending
             * resources and the LDAP result has been received.
                     * This method must be invoked with the sequenceLock held.
                     */
                    private void completeIfNecessary() {
            private void completeIfNecessary(final PromiseImpl<QueryResponse, ResourceException> handler) {
                        if (pendingResourceCount == 0 && pendingResult != null && !resultSent) {
                            if (pendingResult == SUCCESS) {
                                h.handleResult(new QueryResult(cookie, -1));
                        handler.handleResult(Responses.newQueryResponse(cookie));
                            } else {
                                h.handleError(pendingResult);
                        handler.handleException(pendingResult);
                            }
                            resultSent = true;
                        }
                    }
                });
            }
        });
    }
    @Override
    public void readInstance(final ServerContext context, final String resourceId, final ReadRequest request,
        final ResultHandler<Resource> handler) {
        final Context c = wrap(context);
        final ResultHandler<Resource> h = wrap(c, handler);
        // Get connection then perform the search.
        c.run(h, new Runnable() {
            @Override
            public void run() {
                // Do the search.
                final String[] attributes = getLDAPAttributes(c, request.getFields());
                final SearchRequest request =
                    nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute(attributes);
                c.getConnection().searchSingleEntryAsync(request).thenOnResult(
                    new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() {
                        @Override
                        public void handleResult(final SearchResultEntry entry) {
                            adaptEntry(c, entry, h);
                        }
                    }).thenOnException(new ExceptionHandler<LdapException>() {
                        @Override
                        public void handleException(final LdapException exception) {
                            h.handleError(asResourceException(exception));
                        }
                    });
            };
        });
    }
    @Override
    public void updateInstance(final ServerContext context, final String resourceId, final UpdateRequest request,
            final ResultHandler<Resource> handler) {
        /*
         * Update operations are a bit awkward because there is no direct
         * mapping to LDAP. We need to convert the update request into an LDAP
         * modify operation which means reading the current LDAP entry,
         * generating the new entry content, then comparing the two in order to
         * obtain a set of changes. We also need to handle read-only fields
         * correctly: if a read-only field is included with the new resource
         * then it must match exactly the value of the existing field.
         */
        final Context c = wrap(context);
        final ResultHandler<Resource> h = wrap(c, handler);
    public Promise<ResourceResponse, ResourceException> readInstance(
            final Context context, final String resourceId, final ReadRequest request) {
        final RequestState requestState = wrap(context);
        // Get connection then, search for the existing entry, then modify.
        c.run(h, new Runnable() {
        return requestState.getConnection()
                .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
            @Override
            public void run() {
                final String[] attributes = getLDAPAttributes(c, Collections.<JsonPointer> emptyList());
                final SearchRequest searchRequest = nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId)
                    public Promise<ResourceResponse, ResourceException> apply(Connection connection)
                            throws ResourceException {
                        // Do the search.
                        final String[] attributes = getLDAPAttributes(requestState, request.getFields());
                        final SearchRequest request =
                                nameStrategy.createSearchRequest(requestState, getBaseDN(requestState), resourceId)
                        .addAttribute(attributes);
                c.getConnection().searchSingleEntryAsync(searchRequest)
                        .thenOnResult(new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() {
                        return connection.searchSingleEntryAsync(request)
                                    .thenAsync(
                                        new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
                                            public Promise<ResourceResponse, ResourceException> apply(
                                                    SearchResultEntry entry) throws ResourceException {
                                                return adaptEntry(requestState, entry);
                                            }
                                        },
                                        ldapExceptionToResourceException());
                    }
                })
                .thenFinally(close(requestState));
    }
                            @Override
                            public void handleResult(final SearchResultEntry entry) {
    public Promise<ResourceResponse, ResourceException> updateInstance(
            final Context context, final String resourceId, final UpdateRequest request) {
        final RequestState requestState = wrap(context);
        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
        return requestState.getConnection().thenOnResult(saveConnection(connectionHolder))
                .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(final Connection connection)
                            throws ResourceException {
                        final String[] attributes = getLDAPAttributes(
                                requestState, Collections.<JsonPointer> emptyList());
                        final SearchRequest searchRequest = nameStrategy.createSearchRequest(
                                requestState, getBaseDN(requestState), resourceId).addAttribute(attributes);
                        return connection.searchSingleEntryAsync(searchRequest)
                                .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
                                    @Override
                                    public Promise<ResourceResponse, ResourceException> apply(
                                            final SearchResultEntry entry) {
                                try {
                                    // Fail-fast if there is a version mismatch.
                                    ensureMVCCVersionMatches(entry, request.getRevision());
@@ -582,65 +729,59 @@
                                    // Create the modify request.
                                    final ModifyRequest modifyRequest = newModifyRequest(entry.getName());
                                    if (config.readOnUpdatePolicy() == CONTROLS) {
                                        final String[] attributes = getLDAPAttributes(c, request.getFields());
                                        modifyRequest.addControl(PostReadRequestControl.newControl(false, attributes));
                                                final String[] attributes =
                                                        getLDAPAttributes(requestState, request.getFields());
                                                modifyRequest.addControl(
                                                        PostReadRequestControl.newControl(false, attributes));
                                    }
                                    if (config.usePermissiveModify()) {
                                        modifyRequest.addControl(PermissiveModifyRequestControl.newControl(true));
                                                modifyRequest.addControl(
                                                        PermissiveModifyRequestControl.newControl(true));
                                    }
                                    addAssertionControl(modifyRequest, request.getRevision());
                                    /*
                                     * Determine the set of changes that need to be performed.
                                     */
                                    attributeMapper.update(c, new JsonPointer(), entry, request.getNewContent(),
                                            new ResultHandler<List<Modification>>() {
                                            // Determine the set of changes that need to be performed.
                                            return attributeMapper.update(
                                                        requestState, new JsonPointer(), entry, request.getContent())
                                                    .thenAsync(new AsyncFunction<
                                                            List<Modification>, ResourceResponse, ResourceException>() {
                                                @Override
                                                public void handleError(final ResourceException error) {
                                                    h.handleError(error);
                                                        public Promise<ResourceResponse, ResourceException> apply(
                                                                List<Modification> modifications)
                                                                throws ResourceException {
                                                            if (modifications.isEmpty()) {
                                                                // No changes to be performed so just return
                                                                // the entry that we read.
                                                                return adaptEntry(requestState, entry);
                                                }
                                                @Override
                                                public void handleResult(final List<Modification> result) {
                                                    // Perform the modify operation.
                                                    if (result.isEmpty()) {
                                                        /*
                                                         * No changes to be performed, so just return
                                                         * the entry that we read.
                                                         */
                                                        adaptEntry(c, entry, h);
                                                    } else {
                                                        modifyRequest.getModifications().addAll(result);
                                                        c.getConnection().applyChangeAsync(modifyRequest)
                                                                .thenOnResult(postUpdateResultHandler(c, h))
                                                                .thenOnException(postUpdateExceptionHandler(h));
                                                    }
                                                            modifyRequest.getModifications().addAll(modifications);
                                                            return connection.applyChangeAsync(modifyRequest).thenAsync(
                                                                    postUpdateResultAsyncFunction(requestState),
                                                                    ldapExceptionToResourceException());
                                                }
                                            });
                                } catch (final Exception e) {
                                    h.handleError(asResourceException(e));
                                            return Promises.newExceptionPromise(asResourceException(e));
                                }
                            }
                        }).thenOnException(new ExceptionHandler<LdapException>() {
                            @Override
                            public void handleException(final LdapException exception) {
                                h.handleError(asResourceException(exception));
                                }, ldapExceptionToResourceException());
                            }
                        });
            }
        });
                }).thenFinally(close(requestState));
    }
    private void adaptEntry(final Context c, final Entry entry, final ResultHandler<Resource> handler) {
        final String actualResourceId = nameStrategy.getResourceId(c, entry);
    private Promise<ResourceResponse, ResourceException> adaptEntry(
            final RequestState requestState, final Entry entry) {
        final String actualResourceId = nameStrategy.getResourceId(requestState, entry);
        final String revision = getRevisionFromEntry(entry);
        attributeMapper.read(c, new JsonPointer(), entry, transform(
                new Function<JsonValue, Resource, NeverThrowsException>() {
        return attributeMapper.read(requestState, new JsonPointer(), entry)
                              .then(new Function<JsonValue, ResourceResponse, ResourceException>() {
                    @Override
                    public Resource apply(final JsonValue value) {
                        return new Resource(actualResourceId, revision, new JsonValue(value));
                                  public ResourceResponse apply(final JsonValue value) {
                                      return Responses.newResourceResponse(
                                              actualResourceId, revision, new JsonValue(value));
                    }
                }, handler));
                              });
    }
    private void addAssertionControl(final ChangeRecord request, final String expectedRevision)
@@ -652,39 +793,39 @@
        }
    }
    private Runnable doUpdate(final Context c, final String resourceId, final String revision,
            final ResultHandler<DN> updateHandler) {
        return new Runnable() {
    private AsyncFunction<Connection, DN, ResourceException> doUpdateFunction(
            final RequestState requestState, final String resourceId, final String revision) {
        return new AsyncFunction<Connection, DN, ResourceException>() {
            @Override
            public void run() {
            public Promise<DN, ResourceException> apply(Connection connection) {
                final String ldapAttribute =
                        (etagAttribute != null && revision != null) ? etagAttribute.toString()
                                : "1.1";
                        (etagAttribute != null && revision != null) ? etagAttribute.toString() : "1.1";
                final SearchRequest searchRequest =
                        nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute(
                                ldapAttribute);
                        nameStrategy.createSearchRequest(requestState, getBaseDN(requestState), resourceId)
                                    .addAttribute(ldapAttribute);
                if (searchRequest.getScope().equals(SearchScope.BASE_OBJECT)) {
                    // There's no point in doing a search because we already know the DN.
                    updateHandler.handleResult(searchRequest.getName());
                    return Promises.newResultPromise(searchRequest.getName());
                } else {
                    c.getConnection().searchSingleEntryAsync(searchRequest)
                            .thenOnResult(new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() {
                    return connection.searchSingleEntryAsync(searchRequest)
                            .thenAsync(new AsyncFunction<SearchResultEntry, DN, ResourceException>() {
                                @Override
                                public void handleResult(final SearchResultEntry entry) {
                                public Promise<DN, ResourceException> apply(SearchResultEntry entry)
                                        throws ResourceException {
                                    try {
                                        // Fail-fast if there is a version mismatch.
                                        ensureMVCCVersionMatches(entry, revision);
                                        // Perform update operation.
                                        updateHandler.handleResult(entry.getName());
                                        return Promises.newResultPromise(entry.getName());
                                    } catch (final Exception e) {
                                        updateHandler.handleError(asResourceException(e));
                                        return Promises.newExceptionPromise(asResourceException(e));
                                    }
                                }
                            }).thenOnException(new ExceptionHandler<LdapException>() {
                            }, new AsyncFunction<LdapException, DN, ResourceException>() {
                                @Override
                                public void handleException(final LdapException exception) {
                                    updateHandler.handleError(asResourceException(exception));
                                public Promise<DN, ResourceException> apply(LdapException ldapException)
                                        throws ResourceException {
                                    return Promises.newExceptionPromise(asResourceException(ldapException));
                                }
                            });
                }
@@ -715,7 +856,7 @@
        }
    }
    private DN getBaseDN(final Context context) {
    private DN getBaseDN(final RequestState requestState) {
        return baseDN;
    }
@@ -723,274 +864,49 @@
     * Determines the set of LDAP attributes to request in an LDAP read (search,
     * post-read), based on the provided list of JSON pointers.
     *
     * @param requestState
     *          The request state.
     * @param requestedAttributes
     *            The list of resource attributes to be read.
     * @return The set of LDAP attributes associated with the resource
     *         attributes.
     */
    private String[] getLDAPAttributes(final Context c,
            final Collection<JsonPointer> requestedAttributes) {
    private String[] getLDAPAttributes(
            final RequestState requestState, final Collection<JsonPointer> requestedAttributes) {
        // Get all the LDAP attributes required by the attribute mappers.
        final Set<String> requestedLDAPAttributes;
        if (requestedAttributes.isEmpty()) {
            // Full read.
            requestedLDAPAttributes = new LinkedHashSet<>();
            attributeMapper.getLDAPAttributes(c, new JsonPointer(), new JsonPointer(),
            attributeMapper.getLDAPAttributes(requestState, new JsonPointer(), new JsonPointer(),
                    requestedLDAPAttributes);
        } else {
            // Partial read.
            requestedLDAPAttributes = new LinkedHashSet<>(requestedAttributes.size());
            for (final JsonPointer requestedAttribute : requestedAttributes) {
                attributeMapper.getLDAPAttributes(c, new JsonPointer(), requestedAttribute,
                attributeMapper.getLDAPAttributes(requestState, new JsonPointer(), requestedAttribute,
                        requestedLDAPAttributes);
            }
        }
        // Get the LDAP attributes required by the Etag and name stategies.
        nameStrategy.getLDAPAttributes(c, requestedLDAPAttributes);
        nameStrategy.getLDAPAttributes(requestState, requestedLDAPAttributes);
        if (etagAttribute != null) {
            requestedLDAPAttributes.add(etagAttribute.toString());
        }
        return requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]);
    }
    private void getLDAPFilter(final Context c, final QueryFilter queryFilter, final ResultHandler<Filter> h) {
        final QueryFilterVisitor<Void, ResultHandler<Filter>> visitor =
                new QueryFilterVisitor<Void, ResultHandler<Filter>>() {
                    @Override
                    public Void visitAndFilter(final ResultHandler<Filter> p, final List<QueryFilter> subFilters) {
                        List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size());
                        for (final QueryFilter subFilter : subFilters) {
                            final ResultHandlerFromPromise<Filter> handler = new ResultHandlerFromPromise<>();
                            subFilter.accept(this, handler);
                            promises.add(handler.promise);
                        }
                        Promises.when(promises)
                                .then(new org.forgerock.util.Function<List<Filter>, Filter, ResourceException>() {
                                    @Override
                                    public Filter apply(final List<Filter> value) {
                                        // Check for unmapped filter components and optimize.
                                        final Iterator<Filter> i = value.iterator();
                                        while (i.hasNext()) {
                                            final Filter f = i.next();
                                            if (f == alwaysFalse()) {
                                                return alwaysFalse();
                                            } else if (f == alwaysTrue()) {
                                                i.remove();
                                            }
                                        }
                                        switch (value.size()) {
                                        case 0:
                                            return alwaysTrue();
                                        case 1:
                                            return value.get(0);
                                        default:
                                            return Filter.and(value);
                                        }
                                    }
                                }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Filter>() {
                                    @Override
                                    public void handleResult(Filter result) {
                                        p.handleResult(result);
                                    }
                                }).thenOnException(new ExceptionHandler<ResourceException>() {
                                    @Override
                                    public void handleException(ResourceException exception) {
                                        p.handleError(exception);
                                    }
                                });
                        return null;
                    }
                    @Override
                    public Void visitBooleanLiteralFilter(final ResultHandler<Filter> p,
                            final boolean value) {
                        p.handleResult(toFilter(value));
                        return null;
                    }
                    @Override
                    public Void visitContainsFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.CONTAINS, null, valueAssertion, p);
                        return null;
                    }
                    @Override
                    public Void visitEqualsFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.EQUAL_TO, null, valueAssertion, p);
                        return null;
                    }
                    @Override
                    public Void visitExtendedMatchFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final String operator,
                            final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.EXTENDED, operator, valueAssertion, p);
                        return null;
                    }
                    @Override
                    public Void visitGreaterThanFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.GREATER_THAN, null, valueAssertion, p);
                        return null;
                    }
                    @Override
                    public Void visitGreaterThanOrEqualToFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion, p);
                        return null;
                    }
                    @Override
                    public Void visitLessThanFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.LESS_THAN, null, valueAssertion, p);
                        return null;
                    }
                    @Override
                    public Void visitLessThanOrEqualToFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion, p);
                        return null;
                    }
                    @Override
                    public Void visitNotFilter(final ResultHandler<Filter> p,
                            final QueryFilter subFilter) {
                        subFilter.accept(this, transform(new Function<Filter, Filter, NeverThrowsException>() {
                            @Override
                            public Filter apply(final Filter value) {
                                if (value == null || value == alwaysFalse()) {
                                    return alwaysTrue();
                                } else if (value == alwaysTrue()) {
                                    return alwaysFalse();
                                } else {
                                    return Filter.not(value);
                                }
                            }
                        }, p));
                        return null;
                    }
                    @Override
                    public Void visitOrFilter(final ResultHandler<Filter> p, final List<QueryFilter> subFilters) {
                        List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size());
                        for (final QueryFilter subFilter : subFilters) {
                            final ResultHandlerFromPromise<Filter> handler = new ResultHandlerFromPromise<>();
                            subFilter.accept(this, handler);
                            promises.add(handler.promise);
                        }
                        Promises.when(promises)
                                .then(new org.forgerock.util.Function<List<Filter>, Filter, ResourceException>() {
                                    @Override
                                    public Filter apply(final List<Filter> value) {
                                        // Check for unmapped filter components and optimize.
                                        final Iterator<Filter> i = value.iterator();
                                        while (i.hasNext()) {
                                            final Filter f = i.next();
                                            if (f == alwaysFalse()) {
                                                i.remove();
                                            } else if (f == alwaysTrue()) {
                                                return alwaysTrue();
                                            }
                                        }
                                        switch (value.size()) {
                                        case 0:
                                            return alwaysFalse();
                                        case 1:
                                            return value.get(0);
                                        default:
                                            return Filter.or(value);
                                        }
                                    }
                                }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Filter>() {
                                    @Override
                                    public void handleResult(Filter result) {
                                        p.handleResult(result);
                                    }
                                }).thenOnException(new ExceptionHandler<ResourceException>() {
                                    @Override
                                    public void handleException(ResourceException exception) {
                                        p.handleError(exception);
                                    }
                                });
                        return null;
                    }
                    @Override
                    public Void visitPresentFilter(final ResultHandler<Filter> p,
                            final JsonPointer field) {
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.PRESENT, null, null, p);
                        return null;
                    }
                    @Override
                    public Void visitStartsWithFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.STARTS_WITH, null, valueAssertion, p);
                        return null;
                    }
                };
        /*
         * Note that the returned LDAP filter may be null if it could not be mapped by any attribute mappers.
         */
        queryFilter.accept(visitor, h);
    }
    private String getRevisionFromEntry(final Entry entry) {
        return etagAttribute != null ? entry.parseAttribute(etagAttribute).asString() : null;
    }
    private org.forgerock.util.promise.ResultHandler<SearchResultEntry> postEmptyPatchResultHandler(
            final Context c, final PatchRequest request, final ResultHandler<Resource> h) {
        return new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() {
            @Override
            public void handleResult(final SearchResultEntry entry) {
                try {
                    // Fail if there is a version mismatch.
                    ensureMVCCVersionMatches(entry, request.getRevision());
                    adaptEntry(c, entry, h);
                } catch (final Exception e) {
                    h.handleError(asResourceException(e));
                }
            }
        };
    }
    private ExceptionHandler<LdapException> postEmptyPatchExceptionHandler(final ResultHandler<Resource> h) {
        return new ExceptionHandler<LdapException>() {
            @Override
            public void handleException(final LdapException exception) {
                h.handleError(asResourceException(exception));
            }
        };
    }
    private org.forgerock.util.promise.ResultHandler<Result> postUpdateResultHandler(
            final Context c, final ResultHandler<Resource> handler) {
    private AsyncFunction<Result, ResourceResponse, ResourceException> postUpdateResultAsyncFunction(
            final RequestState requestState) {
        // The handler which will be invoked for the LDAP add result.
        return new org.forgerock.util.promise.ResultHandler<Result>() {
        return new AsyncFunction<Result, ResourceResponse, ResourceException>() {
            @Override
            public void handleResult(final Result result) {
            public Promise<ResourceResponse, ResourceException> apply(Result result) throws ResourceException {
                // FIXME: handle USE_SEARCH policy.
                Entry entry;
                try {
@@ -1012,76 +928,45 @@
                    entry = null;
                }
                if (entry != null) {
                    adaptEntry(c, entry, handler);
                    return adaptEntry(requestState, entry);
                } else {
                    final Resource resource = new Resource(null, null, new JsonValue(Collections.emptyMap()));
                    handler.handleResult(resource);
                    return Promises.newResultPromise(
                            Responses.newResourceResponse(null, null, new JsonValue(Collections.emptyMap())));
                }
            }
        };
    }
    private ExceptionHandler<LdapException> postUpdateExceptionHandler(final ResultHandler<Resource> handler) {
    private AsyncFunction<LdapException, ResourceResponse, ResourceException> ldapExceptionToResourceException() {
        // The handler which will be invoked for the LDAP add result.
        return new ExceptionHandler<LdapException>() {
        return new AsyncFunction<LdapException, ResourceResponse, ResourceException>() {
            @Override
            public void handleException(final LdapException exception) {
                handler.handleError(asResourceException(exception));
            public Promise<ResourceResponse, ResourceException> apply(final LdapException ldapException)
                    throws ResourceException {
                return Promises.newExceptionPromise(asResourceException(ldapException));
            }
        };
    }
    private QueryResultHandler wrap(final Context c, final QueryResultHandler handler) {
        return new QueryResultHandler() {
            @Override
            public void handleError(final ResourceException error) {
                try {
                    handler.handleError(error);
                } finally {
                    c.close();
                }
    private RequestState wrap(final Context context) {
        return new RequestState(config, context);
            }
    private Runnable close(final RequestState requestState) {
        return new Runnable() {
            @Override
            public boolean handleResource(final Resource resource) {
                return handler.handleResource(resource);
            }
            @Override
            public void handleResult(final QueryResult result) {
                try {
                    handler.handleResult(result);
                } finally {
                    c.close();
                }
            public void run() {
                requestState.close();
            }
        };
    }
    private <V> ResultHandler<V> wrap(final Context c, final ResultHandler<V> handler) {
        return new ResultHandler<V>() {
    private ResultHandler<Connection> saveConnection(final AtomicReference<Connection> connectionHolder) {
        return new ResultHandler<Connection>() {
            @Override
            public void handleError(final ResourceException error) {
                try {
                    handler.handleError(error);
                } finally {
                    c.close();
                }
            }
            @Override
            public void handleResult(final V result) {
                try {
                    handler.handleResult(result);
                } finally {
                    c.close();
                }
            public void handleResult(Connection connection) {
                connectionHolder.set(connection);
            }
        };
    }
    private Context wrap(final ServerContext context) {
        return new Context(config, context);
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java
@@ -11,7 +11,7 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2013 ForgeRock AS.
 * Copyright 2013-2015 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
@@ -40,8 +40,8 @@
     * Returns a search request which can be used to obtain the specified REST
     * resource.
     *
     * @param c
     *            The context.
     * @param requestState
     *            The request state.
     * @param baseDN
     *            The search base DN.
     * @param resourceId
@@ -49,40 +49,40 @@
     * @return A search request which can be used to obtain the specified REST
     *         resource.
     */
    abstract SearchRequest createSearchRequest(Context c, DN baseDN, String resourceId);
    abstract SearchRequest createSearchRequest(RequestState requestState, DN baseDN, String resourceId);
    /**
     * Adds the name of any LDAP attribute required by this name strategy to the
     * provided set.
     *
     * @param c
     *            The context.
     * @param requestState
     *            The request state.
     * @param ldapAttributes
     *            The set into which any required LDAP attribute name should be
     *            put.
     */
    abstract void getLDAPAttributes(Context c, Set<String> ldapAttributes);
    abstract void getLDAPAttributes(RequestState requestState, Set<String> ldapAttributes);
    /**
     * Retrieves the resource ID from the provided LDAP entry. Implementations
     * may use the entry DN as well as any attributes in order to determine the
     * resource ID.
     *
     * @param c
     *            The context.
     * @param requestState
     *            The request state.
     * @param entry
     *            The LDAP entry from which the resource ID should be obtained.
     * @return The resource ID.
     */
    abstract String getResourceId(Context c, Entry entry);
    abstract String getResourceId(RequestState requestState, Entry entry);
    /**
     * Sets the resource ID in the provided LDAP entry. Implementations are
     * responsible for setting the entry DN as well as any attributes associated
     * with the resource ID.
     *
     * @param c
     *            The context.
     * @param requestState
     *            The request state.
     * @param baseDN
     *            The baseDN to use when constructing the entry's DN.
     * @param resourceId
@@ -93,7 +93,7 @@
     * @throws ResourceException
     *             If the resource ID cannot be determined.
     */
    abstract void setResourceId(Context c, DN baseDN, String resourceId, Entry entry)
    abstract void setResourceId(RequestState requestState, DN baseDN, String resourceId, Entry entry)
            throws ResourceException;
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
@@ -15,6 +15,12 @@
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.json.resource.PatchOperation.operation;
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Collections;
@@ -23,27 +29,20 @@
import java.util.Map;
import java.util.Set;
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.PatchOperation;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.util.Function;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.Promises;
import static org.forgerock.json.resource.PatchOperation.*;
import static org.forgerock.opendj.ldap.Filter.*;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
import static org.forgerock.opendj.rest2ldap.Utils.*;
/**
 * An attribute mapper which maps JSON objects to LDAP attributes.
 */
/** An attribute mapper which maps JSON objects to LDAP attributes. */
public final class ObjectAttributeMapper extends AttributeMapper {
    private static final class Mapping {
@@ -88,8 +87,8 @@
    }
    @Override
    void create(final Context c, final JsonPointer path, final JsonValue v,
            final ResultHandler<List<Attribute>> h) {
    Promise<List<Attribute>, ResourceException> create(
            final RequestState requestState, final JsonPointer path, final JsonValue v) {
        try {
            /*
             * First check that the JSON value is an object and that the fields
@@ -98,66 +97,67 @@
            final Map<String, Mapping> missingMappings = checkMapping(path, v);
            // Accumulate the results of the subordinate mappings.
            final ResultHandler<List<Attribute>> handler = accumulator(h);
            final List<Promise<List<Attribute>, ResourceException>> promises = new ArrayList<>();
            // Invoke mappings for which there are values provided.
            if (v != null && !v.isNull()) {
                for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
                    final Mapping mapping = getMapping(me.getKey());
                    final JsonValue subValue = new JsonValue(me.getValue());
                    mapping.mapper.create(c, path.child(me.getKey()), subValue, handler);
                    promises.add(mapping.mapper.create(requestState, path.child(me.getKey()), subValue));
                }
            }
            // Invoke mappings for which there were no values provided.
            for (final Mapping mapping : missingMappings.values()) {
                mapping.mapper.create(c, path.child(mapping.name), null, handler);
                promises.add(mapping.mapper.create(requestState, path.child(mapping.name), null));
            }
            return Promises.when(promises)
                           .then(this.<Attribute> accumulateResults());
        } catch (final Exception e) {
            h.handleError(asResourceException(e));
            return Promises.newExceptionPromise(asResourceException(e));
        }
    }
    @Override
    void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
    void getLDAPAttributes(final RequestState requestState, final JsonPointer path, final JsonPointer subPath,
            final Set<String> ldapAttributes) {
        if (subPath.isEmpty()) {
            // Request all subordinate mappings.
            for (final Mapping mapping : mappings.values()) {
                mapping.mapper.getLDAPAttributes(c, path.child(mapping.name), subPath,
                        ldapAttributes);
                mapping.mapper.getLDAPAttributes(requestState, path.child(mapping.name), subPath, ldapAttributes);
            }
        } else {
            // Request single subordinate mapping.
            final Mapping mapping = getMapping(subPath);
            if (mapping != null) {
                mapping.mapper.getLDAPAttributes(c, path.child(subPath.get(0)), subPath
                        .relativePointer(), ldapAttributes);
                mapping.mapper.getLDAPAttributes(
                        requestState, path.child(subPath.get(0)), subPath.relativePointer(), ldapAttributes);
            }
        }
    }
    @Override
    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
            final FilterType type, final String operator, final Object valueAssertion,
            final ResultHandler<Filter> h) {
    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
        final Mapping mapping = getMapping(subPath);
        if (mapping != null) {
            mapping.mapper.getLDAPFilter(c, path.child(subPath.get(0)), subPath.relativePointer(),
                    type, operator, valueAssertion, h);
            return mapping.mapper.getLDAPFilter(requestState, path.child(subPath.get(0)),
                    subPath.relativePointer(), type, operator, valueAssertion);
        } else {
            /*
             * Either the filter targeted the entire object (i.e. it was "/"),
             * or it targeted an unrecognized attribute within the object.
             * Either way, the filter will never match.
             */
            h.handleResult(alwaysFalse());
            return Promises.newResultPromise(alwaysFalse());
        }
    }
    @Override
    void patch(final Context c, final JsonPointer path, final PatchOperation operation,
            final ResultHandler<List<Modification>> h) {
    Promise<List<Modification>, ResourceException> patch(
            final RequestState requestState, final JsonPointer path, final PatchOperation operation) {
        try {
            final JsonPointer field = operation.getField();
            final JsonValue v = operation.getValue();
@@ -168,11 +168,10 @@
                 * by allowing the JSON value to be a partial object and
                 * add/remove/replace only the provided values.
                 */
                final Map<String, Mapping> missingMappings = checkMapping(path, v);
                checkMapping(path, v);
                // Accumulate the results of the subordinate mappings.
                final ResultHandler<List<Modification>> handler =
                        accumulator(mappings.size() - missingMappings.size(), h);
                final List<Promise<List<Modification>, ResourceException>> promises = new ArrayList<>();
                // Invoke mappings for which there are values provided.
                if (!v.isNull()) {
@@ -181,9 +180,12 @@
                        final JsonValue subValue = new JsonValue(me.getValue());
                        final PatchOperation subOperation =
                                operation(operation.getOperation(), field /* empty */, subValue);
                        mapping.mapper.patch(c, path.child(me.getKey()), subOperation, handler);
                        promises.add(mapping.mapper.patch(requestState, path.child(me.getKey()), subOperation));
                    }
                }
                return Promises.when(promises)
                               .then(this.<Modification> accumulateResults());
            } else {
                /*
                 * The patch operation targets a subordinate field. Create a new
@@ -199,92 +201,92 @@
                }
                final PatchOperation subOperation =
                        operation(operation.getOperation(), field.relativePointer(), v);
                mapping.mapper.patch(c, path.child(fieldName), subOperation, h);
                return mapping.mapper.patch(requestState, path.child(fieldName), subOperation);
            }
        } catch (final Exception ex) {
            h.handleError(asResourceException(ex));
            return Promises.newExceptionPromise(asResourceException(ex));
        }
    }
    @Override
    void read(final Context c, final JsonPointer path, final Entry e,
            final ResultHandler<JsonValue> h) {
    Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) {
        /*
         * Use an accumulator which will aggregate the results from the
         * subordinate mappers into a single list. On completion, the
         * accumulator combines the results into a single JSON map object.
         */
        final ResultHandler<Map.Entry<String, JsonValue>> handler =
                accumulate(mappings.size(), transform(
                        new Function<List<Map.Entry<String, JsonValue>>, JsonValue, NeverThrowsException>() {
        final List<Promise<Map.Entry<String, JsonValue>, ResourceException>> promises =
                new ArrayList<>(mappings.size());
        for (final Mapping mapping : mappings.values()) {
            promises.add(mapping.mapper.read(requestState, path.child(mapping.name), e)
                    .then(new Function<JsonValue, Map.Entry<String, JsonValue>, ResourceException>() {
                        @Override
                        public Map.Entry<String, JsonValue> apply(final JsonValue value) {
                            return value != null ? new SimpleImmutableEntry<String, JsonValue>(mapping.name, value)
                                                 : null;
                        }
                    }));
        }
        return Promises.when(promises)
                .then(new Function<List<Map.Entry<String, JsonValue>>, JsonValue, ResourceException>() {
                            @Override
                            public JsonValue apply(final List<Map.Entry<String, JsonValue>> value) {
                                if (value.isEmpty()) {
                                    /*
                                     * No subordinate attributes, so omit the
                                     * entire JSON object from the resource.
                             * No subordinate attributes, so omit the entire
                             * JSON object from the resource.
                                     */
                                    return null;
                                } else {
                                    // Combine the sub-attributes into a single JSON object.
                                    final Map<String, Object> result = new LinkedHashMap<>(value.size());
                                    for (final Map.Entry<String, JsonValue> e : value) {
                                if (e != null) {
                                        result.put(e.getKey(), e.getValue().getObject());
                                    }
                            }
                                    return new JsonValue(result);
                                }
                            }
                        }, h));
        for (final Mapping mapping : mappings.values()) {
            mapping.mapper.read(c, path.child(mapping.name), e, transform(
                    new Function<JsonValue, Map.Entry<String, JsonValue>, NeverThrowsException>() {
                        @Override
                        public Map.Entry<String, JsonValue> apply(final JsonValue value) {
                            return value != null ? new SimpleImmutableEntry<String, JsonValue>(
                                    mapping.name, value) : null;
                        }
                    }, handler));
        }
                });
    }
    @Override
    void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
            final ResultHandler<List<Modification>> h) {
    Promise<List<Modification>, ResourceException> update(
            final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) {
        try {
            /*
             * First check that the JSON value is an object and that the fields
             * it contains are known by this mapper.
             */
            // First check that the JSON value is an object and that the fields
            // it contains are known by this mapper.
            final Map<String, Mapping> missingMappings = checkMapping(path, v);
            // Accumulate the results of the subordinate mappings.
            final ResultHandler<List<Modification>> handler = accumulator(h);
            final List<Promise<List<Modification>, ResourceException>> promises = new ArrayList<>();
            // Invoke mappings for which there are values provided.
            if (v != null && !v.isNull()) {
                for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
                    final Mapping mapping = getMapping(me.getKey());
                    final JsonValue subValue = new JsonValue(me.getValue());
                    mapping.mapper.update(c, path.child(me.getKey()), e, subValue, handler);
                    promises.add(mapping.mapper.update(requestState, path.child(me.getKey()), e, subValue));
                }
            }
            // Invoke mappings for which there were no values provided.
            for (final Mapping mapping : missingMappings.values()) {
                mapping.mapper.update(c, path.child(mapping.name), e, null, handler);
                promises.add(mapping.mapper.update(requestState, path.child(mapping.name), e, null));
            }
            return Promises.when(promises)
                           .then(this.<Modification> accumulateResults());
        } catch (final Exception ex) {
            h.handleError(asResourceException(ex));
            return Promises.newExceptionPromise(asResourceException(ex));
        }
    }
    private <T> ResultHandler<List<T>> accumulator(final ResultHandler<List<T>> h) {
        return accumulator(mappings.size(), h);
    }
    private <T> ResultHandler<List<T>> accumulator(final int size, final ResultHandler<List<T>> h) {
        return accumulate(size, transform(new Function<List<List<T>>, List<T>, NeverThrowsException>() {
    private <T> Function<List<List<T>>, List<T>, ResourceException> accumulateResults() {
        return new Function<List<List<T>>, List<T>, ResourceException>() {
            @Override
            public List<T> apply(final List<List<T>> value) {
                switch (value.size()) {
@@ -300,13 +302,10 @@
                    return attributes;
                }
            }
        }, h));
        };
    }
    /**
     * Fail immediately if the JSON value has the wrong type or contains unknown
     * attributes.
     */
    /** Fail immediately if the JSON value has the wrong type or contains unknown attributes. */
    private Map<String, Mapping> checkMapping(final JsonPointer path, final JsonValue v)
            throws ResourceException {
        final Map<String, Mapping> missingMappings = new LinkedHashMap<>(mappings);
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
@@ -15,6 +15,12 @@
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.opendj.ldap.LdapException.newLdapException;
import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.LinkedList;
@@ -23,19 +29,19 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.EntryNotFoundException;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.LinkedAttribute;
import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
import org.forgerock.opendj.ldap.ResultCode;
@@ -45,14 +51,13 @@
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
import org.forgerock.opendj.ldap.responses.SearchResultReference;
import org.forgerock.util.AsyncFunction;
import org.forgerock.util.Function;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.ExceptionHandler;
import static org.forgerock.opendj.ldap.LdapException.*;
import static org.forgerock.opendj.ldap.requests.Requests.*;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
import static org.forgerock.opendj.rest2ldap.Utils.*;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.PromiseImpl;
import org.forgerock.util.promise.Promises;
import org.forgerock.util.promise.ResultHandler;
/**
 * An attribute mapper which provides a mapping from a JSON value to a single DN
@@ -125,33 +130,28 @@
    }
    @Override
    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath, final FilterType type,
        final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
        // Construct a filter which can be used to find referenced resources.
        mapper.getLDAPFilter(c, path, subPath, type, operator, valueAssertion, new ResultHandler<Filter>() {
            @Override
            public void handleError(final ResourceException error) {
                h.handleError(error); // Propagate.
            }
    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
        return mapper.getLDAPFilter(requestState, path, subPath, type, operator, valueAssertion)
                .thenAsync(new AsyncFunction<Filter, Filter, ResourceException>() {
            @Override
            public void handleResult(final Filter result) {
                    public Promise<Filter, ResourceException> apply(final Filter result) {
                // Search for all referenced entries and construct a filter.
                final SearchRequest request = createSearchRequest(result);
                final List<Filter> subFilters = new LinkedList<>();
                final ExceptionHandler<LdapException> exceptionHandler = new ExceptionHandler<LdapException>() {
                        return requestState.getConnection().thenAsync(
                                new AsyncFunction<Connection, Filter, ResourceException>() {
                    @Override
                    public void handleException(LdapException exception) {
                        h.handleError(asResourceException(exception)); // Propagate.
                    }
                };
                c.getConnection().searchAsync(request, new SearchResultHandler() {
                                    public Promise<Filter, ResourceException> apply(final Connection connection)
                                            throws ResourceException {
                                        return connection.searchAsync(request, new SearchResultHandler() {
                    @Override
                    public boolean handleEntry(final SearchResultEntry entry) {
                        if (subFilters.size() < SEARCH_MAX_CANDIDATES) {
                            subFilters.add(Filter.equality(ldapAttributeName.toString(), entry.getName()));
                                                    subFilters.add(Filter.equality(
                                                            ldapAttributeName.toString(), entry.getName()));
                            return true;
                        } else {
                            // No point in continuing - maximum candidates reached.
@@ -164,25 +164,33 @@
                        // Ignore references.
                        return true;
                    }
                }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Result>() {
                                        }).then(new Function<Result, Filter, ResourceException>() {
                    @Override
                    public void handleResult(Result result) {
                                            public Filter apply(Result result) throws ResourceException {
                        if (subFilters.size() >= SEARCH_MAX_CANDIDATES) {
                            exceptionHandler.handleException(newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED));
                                                    throw asResourceException(
                                                            newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED));
                        } else if (subFilters.size() == 1) {
                            h.handleResult(subFilters.get(0));
                                                    return subFilters.get(0);
                        } else {
                            h.handleResult(Filter.or(subFilters));
                                                    return Filter.or(subFilters);
                        }
                    }
                }).thenOnException(exceptionHandler);
                                        }, new Function<LdapException, Filter, ResourceException>() {
                                            @Override
                                            public Filter apply(LdapException exception) throws ResourceException {
                                                throw asResourceException(exception);
                                            }
                                        });
                                    }
                                });
            }
        });
    }
    @Override
    void getNewLDAPAttributes(final Context c, final JsonPointer path, final List<Object> newValues,
        final ResultHandler<Attribute> h) {
    Promise<Attribute, ResourceException> getNewLDAPAttributes(
            final RequestState requestState, final JsonPointer path, final List<Object> newValues) {
        /*
         * For each value use the subordinate mapper to obtain the LDAP primary
         * key, the perform a search for each one to find the corresponding entries.
@@ -190,17 +198,12 @@
        final Attribute newLDAPAttribute = new LinkedAttribute(ldapAttributeName);
        final AtomicInteger pendingSearches = new AtomicInteger(newValues.size());
        final AtomicReference<ResourceException> exception = new AtomicReference<>();
        final PromiseImpl<Attribute, ResourceException> promise = PromiseImpl.create();
        for (final Object value : newValues) {
            mapper.create(c, path, new JsonValue(value), new ResultHandler<List<Attribute>>() {
            mapper.create(requestState, path, new JsonValue(value)).thenOnResult(new ResultHandler<List<Attribute>>() {
                @Override
                public void handleError(final ResourceException error) {
                    h.handleError(error);
                }
                @Override
                public void handleResult(final List<Attribute> result) {
                public void handleResult(List<Attribute> result) {
                    Attribute primaryKeyAttribute = null;
                    for (final Attribute attribute : result) {
                        if (attribute.getAttributeDescription().equals(primaryKey)) {
@@ -210,25 +213,26 @@
                    }
                    if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) {
                        h.handleError(new BadRequestException(i18n(
                            "The request cannot be processed because the reference "
                                + "field '%s' contains a value which does not contain " + "a primary key", path)));
                        return;
                        promise.handleException(new BadRequestException(
                                i18n("The request cannot be processed because the reference field '%s' contains "
                                        + "a value which does not contain a primary key", path)));
                    }
                    if (primaryKeyAttribute.size() > 1) {
                        h.handleError(new BadRequestException(i18n(
                            "The request cannot be processed because the reference "
                                + "field '%s' contains a value which contains multiple " + "primary keys", path)));
                        return;
                        promise.handleException(new BadRequestException(
                                i18n("The request cannot be processed because the reference field '%s' contains "
                                        + "a value which contains multiple primary keys", path)));
                    }
                    // Now search for the referenced entry in to get its DN.
                    final ByteString primaryKeyValue = primaryKeyAttribute.firstValue();
                    final Filter filter = Filter.equality(primaryKey.toString(), primaryKeyValue);
                    final SearchRequest search = createSearchRequest(filter);
                    c.getConnection().searchSingleEntryAsync(search).thenOnResult(
                            new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() {
                    requestState.getConnection().thenOnResult(new ResultHandler<Connection>() {
                        @Override
                        public void handleResult(Connection connection) {
                            connection.searchSingleEntryAsync(search)
                                      .thenOnResult(new ResultHandler<SearchResultEntry>() {
                            @Override
                            public void handleResult(final SearchResultEntry result) {
                                synchronized (newLDAPAttribute) {
@@ -244,13 +248,13 @@
                                    throw error;
                                } catch (final EntryNotFoundException e) {
                                    re = new BadRequestException(i18n(
                                            "The request cannot be processed " + "because the resource '%s' "
                                                    + "referenced in field '%s' does " + "not exist",
                                                          "The request cannot be processed because the resource "
                                                          + "'%s' referenced in field '%s' does not exist",
                                            primaryKeyValue.toString(), path));
                                } catch (final MultipleEntriesFoundException e) {
                                    re = new BadRequestException(i18n(
                                            "The request cannot be processed " + "because the resource '%s' "
                                                    + "referenced in field '%s' is " + "ambiguous",
                                                          "The request cannot be processed because the resource "
                                                          + "'%s' referenced in field '%s' is ambiguous",
                                            primaryKeyValue.toString(), path));
                                } catch (final LdapException e) {
                                    re = asResourceException(e);
@@ -260,18 +264,21 @@
                            }
                        });
                }
                    });
                }
                private void completeIfNecessary() {
                    if (pendingSearches.decrementAndGet() == 0) {
                        if (exception.get() != null) {
                            h.handleError(exception.get());
                            promise.handleException(exception.get());
                        } else {
                            h.handleResult(newLDAPAttribute);
                            promise.handleResult(newLDAPAttribute);
                        }
                    }
                }
            });
        }
        return promise;
    }
    @Override
@@ -280,30 +287,33 @@
    }
    @Override
    void read(final Context c, final JsonPointer path, final Entry e, final ResultHandler<JsonValue> h) {
    Promise<JsonValue, ResourceException> read(final RequestState c, final JsonPointer path, final Entry e) {
        final Attribute attribute = e.getAttribute(ldapAttributeName);
        if (attribute == null || attribute.isEmpty()) {
            h.handleResult(null);
            return Promises.newResultPromise(null);
        } else if (attributeIsSingleValued()) {
            try {
                final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN();
                readEntry(c, path, dn, h);
                return readEntry(c, path, dn);
            } catch (final Exception ex) {
                // The LDAP attribute could not be decoded.
                h.handleError(asResourceException(ex));
                return Promises.newExceptionPromise(asResourceException(ex));
            }
        } else {
            try {
                final Set<DN> dns = attribute.parse().usingSchema(c.getConfig().schema()).asSetOfDN();
                final ResultHandler<JsonValue> handler =
                    accumulate(dns.size(), transform(new Function<List<JsonValue>, JsonValue, NeverThrowsException>() {
                final List<Promise<JsonValue, ResourceException>> promises = new ArrayList<>(dns.size());
                for (final DN dn : dns) {
                    promises.add(readEntry(c, path, dn));
                }
                return Promises.when(promises)
                               .then(new Function<List<JsonValue>, JsonValue, ResourceException>() {
                        @Override
                        public JsonValue apply(final List<JsonValue> value) {
                            if (value.isEmpty()) {
                                /*
                                 * No values, so omit the entire JSON object
                                 * from the resource.
                                 */
                                           // No values, so omit the entire JSON object from the resource.
                                return null;
                            } else {
                                // Combine values into a single JSON array.
@@ -314,13 +324,10 @@
                                return new JsonValue(result);
                            }
                        }
                    }, h));
                for (final DN dn : dns) {
                    readEntry(c, path, dn, handler);
                }
                               });
            } catch (final Exception ex) {
                // The LDAP attribute could not be decoded.
                h.handleError(asResourceException(ex));
                return Promises.newExceptionPromise(asResourceException(ex));
            }
        }
    }
@@ -330,30 +337,31 @@
        return newSearchRequest(baseDN, scope, searchFilter, "1.1");
    }
    private void readEntry(final Context c, final JsonPointer path, final DN dn,
        final ResultHandler<JsonValue> handler) {
    private Promise<JsonValue, ResourceException> readEntry(
            final RequestState requestState, final JsonPointer path, final DN dn) {
        final Set<String> requestedLDAPAttributes = new LinkedHashSet<>();
        mapper.getLDAPAttributes(c, path, new JsonPointer(), requestedLDAPAttributes);
        c.getConnection().readEntryAsync(dn, requestedLDAPAttributes)
                .thenOnResult(new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() {
        mapper.getLDAPAttributes(requestState, path, new JsonPointer(), requestedLDAPAttributes);
        return requestState.getConnection().thenAsync(new AsyncFunction<Connection, JsonValue, ResourceException>() {
                    @Override
                    public void handleResult(final SearchResultEntry result) {
                        mapper.read(c, path, result, handler);
            public Promise<JsonValue, ResourceException> apply(Connection connection) throws ResourceException {
                return connection.readEntryAsync(dn, requestedLDAPAttributes)
                        .thenAsync(new AsyncFunction<SearchResultEntry, JsonValue, ResourceException>() {
                            @Override
                            public Promise<JsonValue, ResourceException> apply(final SearchResultEntry result) {
                                return mapper.read(requestState, path, result);
                    }
                }).thenOnException(new ExceptionHandler<LdapException>() {
                        }, new AsyncFunction<LdapException, JsonValue, ResourceException>() {
                    @Override
                    public void handleException(final LdapException error) {
                            public Promise<JsonValue, ResourceException> apply(final LdapException error) {
                        if (!(error instanceof EntryNotFoundException)) {
                            handler.handleError(asResourceException(error));
                                    return Promises.newExceptionPromise(asResourceException(error));
                        } else {
                            /*
                             * The referenced entry does not exist so ignore it
                             * since it cannot be mapped.
                             */
                            handler.handleResult(null);
                                    // The referenced entry does not exist so ignore it since it cannot be mapped.
                                    return Promises.newResultPromise(null);
                        }
                    }
                });
    }
        });
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java
File was renamed from opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java
@@ -21,10 +21,10 @@
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import org.forgerock.http.Context;
import org.forgerock.json.resource.InternalServerErrorException;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.SecurityContext;
import org.forgerock.json.resource.ServerContext;
import org.forgerock.opendj.ldap.AbstractAsynchronousConnection;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionEventListener;
@@ -55,20 +55,21 @@
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
import org.forgerock.opendj.ldap.responses.SearchResultReference;
import org.forgerock.util.promise.ExceptionHandler;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.PromiseImpl;
import org.forgerock.util.promise.Promises;
import org.forgerock.util.promise.ResultHandler;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
import static org.forgerock.opendj.rest2ldap.Utils.*;
/**
 * Common context information passed to containers and mappers. A new context is
 * allocated for each REST request.
 * Common request information passed to containers and mappers.
 * A new @{code RequestState} is allocated for each REST request.
 */
final class Context implements Closeable {
final class RequestState implements Closeable {
    /**
     * A cached read request - see cachedReads for more information.
     */
    /** A cached read request - see cachedReads for more information. */
    private static final class CachedRead implements SearchResultHandler, LdapResultHandler<Result> {
        private SearchResultEntry cachedEntry;
        private final String cachedFilterString;
@@ -122,10 +123,7 @@
        }
        LdapPromise<Result> getPromise() {
            /*
             * Perform uninterrupted wait since this method is unlikely to block
             * for a long time.
             */
            // Perform uninterrupted wait since this method is unlikely to block for a long time.
            boolean wasInterrupted = false;
            while (true) {
                try {
@@ -185,11 +183,11 @@
    };
    private final Config config;
    private final ServerContext context;
    private final Context context;
    private Connection connection;
    private Control proxiedAuthzControl;
    Context(final Config config, final ServerContext context) {
    RequestState(final Config config, final Context context) {
        this.config = config;
        this.context = context;
@@ -215,31 +213,20 @@
        return config;
    }
    Connection getConnection() {
        return connection;
    }
    ServerContext getServerContext() {
    Context getContext() {
        return context;
    }
    /**
     * Performs common processing required before handling an HTTP request,
     * including calculating the proxied authorization request control, and
     * obtaining an LDAP connection.
     * including calculating the proxied authorization request control. Then
     * return a promise containing a valid LDAP connection or a
     * {@link ResourceException} if an error is detected.
     * <p>
     * This method should be called at most once per request.
     *
     * @param handler
     *            The result handler which should be invoked if an error is
     *            detected.
     * @param runnable
     *            The runnable which will be invoked once the common processing
     *            has completed. Implementations will be able to call
     *            {@link #getConnection()} to get the LDAP connection for use
     *            with subsequent LDAP requests.
     * @return A {@link Promise} containing a valid {@link Connection}
     */
    void run(final org.forgerock.json.resource.ResultHandler<?> handler, final Runnable runnable) {
    Promise<Connection, ResourceException> getConnection() {
        /*
         * Compute the proxied authorization control from the content of the
         * security context if present. Only do this if we are not using a
@@ -254,13 +241,11 @@
                            securityContext.getAuthorizationId(), config.schema());
                    proxiedAuthzControl = ProxiedAuthV2RequestControl.newControl(authzId);
                } catch (final ResourceException e) {
                    handler.handleError(e);
                    return;
                    return Promises.newExceptionPromise(e);
                }
            } else {
                handler.handleError(new InternalServerErrorException(
                return Promises.<Connection, ResourceException> newExceptionPromise(new InternalServerErrorException(
                        i18n("The request could not be authorized because it did not contain a security context")));
                return;
            }
        }
@@ -270,23 +255,24 @@
         * to re-use the LDAP connection which was used for authentication.
         */
        if (connection != null) {
            // Invoke the handler immediately since a connection is available.
            runnable.run();
            return Promises.newResultPromise(connection);
        } else if (config.connectionFactory() != null) {
            final PromiseImpl<Connection, ResourceException> promise = PromiseImpl.create();
            config.connectionFactory().getConnectionAsync().thenOnResult(new ResultHandler<Connection>() {
                @Override
                public final void handleResult(final Connection result) {
                    connection = wrap(result);
                    runnable.run();
                    promise.handleResult(connection);
                }
            }).thenOnException(new ExceptionHandler<LdapException>() {
                @Override
                public final void handleException(final LdapException exception) {
                    handler.handleError(asResourceException(exception));
                    promise.handleException(asResourceException(exception));
                }
            });
            return promise;
        } else {
            handler.handleError(new InternalServerErrorException(
            return Promises.<Connection, ResourceException> newExceptionPromise(new InternalServerErrorException(
                    i18n("The request could not be processed because there was no LDAP connection available for use")));
        }
    }
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -31,8 +31,8 @@
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.fluent.JsonValueException;
import org.forgerock.json.JsonValue;
import org.forgerock.json.JsonValueException;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.CollectionResourceProvider;
import org.forgerock.json.resource.ResourceException;
@@ -49,11 +49,11 @@
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.EntryNotFoundException;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.FailoverLoadBalancingAlgorithm;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.LDAPConnectionFactory;
import org.forgerock.opendj.ldap.LDAPOptions;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.LinkedAttribute;
import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
import org.forgerock.opendj.ldap.RDN;
@@ -74,7 +74,6 @@
 * collections.
 */
public final class Rest2LDAP {
    /**
     * Indicates whether or not LDAP client connections should use SSL or
     * StartTLS.
@@ -715,23 +714,23 @@
        }
        @Override
        SearchRequest createSearchRequest(final Context c, final DN baseDN, final String resourceId) {
        SearchRequest createSearchRequest(final RequestState requestState, final DN baseDN, final String resourceId) {
            return newSearchRequest(baseDN, SearchScope.SINGLE_LEVEL, Filter.equality(idAttribute
                    .toString(), resourceId));
        }
        @Override
        void getLDAPAttributes(final Context c, final Set<String> ldapAttributes) {
        void getLDAPAttributes(final RequestState requestState, final Set<String> ldapAttributes) {
            ldapAttributes.add(idAttribute.toString());
        }
        @Override
        String getResourceId(final Context c, final Entry entry) {
        String getResourceId(final RequestState requestState, final Entry entry) {
            return entry.parseAttribute(idAttribute).asString();
        }
        @Override
        void setResourceId(final Context c, final DN baseDN, final String resourceId,
        void setResourceId(final RequestState requestState, final DN baseDN, final String resourceId,
                final Entry entry) throws ResourceException {
            if (isServerProvided) {
                if (resourceId != null) {
@@ -756,23 +755,23 @@
        }
        @Override
        SearchRequest createSearchRequest(final Context c, final DN baseDN, final String resourceId) {
        SearchRequest createSearchRequest(final RequestState requestState, final DN baseDN, final String resourceId) {
            return newSearchRequest(baseDN.child(rdn(resourceId)), SearchScope.BASE_OBJECT, Filter
                    .objectClassPresent());
        }
        @Override
        void getLDAPAttributes(final Context c, final Set<String> ldapAttributes) {
        void getLDAPAttributes(final RequestState requestState, final Set<String> ldapAttributes) {
            ldapAttributes.add(attribute.toString());
        }
        @Override
        String getResourceId(final Context c, final Entry entry) {
        String getResourceId(final RequestState requestState, final Entry entry) {
            return entry.parseAttribute(attribute).asString();
        }
        @Override
        void setResourceId(final Context c, final DN baseDN, final String resourceId,
        void setResourceId(final RequestState requestState, final DN baseDN, final String resourceId,
                final Entry entry) throws ResourceException {
            if (resourceId != null) {
                entry.setName(baseDN.child(rdn(resourceId)));
@@ -987,10 +986,6 @@
        return simple(AttributeDescription.valueOf(attribute));
    }
    private static ConnectionFactory configureConnectionFactory(final JsonValue configuration) {
        return configureConnectionFactory(configuration, (ClassLoader) null);
    }
    private static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
                                                                final ClassLoader providerClassLoader) {
        // Parse pool parameters,
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
New file
@@ -0,0 +1,167 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2015 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.http.util.Json.*;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.configureConnectionFactory;
import static org.forgerock.util.Utils.*;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import org.forgerock.http.Context;
import org.forgerock.http.Handler;
import org.forgerock.http.HttpApplication;
import org.forgerock.http.HttpApplicationException;
import org.forgerock.http.handler.Handlers;
import org.forgerock.http.io.Buffer;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.CollectionResourceProvider;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.Router;
import org.forgerock.json.resource.http.CrestHttp;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.util.Factory;
import org.forgerock.util.Reject;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Rest2ldap HTTP application. */
public final class Rest2LDAPHttpApplication implements HttpApplication {
    private static final Logger LOG = LoggerFactory.getLogger(Rest2LDAPHttpApplication.class);
    private static final class HttpHandler implements Handler, Closeable {
        private final ConnectionFactory ldapConnectionFactory;
        private final Handler delegate;
        HttpHandler(final JsonValue configuration) {
            ldapConnectionFactory = createLdapConnectionFactory(configuration);
            try {
                delegate = CrestHttp.newHttpHandler(createRouter(configuration, ldapConnectionFactory));
            } catch (final RuntimeException e) {
                closeSilently(ldapConnectionFactory);
                throw e;
            }
        }
        private static RequestHandler createRouter(
                final JsonValue configuration, final ConnectionFactory ldapConnectionFactory) {
            final AuthorizationPolicy authzPolicy = configuration.get("servlet")
                    .get("authorizationPolicy")
                    .required()
                    .asEnum(AuthorizationPolicy.class);
            final String proxyAuthzTemplate = configuration.get("servlet").get("proxyAuthzIdTemplate").asString();
            final JsonValue mappings = configuration.get("servlet").get("mappings").required();
            final Router router = new Router();
            for (final String mappingUrl : mappings.keys()) {
                final JsonValue mapping = mappings.get(mappingUrl);
                final CollectionResourceProvider provider = Rest2LDAP.builder()
                        .ldapConnectionFactory(ldapConnectionFactory)
                        .authorizationPolicy(authzPolicy)
                        .proxyAuthzIdTemplate(proxyAuthzTemplate)
                        .configureMapping(mapping)
                        .build();
                router.addRoute(Router.uriTemplate(mappingUrl), provider);
            }
            return router;
        }
        private static ConnectionFactory createLdapConnectionFactory(final JsonValue configuration) {
            final String ldapFactoryName = configuration.get("servlet").get("ldapConnectionFactory").asString();
            if (ldapFactoryName != null) {
                return configureConnectionFactory(
                        configuration.get("ldapConnectionFactories").required(), ldapFactoryName);
            }
            return null;
        }
        @Override
        public void close() {
            closeSilently(ldapConnectionFactory);
        }
        @Override
        public Promise<Response, NeverThrowsException> handle(final Context context, final Request request) {
            return delegate.handle(context, request);
        }
    }
    private final URL configurationUrl;
    private HttpHandler handler;
    private HttpAuthenticationFilter filter;
    /**
     * Default constructor called by the HTTP Framework which will use the
     * default configuration file location.
     */
    public Rest2LDAPHttpApplication() {
        this.configurationUrl = getClass().getResource("/opendj-rest2ldap-config.json");
    }
    /**
     * Creates a new Rest2LDAP HTTP application using the provided configuration URL.
     *
     * @param configurationURL
     *            The URL to the JSON configuration file.
     */
    public Rest2LDAPHttpApplication(final URL configurationURL) {
        Reject.ifNull(configurationURL, "The configuration URL must not be null");
        this.configurationUrl = configurationURL;
    }
    private static JsonValue readJson(final URL resource) throws IOException {
        try (InputStream in = resource.openStream()) {
            return new JsonValue(readJsonLenient(in));
        }
    }
    @Override
    public Handler start() throws HttpApplicationException {
        try {
            final JsonValue configuration = readJson(configurationUrl);
            handler = new HttpHandler(configuration);
            filter = new HttpAuthenticationFilter(configuration);
            return Handlers.chainOf(handler, filter);
        } catch (final Exception e) {
            // TODO i18n, once supported in opendj-rest2ldap
            final String errorMsg = "Unable to start Rest2Ldap Http Application";
            LOG.error(errorMsg, e);
            stop();
            throw new HttpApplicationException(errorMsg, e);
        }
    }
    @Override
    public Factory<Buffer> getBufferFactory() {
        // Use container default buffer factory.
        return null;
    }
    @Override
    public void stop() {
        closeSilently(handler, filter);
        handler = null;
        filter = null;
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
@@ -11,7 +11,7 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2012-2014 ForgeRock AS.
 * Copyright 2012-2015 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
@@ -19,10 +19,10 @@
import java.util.List;
import java.util.Set;
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.ByteString;
@@ -30,12 +30,15 @@
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.util.Function;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
import static java.util.Collections.*;
import static org.forgerock.opendj.ldap.Filter.*;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
import static org.forgerock.opendj.rest2ldap.Utils.*;
import static org.forgerock.util.promise.Promises.newExceptionPromise;
import static org.forgerock.util.promise.Promises.newResultPromise;
/**
 * An attribute mapper which provides a simple mapping from a JSON value to a
@@ -111,34 +114,33 @@
    }
    @Override
    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
            final FilterType type, final String operator, final Object valueAssertion,
            final ResultHandler<Filter> h) {
    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
        if (subPath.isEmpty()) {
            try {
                final ByteString va =
                        valueAssertion != null ? encoder().apply(valueAssertion) : null;
                h.handleResult(toFilter(type, ldapAttributeName.toString(), va));
                return newResultPromise(toFilter(type, ldapAttributeName.toString(), va));
            } catch (final Exception e) {
                // Invalid assertion value - bad request.
                h.handleError(new BadRequestException(i18n(
                return newExceptionPromise((ResourceException) new BadRequestException(i18n(
                        "The request cannot be processed because it contained an "
                                + "illegal filter assertion value '%s' for field '%s'", String
                                .valueOf(valueAssertion), path), e));
                                + "illegal filter assertion value '%s' for field '%s'",
                        String.valueOf(valueAssertion), path), e));
            }
        } else {
            // This attribute mapper does not support partial filtering.
            h.handleResult(alwaysFalse());
            return newResultPromise(alwaysFalse());
        }
    }
    @Override
    void getNewLDAPAttributes(final Context c, final JsonPointer path,
            final List<Object> newValues, final ResultHandler<Attribute> h) {
    Promise<Attribute, ResourceException> getNewLDAPAttributes(
            final RequestState requestState, final JsonPointer path, final List<Object> newValues) {
        try {
            h.handleResult(jsonToAttribute(newValues, ldapAttributeName, encoder()));
            return newResultPromise(jsonToAttribute(newValues, ldapAttributeName, encoder()));
        } catch (final Exception ex) {
            h.handleError(new BadRequestException(i18n(
            return newExceptionPromise((ResourceException) new BadRequestException(i18n(
                    "The request cannot be processed because an error occurred while "
                            + "encoding the values for the field '%s': %s", path, ex.getMessage())));
        }
@@ -150,8 +152,7 @@
    }
    @Override
    void read(final Context c, final JsonPointer path, final Entry e,
            final ResultHandler<JsonValue> h) {
    Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) {
        try {
            final Object value;
            if (attributeIsSingleValued()) {
@@ -161,16 +162,16 @@
            } else {
                final Set<Object> s =
                        e.parseAttribute(ldapAttributeName).asSetOf(decoder(), defaultJSONValues);
                value = s.isEmpty() ? null : new ArrayList<Object>(s);
                value = s.isEmpty() ? null : new ArrayList<>(s);
            }
            h.handleResult(value != null ? new JsonValue(value) : null);
            return newResultPromise(value != null ? new JsonValue(value) : null);
        } catch (final Exception ex) {
            // The LDAP attribute could not be decoded.
            h.handleError(asResourceException(ex));
            return newExceptionPromise(asResourceException(ex));
        }
    }
    private Function<ByteString, ? extends Object, NeverThrowsException> decoder() {
    private Function<ByteString, ?, NeverThrowsException> decoder() {
        return decoder == null ? byteStringToJson(ldapAttributeName) : decoder;
    }
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
@@ -25,18 +25,14 @@
import static org.forgerock.opendj.ldap.schema.CoreSchema.getBooleanSyntax;
import static org.forgerock.opendj.ldap.schema.CoreSchema.getGeneralizedTimeSyntax;
import static org.forgerock.opendj.ldap.schema.CoreSchema.getIntegerSyntax;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicInteger;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.json.JsonValue;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.ByteString;
@@ -51,64 +47,6 @@
 * Internal utility methods.
 */
final class Utils {
    /**
     * Implementation class for {@link #accumulate}.
     *
     * @param <V>
     *            The type of result.
     */
    private static final class AccumulatingResultHandler<V> implements ResultHandler<V> {
        /** Guarded by latch. */
        private ResourceException exception;
        private final ResultHandler<List<V>> handler;
        private final AtomicInteger latch;
        private final List<V> results;
        private AccumulatingResultHandler(final int size, final ResultHandler<List<V>> handler) {
            if (size < 0) {
                throw new IllegalStateException();
            }
            this.latch = new AtomicInteger(size);
            this.results = new ArrayList<>(size);
            this.handler = handler;
            if (size == 0) {
                // Invoke immediately.
                handler.handleResult(results);
            }
        }
        @Override
        public void handleError(final ResourceException e) {
            exception = e;
            latch(); // Volatile write publishes exception.
        }
        @Override
        public void handleResult(final V result) {
            if (result != null) {
                synchronized (results) {
                    results.add(result);
                }
            }
            latch();
        }
        private void latch() {
            /*
             * Invoke the handler once all results have been received. Avoid
             * failing-fast when an error occurs because some in-flight tasks
             * may depend on resources (e.g. connections) which are
             * automatically closed on completion.
             */
            if (latch.decrementAndGet() == 0) {
                if (exception != null) {
                    handler.handleError(exception);
                } else {
                    handler.handleResult(results);
                }
            }
        }
    }
    private static final Function<Object, ByteString, NeverThrowsException> BASE64_TO_BYTESTRING =
            new Function<Object, ByteString, NeverThrowsException>() {
@@ -126,30 +64,6 @@
                }
            };
    /**
     * Returns a result handler which can be used to collect the results of
     * {@code size} asynchronous operations. Once all results have been received
     * {@code handler} will be invoked with a list containing the results.
     * Accumulation ignores {@code null} results, so the result list may be
     * smaller than {@code size}. The returned result handler does not
     * fail-fast: it will wait until all results have been received even if an
     * error has been detected. This ensures that asynchronous operations can
     * use resources such as connections which are automatically released
     * (closed) upon completion of the final operation.
     *
     * @param <V>
     *            The type of result to be collected.
     * @param size
     *            The number of expected results.
     * @param handler
     *            The result handler to be invoked when all results have been
     *            received.
     * @return A result handler which can be used to collect the results of
     *         {@code size} asynchronous operations.
     */
    static <V> ResultHandler<V> accumulate(final int size, final ResultHandler<List<V>> handler) {
        return new AccumulatingResultHandler<>(size, handler);
    }
    static Object attributeToJson(final Attribute a) {
        final Function<ByteString, Object, NeverThrowsException> f = byteStringToJson(a.getAttributeDescription());
@@ -294,44 +208,6 @@
        return s != null ? s.toLowerCase(Locale.ENGLISH) : null;
    }
    /**
     * Returns a result handler which accepts results of type {@code M}, applies
     * the function {@code f} in order to convert the result to an object of
     * type {@code N}, and subsequently invokes {@code handler}. If an
     * unexpected error occurs while performing the transformation, the
     * exception is converted to a {@code ResourceException} before invoking
     * {@code handler.handleError()}.
     *
     * @param <M>
     *            The type of result expected by the returned handler.
     * @param <N>
     *            The type of result expected by {@code handler}.
     * @param f
     *            A function which converts the result of type {@code M} to type
     *            {@code N}.
     * @param handler
     *            A result handler which accepts results of type {@code N}.
     * @return A result handler which accepts results of type {@code M}.
     */
    static <M, N> ResultHandler<M> transform(final Function<M, N, NeverThrowsException> f,
            final ResultHandler<N> handler) {
        return new ResultHandler<M>() {
            @Override
            public void handleError(final ResourceException error) {
                handler.handleError(error);
            }
            @Override
            public void handleResult(final M result) {
                try {
                    handler.handleResult(f.apply(result));
                } catch (final Throwable t) {
                    handler.handleError(asResourceException(t));
                }
            }
        };
    }
    private static <T> List<T> asList(final Collection<T> c) {
        if (c instanceof List) {
            return (List<T>) c;
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
@@ -17,10 +17,9 @@
import static java.util.Arrays.asList;
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;
import static org.forgerock.json.fluent.JsonValue.field;
import static org.forgerock.json.fluent.JsonValue.json;
import static org.forgerock.json.fluent.JsonValue.object;
import static org.forgerock.json.JsonValue.field;
import static org.forgerock.json.JsonValue.json;
import static org.forgerock.json.JsonValue.object;
import static org.forgerock.json.resource.PatchOperation.add;
import static org.forgerock.json.resource.PatchOperation.increment;
import static org.forgerock.json.resource.PatchOperation.remove;
@@ -46,22 +45,22 @@
import java.util.LinkedList;
import java.util.List;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.Connection;
import org.forgerock.json.resource.NotFoundException;
import org.forgerock.json.resource.NotSupportedException;
import org.forgerock.json.resource.PreconditionFailedException;
import org.forgerock.json.resource.QueryFilter;
import org.forgerock.json.resource.QueryResult;
import org.forgerock.json.resource.QueryResponse;
import org.forgerock.json.resource.Requests;
import org.forgerock.json.resource.Resource;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.IntermediateResponseHandler;
import org.forgerock.opendj.ldap.LdapResultHandler;
import org.forgerock.opendj.ldap.MemoryBackend;
import org.forgerock.opendj.ldap.RequestContext;
import org.forgerock.opendj.ldap.RequestHandler;
import org.forgerock.opendj.ldap.LdapResultHandler;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.BindRequest;
@@ -79,11 +78,10 @@
import org.forgerock.opendj.ldif.LDIFEntryReader;
import org.forgerock.opendj.rest2ldap.Rest2LDAP.Builder;
import org.forgerock.testng.ForgeRockTestCase;
import org.forgerock.util.query.QueryFilter;
import org.testng.annotations.Test;
/**
 * Tests that CREST requests are correctly mapped to LDAP.
 */
/** Tests that CREST requests are correctly mapped to LDAP. */
@SuppressWarnings({ "javadoc" })
@Test
public final class BasicRequestsTest extends ForgeRockTestCase {
@@ -91,39 +89,38 @@
    // so that we can check that the request handler is returning everything.
    // FIXME: factor out test for re-use as common test suite (e.g. for InMemoryBackend).
    private static final QueryFilter<JsonPointer> NO_FILTER = QueryFilter.alwaysTrue();
    @Test
    public void testQueryAll() throws Exception {
        final Connection connection = newConnection();
        final List<Resource> resources = new LinkedList<>();
        final QueryResult result =
                connection.query(ctx(), Requests.newQueryRequest("").setQueryFilter(
                        QueryFilter.alwaysTrue()), resources);
        final List<ResourceResponse> resources = new LinkedList<>();
        final QueryResponse result = connection.query(
            ctx(), Requests.newQueryRequest("").setQueryFilter(NO_FILTER), resources);
        assertThat(resources).hasSize(5);
        assertThat(result.getPagedResultsCookie()).isNull();
        assertThat(result.getRemainingPagedResults()).isEqualTo(-1);
        assertThat(result.getTotalPagedResults()).isEqualTo(-1);
    }
    @Test
    public void testQueryNone() throws Exception {
        final Connection connection = newConnection();
        final List<Resource> resources = new LinkedList<>();
        final QueryResult result =
                connection.query(ctx(), Requests.newQueryRequest("").setQueryFilter(
                        QueryFilter.alwaysFalse()), resources);
        final List<ResourceResponse> resources = new LinkedList<>();
        final QueryResponse result = connection.query(
            ctx(), Requests.newQueryRequest("").setQueryFilter(QueryFilter.<JsonPointer>alwaysFalse()), resources);
        assertThat(resources).hasSize(0);
        assertThat(result.getPagedResultsCookie()).isNull();
        assertThat(result.getRemainingPagedResults()).isEqualTo(-1);
        assertThat(result.getTotalPagedResults()).isEqualTo(-1);
    }
    @Test
    public void testQueryPageResultsCookie() throws Exception {
        final Connection connection = newConnection();
        final List<Resource> resources = new ArrayList<>();
        final List<ResourceResponse> resources = new ArrayList<>();
        // Read first page.
        QueryResult result =
                connection.query(ctx(), newQueryRequest("")
                        .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2), resources);
        QueryResponse result = connection.query(
                ctx(), newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2), resources);
        assertThat(result.getPagedResultsCookie()).isNotNull();
        assertThat(resources).hasSize(2);
        assertThat(resources.get(0).getId()).isEqualTo("test1");
@@ -133,10 +130,8 @@
        resources.clear();
        // Read second page.
        result =
                connection.query(ctx(), newQueryRequest("")
                        .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2)
                        .setPagedResultsCookie(cookie), resources);
        result = connection.query(ctx(),
                newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources);
        assertThat(result.getPagedResultsCookie()).isNotNull();
        assertThat(resources).hasSize(2);
        assertThat(resources.get(0).getId()).isEqualTo("test3");
@@ -146,10 +141,8 @@
        resources.clear();
        // Read third page.
        result =
                connection.query(ctx(), newQueryRequest("")
                        .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2)
                        .setPagedResultsCookie(cookie), resources);
        result = connection.query(ctx(),
                newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources);
        assertThat(result.getPagedResultsCookie()).isNull();
        assertThat(resources).hasSize(1);
        assertThat(resources.get(0).getId()).isEqualTo("test5");
@@ -158,42 +151,29 @@
    @Test
    public void testQueryPageResultsIndexed() throws Exception {
        final Connection connection = newConnection();
        final List<Resource> resources = new ArrayList<>();
        QueryResult result =
                connection.query(ctx(), newQueryRequest("")
                        .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2)
                        .setPagedResultsOffset(1), resources);
        final List<ResourceResponse> resources = new ArrayList<>();
        QueryResponse result = connection.query(ctx(),
                newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsOffset(1), resources);
        assertThat(result.getPagedResultsCookie()).isNotNull();
        assertThat(resources).hasSize(2);
        assertThat(resources.get(0).getId()).isEqualTo("test3");
        assertThat(resources.get(1).getId()).isEqualTo("test4");
    }
    @Test
    @Test(expectedExceptions = NotFoundException.class)
    public void testDelete() throws Exception {
        final Connection connection = newConnection();
        final Resource resource = connection.delete(ctx(), newDeleteRequest("/test1"));
        final ResourceResponse resource = connection.delete(ctx(), newDeleteRequest("/test1"));
        checkResourcesAreEqual(resource, getTestUser1(12345));
        try {
            connection.read(ctx(), newReadRequest("/test1"));
            fail("Read succeeded unexpectedly");
        } catch (final NotFoundException e) {
            // Expected.
        }
    }
    @Test
    @Test(expectedExceptions = NotFoundException.class)
    public void testDeleteMVCCMatch() throws Exception {
        final Connection connection = newConnection();
        final Resource resource =
                connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12345"));
        final ResourceResponse resource = connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12345"));
        checkResourcesAreEqual(resource, getTestUser1(12345));
        try {
            connection.read(ctx(), newReadRequest("/test1"));
            fail("Read succeeded unexpectedly");
        } catch (final NotFoundException e) {
            // Expected.
        }
    }
    @Test(expectedExceptions = PreconditionFailedException.class)
@@ -211,11 +191,10 @@
    @Test
    public void testPatch() throws Exception {
        final Connection connection = newConnection();
        final Resource resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName",
                        "changed")));
        final ResourceResponse resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")));
        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
    }
@@ -223,7 +202,7 @@
    public void testPatchEmpty() throws Exception {
        final List<Request> requests = new LinkedList<>();
        final Connection connection = newConnection(requests);
        final Resource resource1 = connection.patch(ctx(), newPatchRequest("/test1"));
        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1"));
        checkResourcesAreEqual(resource1, getTestUser1(12345));
        /*
@@ -233,7 +212,7 @@
        assertThat(requests).hasSize(1);
        assertThat(requests.get(0)).isInstanceOf(SearchRequest.class);
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1(12345));
    }
@@ -242,11 +221,11 @@
        final Connection connection = newConnection();
        final JsonValue newContent = getTestUser1(12345);
        newContent.put("description", asList("one", "two"));
        final Resource resource1 =
        final ResourceResponse resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one",
                        "two"))));
        checkResourcesAreEqual(resource1, newContent);
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
@@ -255,29 +234,25 @@
        final Connection connection = newConnection();
        final JsonValue newContent = getTestUser1(12345);
        newContent.put("description", asList("one", "two"));
        final Resource resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", add("/description/-", "one"),
                        add("/description/-", "two")));
        final ResourceResponse resource1 = connection.patch(
            ctx(), newPatchRequest("/test1", add("/description/-", "one"), add("/description/-", "two")));
        checkResourcesAreEqual(resource1, newContent);
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchConstantAttribute() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/test1", add("/schemas", asList("junk"))));
        newConnection().patch(ctx(), newPatchRequest("/test1", add("/schemas", asList("junk"))));
    }
    @Test
    public void testPatchDeleteOptionalAttribute() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(),
                newPatchRequest("/test1", add("/description", asList("one", "two"))));
        final Resource resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", remove("/description")));
        connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", "two"))));
        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1", remove("/description")));
        checkResourcesAreEqual(resource1, getTestUser1(12345));
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1(12345));
    }
@@ -288,33 +263,31 @@
        newContent.put("singleNumber", 100);
        newContent.put("multiNumber", asList(200, 300));
        final Resource resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", add("/singleNumber", 0), add(
                        "/multiNumber", asList(100, 200)), increment("/singleNumber", 100),
        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1",
            add("/singleNumber", 0),
            add("/multiNumber", asList(100, 200)),
            increment("/singleNumber", 100),
                        increment("/multiNumber", 100)));
        checkResourcesAreEqual(resource1, newContent);
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchMissingRequiredAttribute() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/test1", remove("/name/surname")));
        newConnection().patch(ctx(), newPatchRequest("/test1", remove("/name/surname")));
    }
    @Test
    public void testPatchModifyOptionalAttribute() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(),
                newPatchRequest("/test1", add("/description", asList("one", "two"))));
        final Resource resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", add("/description",
                        asList("three"))));
        connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", "two"))));
        final ResourceResponse resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("three"))));
        final JsonValue newContent = getTestUser1(12345);
        newContent.put("description", asList("one", "two", "three"));
        checkResourcesAreEqual(resource1, newContent);
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
@@ -340,63 +313,62 @@
    @Test
    public void testPatchMVCCMatch() throws Exception {
        final Connection connection = newConnection();
        final Resource resource1 =
                connection.patch(ctx(), newPatchRequest("/test1",
                        add("/name/displayName", "changed")).setRevision("12345"));
        final ResourceResponse resource1 = connection.patch(
            ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12345"));
        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
    }
    @Test(expectedExceptions = PreconditionFailedException.class)
    public void testPatchMVCCNoMatch() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed"))
                .setRevision("12346"));
        connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12346"));
    }
    @Test(expectedExceptions = NotFoundException.class)
    public void testPatchNotFound() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/missing", add("/name/displayName", "changed")));
        newConnection().patch(ctx(), newPatchRequest("/missing", add("/name/displayName", "changed")));
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchReadOnlyAttribute() throws Exception {
        final Connection connection = newConnection();
        // Etag is read-only.
        connection.patch(ctx(), newPatchRequest("/test1", add("_rev", "99999")));
        newConnection().patch(ctx(), newPatchRequest("/test1", add("_rev", "99999")));
    }
    @Test
    public void testPatchReplacePartialObject() throws Exception {
        final Connection connection = newConnection();
        final JsonValue expected =
                json(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id",
                        "test1"), field("_rev", "12345"), field("name", object(field("displayName",
                        "Humpty"), field("surname", "Dumpty")))));
        final Resource resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", replace("/name", object(field(
                        "displayName", "Humpty"), field("surname", "Dumpty")))));
        final JsonValue expected = json(object(
            field("schemas", asList("urn:scim:schemas:core:1.0")),
            field("_id", "test1"),
            field("_rev", "12345"),
            field("name", object(field("displayName", "Humpty"),
                                 field("surname", "Dumpty")))));
        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1",
            replace("/name", object(field("displayName", "Humpty"), field("surname", "Dumpty")))));
        checkResourcesAreEqual(resource1, expected);
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, expected);
    }
    @Test
    public void testPatchReplaceWholeObject() throws Exception {
        final Connection connection = newConnection();
        final JsonValue newContent =
                json(object(field("name", object(field("displayName", "Humpty"), field("surname",
                        "Dumpty")))));
        final JsonValue expected =
                json(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id",
                        "test1"), field("_rev", "12345"), field("name", object(field("displayName",
                        "Humpty"), field("surname", "Dumpty")))));
        final Resource resource1 =
        final JsonValue newContent = json(object(
            field("name", object(field("displayName", "Humpty"),
                                 field("surname", "Dumpty")))));
        final JsonValue expected = json(object(
            field("schemas", asList("urn:scim:schemas:core:1.0")),
            field("_id", "test1"),
            field("_rev", "12345"),
            field("name", object(field("displayName", "Humpty"),
                                 field("surname", "Dumpty")))));
        final ResourceResponse resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", replace("/", newContent)));
        checkResourcesAreEqual(resource1, expected);
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, expected);
    }
@@ -439,7 +411,7 @@
    @Test
    public void testRead() throws Exception {
        final Resource resource = newConnection().read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource = newConnection().read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource, getTestUser1(12345));
    }
@@ -450,15 +422,14 @@
    @Test
    public void testReadSelectAllFields() throws Exception {
        final Resource resource =
                newConnection().read(ctx(), newReadRequest("/test1").addField("/"));
        final ResourceResponse resource = newConnection().read(ctx(), newReadRequest("/test1").addField("/"));
        checkResourcesAreEqual(resource, getTestUser1(12345));
    }
    @Test
    public void testReadSelectPartial() throws Exception {
        final Resource resource =
                newConnection().read(ctx(), newReadRequest("/test1").addField("/name/surname"));
        final ResourceResponse resource = newConnection().read(
            ctx(), newReadRequest("/test1").addField("/name/surname"));
        assertThat(resource.getId()).isEqualTo("test1");
        assertThat(resource.getRevision()).isEqualTo("12345");
        assertThat(resource.getContent().get("_id").asString()).isNull();
@@ -470,8 +441,8 @@
    /** Disabled - see CREST-86 (Should JSON resource fields be case insensitive?) */
    @Test(enabled = false)
    public void testReadSelectPartialInsensitive() throws Exception {
        final Resource resource =
                newConnection().read(ctx(), newReadRequest("/test1").addField("/name/SURNAME"));
        final ResourceResponse resource = newConnection().read(
            ctx(), newReadRequest("/test1").addField("/name/SURNAME"));
        assertThat(resource.getId()).isEqualTo("test1");
        assertThat(resource.getRevision()).isEqualTo("12345");
        assertThat(resource.getContent().get("_id").asString()).isNull();
@@ -483,10 +454,10 @@
    @Test
    public void testUpdate() throws Exception {
        final Connection connection = newConnection();
        final Resource resource1 =
                connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)));
        final ResourceResponse resource1 = connection.update(
            ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)));
        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
    }
@@ -494,18 +465,15 @@
    public void testUpdateNoChange() throws Exception {
        final List<Request> requests = new LinkedList<>();
        final Connection connection = newConnection(requests);
        final Resource resource1 =
                connection.update(ctx(), newUpdateRequest("/test1", getTestUser1(12345)));
        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", getTestUser1(12345)));
        /*
         * Check that no modify operation was sent (only a single search should
         * be sent in order to get the current resource).
         */
        // Check that no modify operation was sent
        // (only a single search should be sent in order to get the current resource).
        assertThat(requests).hasSize(1);
        assertThat(requests.get(0)).isInstanceOf(SearchRequest.class);
        checkResourcesAreEqual(resource1, getTestUser1(12345));
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1(12345));
    }
@@ -514,9 +482,9 @@
        final Connection connection = newConnection();
        final JsonValue newContent = getTestUser1Updated(12345);
        newContent.put("description", asList("one", "two"));
        final Resource resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
        checkResourcesAreEqual(resource1, newContent);
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
@@ -535,9 +503,9 @@
        newContent.put("description", asList("one", "two"));
        connection.update(ctx(), newUpdateRequest("/test1", newContent));
        newContent.remove("description");
        final Resource resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
        checkResourcesAreEqual(resource1, newContent);
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
@@ -556,20 +524,19 @@
        newContent.put("description", asList("one", "two"));
        connection.update(ctx(), newUpdateRequest("/test1", newContent));
        newContent.put("description", asList("three"));
        final Resource resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
        checkResourcesAreEqual(resource1, newContent);
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
    @Test
    public void testUpdateMVCCMatch() throws Exception {
        final Connection connection = newConnection();
        final Resource resource1 =
                connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345))
                        .setRevision("12345"));
        final ResourceResponse resource1 =
                connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12345"));
        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
    }
@@ -643,8 +610,8 @@
                        .attribute("multiNumber", simple("multiNumber").decoder(byteStringToInteger())));
    }
    private void checkResourcesAreEqual(final Resource actual, final JsonValue expected) {
        final Resource expectedResource = asResource(expected);
    private void checkResourcesAreEqual(final ResourceResponse actual, final JsonValue expected) {
        final ResourceResponse expectedResource = asResource(expected);
        assertThat(actual.getId()).isEqualTo(expectedResource.getId());
        assertThat(actual.getRevision()).isEqualTo(expectedResource.getRevision());
        assertThat(actual.getContent().getObject()).isEqualTo(
@@ -790,14 +757,20 @@
    }
    private JsonValue getTestUser1(final int rev) {
        return content(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id",
                "test1"), field("_rev", String.valueOf(rev)), field("name", object(field(
                "displayName", "test user 1"), field("surname", "user 1")))));
        return content(object(
                field("schemas", asList("urn:scim:schemas:core:1.0")),
                field("_id", "test1"),
                field("_rev", String.valueOf(rev)),
                field("name", object(field("displayName", "test user 1"),
                                     field("surname", "user 1")))));
    }
    private JsonValue getTestUser1Updated(final int rev) {
        return content(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id",
                "test1"), field("_rev", String.valueOf(rev)), field("name", object(field(
                "displayName", "changed"), field("surname", "user 1")))));
        return content(object(
                field("schemas", asList("urn:scim:schemas:core:1.0")),
                field("_id", "test1"),
                field("_rev", String.valueOf(rev)),
                field("name", object(field("displayName", "changed"),
                                     field("surname", "user 1")))));
    }
}
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java
@@ -15,20 +15,18 @@
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.json.fluent.JsonValue.json;
import static org.forgerock.json.JsonValue.json;
import java.util.ArrayList;
import java.util.List;
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.Resource;
import org.forgerock.json.resource.RootContext;
import org.forgerock.http.context.RootContext;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.Responses;
/**
 * Unit test utility methods, including fluent methods for creating JSON
 * objects.
 */
/** Unit test utility methods, including fluent methods for creating JSON objects. */
public final class TestUtils {
    /**
@@ -39,8 +37,8 @@
     *            The JSON content.
     * @return A {@code Resource} containing the provided JSON content.
     */
    public static Resource asResource(final JsonValue content) {
        return new Resource(content.get("_id").asString(), content.get("_rev").asString(), content);
    public static ResourceResponse asResource(final JsonValue content) {
        return Responses.newResourceResponse(content.get("_id").asString(), content.get("_rev").asString(), content);
    }
    /**
opendj-server-legacy/pom.xml
@@ -89,7 +89,7 @@
    <!-- ForgeRock libraries -->
    <dependency>
      <groupId>org.forgerock.opendj</groupId>
      <artifactId>opendj-rest2ldap-servlet</artifactId>
      <artifactId>opendj-rest2ldap</artifactId>
      <version>${project.version}</version>
    </dependency>
    <dependency>
@@ -130,6 +130,28 @@
      <artifactId>slf4j-jdk14</artifactId>
    </dependency>
    <dependency>
      <groupId>org.forgerock.commons</groupId>
      <artifactId>json-resource</artifactId>
      <version>${forgerockRestVersion}</version>
    </dependency>
    <dependency>
      <groupId>org.forgerock.commons</groupId>
      <artifactId>json-resource-http</artifactId>
      <version>${forgerockRestVersion}</version>
    </dependency>
    <dependency>
      <groupId>org.forgerock.http</groupId>
      <artifactId>chf-http-core</artifactId>
      <version>${forgerockHttpVersion}</version>
    </dependency>
    <dependency>
      <groupId>org.forgerock.http</groupId>
      <artifactId>chf-http-servlet</artifactId>
      <version>${forgerockHttpVersion}</version>
    </dependency>
    <!-- servlet and mail -->
    <dependency>
      <groupId>javax.servlet</groupId>
opendj-server-legacy/src/main/assembly/opendj-archive-component.xml
@@ -35,7 +35,6 @@
      <outputFileNameMapping>${artifact.artifactId}.${artifact.extension}</outputFileNameMapping>
      <excludes>
        <exclude>javax.activation:activation</exclude>
        <exclude>org.forgerock.commons:json-resource-servlet:war</exclude>
        <exclude>org.forgerock.opendj:opendj-server-legacy</exclude>
      </excludes>
    </dependencySet>
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
@@ -25,6 +25,7 @@
 */
package org.opends.server.protocols.http;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -32,68 +33,45 @@
import java.text.ParseException;
import java.util.Collection;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.forgerock.http.Context;
import org.forgerock.http.Handler;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.http.protocol.Status;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.opendj.ldap.*;
import org.forgerock.opendj.adapter.server3x.Adapters;
import org.forgerock.opendj.ldap.AddressMask;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.responses.BindResult;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
import org.forgerock.opendj.rest2ldap.Rest2LDAP;
import org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory;
import org.forgerock.util.AsyncFunction;
import org.forgerock.util.promise.ExceptionHandler;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.ResultHandler;
import org.forgerock.util.promise.Promises;
import org.opends.server.admin.std.server.ConnectionHandlerCfg;
import org.opends.server.schema.SchemaConstants;
import org.opends.server.types.DisconnectReason;
import org.opends.server.util.Base64;
import static org.forgerock.opendj.adapter.server3x.Adapters.*;
import static org.forgerock.opendj.ldap.LdapException.*;
import static org.forgerock.util.promise.Promises.*;
import static org.opends.messages.ProtocolMessages.*;
import static org.opends.server.loggers.AccessLogger.*;
import static org.opends.server.util.StaticUtils.*;
/**
 * Servlet {@link Filter} that collects information about client connections.
 */
final class CollectClientConnectionsFilter implements javax.servlet.Filter
/** Servlet {@link Filter} that collects information about client connections. */
final class CollectClientConnectionsFilter implements org.forgerock.http.Filter, Closeable
{
  /** This class holds all the necessary data to complete an HTTP request. */
  private static final class HTTPRequestContext
  {
    private AsyncContext asyncContext;
    private HttpServletRequest request;
    private HttpServletResponse response;
    private FilterChain chain;
    private HTTPClientConnection clientConnection;
    private Connection connection;
    /** Whether to pretty print the resulting JSON. */
    private boolean prettyPrint;
    /** Used for the bind request when credentials are specified. */
    private String userName;
    /**
     * Used for the bind request when credentials are specified. For security
     * reasons, the password must be discarded as soon as possible after it's
     * been used.
     */
    private String password;
  }
  /** HTTP Header sent by the client with HTTP basic authentication. */
  static final String HTTP_BASIC_AUTH_HEADER = "Authorization";
@@ -118,125 +96,165 @@
   *          authentication
   */
  public CollectClientConnectionsFilter(
      HTTPConnectionHandler connectionHandler,
      HTTPAuthenticationConfig authenticationConfig)
      HTTPConnectionHandler connectionHandler, HTTPAuthenticationConfig authenticationConfig)
  {
    this.connectionHandler = connectionHandler;
    this.authConfig = authenticationConfig;
  }
  /** {@inheritDoc} */
  @Override
  public void init(FilterConfig filterConfig) throws ServletException
  public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next)
  {
    // nothing to do
  }
    final HTTPClientConnection clientConnection = new HTTPClientConnection(this.connectionHandler, context, request);
    connectionHandler.addClientConnection(clientConnection);
  /** {@inheritDoc} */
  @Override
  public void doFilter(ServletRequest req, ServletResponse resp,
      FilterChain chain)
    if (connectionHandler.keepStats())
  {
    final HttpServletRequest request = (HttpServletRequest) req;
    final HttpServletResponse response = (HttpServletResponse) resp;
    final HTTPRequestContext ctx = new HTTPRequestContext();
    ctx.request = request;
    ctx.response = new HttpServletResponseWrapper(response)
    {
      /** {@inheritDoc} */
      @Override
      public void setStatus(int sc)
      {
        ctx.clientConnection.log(sc);
        super.setStatus(sc);
      }
      /** {@inheritDoc} */
      @SuppressWarnings("deprecation")
      @Override
      public void setStatus(int sc, String sm)
      {
        ctx.clientConnection.log(sc);
        super.setStatus(sc, sm);
      }
    };
    ctx.chain = chain;
    ctx.prettyPrint =
        Boolean.parseBoolean(request.getParameter("_prettyPrint"));
    final HTTPClientConnection clientConnection =
        new HTTPClientConnection(this.connectionHandler, request);
    this.connectionHandler.addClientConnection(clientConnection);
    ctx.clientConnection = clientConnection;
    if (this.connectionHandler.keepStats()) {
      this.connectionHandler.getStatTracker().addRequest(
          ctx.clientConnection.getMethod());
      connectionHandler.getStatTracker().addRequest(request.getMethod());
    }
    try
    {
      if (!canProcessRequest(request, clientConnection))
      if (!canProcessRequest(clientConnection))
      {
        return;
        return resourceExceptionToPromise(ResourceException.getException(ResourceException.INTERNAL_ERROR));
      }
      // logs the connect after all the possible disconnect reasons have been
      // checked.
      // Logs the connect after all the possible disconnect reasons have been checked.
      logConnect(clientConnection);
      ctx.connection = new SdkConnectionAdapter(clientConnection);
      final Connection connection = new SdkConnectionAdapter(clientConnection);
      final String[] userCredentials = extractUsernamePassword(request);
      if (userCredentials != null && userCredentials.length == 2)
      {
        ctx.userName = userCredentials[0];
        ctx.password = userCredentials[1];
        ctx.asyncContext = getAsyncContext(request);
        final String userName = userCredentials[0];
        final String password = userCredentials[1];
        newRootConnection().searchSingleEntryAsync(buildSearchRequest(ctx.userName)).thenAsync(
            new AsyncFunction<SearchResultEntry, BindResult, LdapException>() {
        return Adapters.newRootConnection()
            .searchSingleEntryAsync(buildSearchRequest(userName))
            .thenAsync(doBindAfterSearch(context, request, next, userName, password, clientConnection, connection),
                       returnErrorAfterFailedSearch(clientConnection));
      }
      else if (this.connectionHandler.acceptUnauthenticatedRequests())
      {
        // Use unauthenticated user
        return doFilter(context, request, next, connection);
      }
      else
      {
        return authenticationFailure(clientConnection);
      }
    }
    catch (Exception e)
    {
      return asErrorResponse(e, clientConnection);
    }
  }
  private boolean canProcessRequest(final HTTPClientConnection connection) throws UnknownHostException
  {
    final InetAddress clientAddr = connection.getRemoteAddress();
    // Check to see if the core server rejected the connection (e.g. already too many connections established).
    if (connection.getConnectionID() < 0)
    {
      connection.disconnect(
          DisconnectReason.ADMIN_LIMIT_EXCEEDED, true, ERR_CONNHANDLER_REJECTED_BY_SERVER.get());
      return false;
    }
    // Check to see if the client is on the denied list. If so, then reject it immediately.
    final ConnectionHandlerCfg config = this.connectionHandler.getCurrentConfig();
    final Collection<AddressMask> deniedClients = config.getDeniedClient();
    if (!deniedClients.isEmpty()
        && AddressMask.matchesAny(deniedClients, clientAddr))
    {
      connection.disconnect(DisconnectReason.CONNECTION_REJECTED, false,
          ERR_CONNHANDLER_DENIED_CLIENT.get(connection.getClientHostPort(), connection.getServerHostPort()));
      return false;
    }
    // Check to see if there is an allowed list and if there is whether the client is on that list.
    // If not, then reject the connection.
    final Collection<AddressMask> allowedClients = config.getAllowedClient();
    if (!allowedClients.isEmpty()
        && !AddressMask.matchesAny(allowedClients, clientAddr))
    {
      connection.disconnect(DisconnectReason.CONNECTION_REJECTED, false,
          ERR_CONNHANDLER_DISALLOWED_CLIENT.get(connection.getClientHostPort(), connection.getServerHostPort()));
      return false;
    }
    return true;
  }
  private SearchRequest buildSearchRequest(String userName)
  {
    // Use configured rights to find the user DN
    final Filter filter = Filter.format(authConfig.getSearchFilterTemplate(), userName);
    return Requests.newSearchRequest(
        authConfig.getSearchBaseDN(), authConfig.getSearchScope(), filter, SchemaConstants.NO_ATTRIBUTES);
  }
  private AsyncFunction<SearchResultEntry, Response, NeverThrowsException> doBindAfterSearch(
      final Context context, final Request request, final Handler next, final String userName, final String password,
      final HTTPClientConnection clientConnection, final Connection connection)
  {
    return new AsyncFunction<SearchResultEntry, Response, NeverThrowsException>()
    {
              @Override
              public Promise<BindResult, LdapException> apply(SearchResultEntry resultEntry) throws LdapException
      public Promise<Response, NeverThrowsException> apply(final SearchResultEntry resultEntry)
              {
                final DN bindDN = resultEntry.getName();
                if (bindDN == null)
                {
                  sendAuthenticationFailure(ctx);
                  return newExceptionPromise(newLdapException(ResultCode.CANCELLED));
                }
                else
                {
                  final BindRequest bindRequest =
                      Requests.newSimpleBindRequest(bindDN.toString(), ctx.password.getBytes(Charset.forName("UTF-8")));
                  // We are done with the password at this stage,
                  // wipe it from memory for security reasons
                  ctx.password = null;
                  return ctx.connection.bindAsync(bindRequest);
                }
          return authenticationFailure(clientConnection);
              }
        final BindRequest bindRequest =
            Requests.newSimpleBindRequest(bindDN.toString(), password.getBytes(Charset.forName("UTF-8")));
        return connection.bindAsync(bindRequest)
                         .thenAsync(doChain(context, request, next, userName, clientConnection, connection),
                                    returnErrorAfterFailedBind(clientConnection));
            }
        ).thenOnResult(new ResultHandler<BindResult>() {
          @Override
          public void handleResult(BindResult result)
    };
  }
  private AsyncFunction<BindResult, Response, NeverThrowsException> doChain(
      final Context context, final Request request, final Handler next, final String userName,
      final HTTPClientConnection clientConnection, final Connection connection)
          {
            ctx.clientConnection.setAuthUser(ctx.userName);
    return new AsyncFunction<BindResult, Response, NeverThrowsException>()
    {
      @Override
      public Promise<Response, NeverThrowsException> apply(BindResult value) throws NeverThrowsException
      {
        clientConnection.setAuthUser(userName);
            try
            {
              doFilter(ctx);
          return doFilter(context, request, next, connection);
            }
            catch (Exception e)
            {
              onException(e, ctx);
          return asErrorResponse(e, clientConnection);
            }
          }
        }).thenOnException(new ExceptionHandler<LdapException>(){
    };
  }
  private Promise<Response, NeverThrowsException> doFilter(
      final Context context, final Request request, final Handler next, final Connection connection) throws Exception
  {
    final Context forwardedContext = new AuthenticatedConnectionContext(context, connection);
    // Send the request further down the filter chain or pass to servlet
    return next.handle(forwardedContext, request);
  }
  private AsyncFunction<? super LdapException, Response, NeverThrowsException> returnErrorAfterFailedSearch(
      final HTTPClientConnection clientConnection)
  {
    return new AsyncFunction<LdapException, Response, NeverThrowsException>()
    {
          @Override
          public void handleException(LdapException exception)
      public Promise<Response, NeverThrowsException> apply(final LdapException exception)
          {
            final ResultCode rc = exception.getResult().getResultCode();
            if (ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED.equals(rc)
@@ -244,141 +262,74 @@
            {
              // Avoid information leak:
              // do not hint to the user that it is the username that is invalid
              sendAuthenticationFailure(ctx);
          return authenticationFailure(clientConnection);
            }
            else
            {
              onException(exception, ctx);
          return asErrorResponse(exception, clientConnection);
            }
          }
        });
      }
      else if (this.connectionHandler.acceptUnauthenticatedRequests())
      {
        // use unauthenticated user
        doFilter(ctx);
      }
      else
      {
        sendAuthenticationFailure(ctx);
      }
    }
    catch (Exception e)
    {
      onException(e, ctx);
    }
    };
  }
  private void doFilter(HTTPRequestContext ctx)
      throws Exception
  private AsyncFunction<LdapException, Response, NeverThrowsException> returnErrorAfterFailedBind(
      final HTTPClientConnection clientConnection)
  {
    /*
     * WARNING: This action triggers 3-4 others: Set the connection for use with
     * this request on the HttpServletRequest. It will make
     * Rest2LDAPContextFactory create an AuthenticatedConnectionContext which
     * will in turn ensure Rest2LDAP uses the supplied Connection object.
     */
    ctx.request.setAttribute(
        Rest2LDAPContextFactory.ATTRIBUTE_AUTHN_CONNECTION, ctx.connection);
    // send the request further down the filter chain or pass to servlet
    ctx.chain.doFilter(ctx.request, ctx.response);
    return new AsyncFunction<LdapException, Response, NeverThrowsException>()
    {
      @Override
      public Promise<Response, NeverThrowsException> apply(final LdapException e)
      {
        return asErrorResponse(e, clientConnection);
      }
    };
  }
  private void sendAuthenticationFailure(HTTPRequestContext ctx)
  private Promise<Response, NeverThrowsException> authenticationFailure(final HTTPClientConnection clientConnection)
  {
    final int statusCode = HttpServletResponse.SC_UNAUTHORIZED;
    return asErrorResponse(ResourceException.getException(401, "Invalid Credentials"), clientConnection,
        DisconnectReason.INVALID_CREDENTIALS, false);
  }
  private Promise<Response, NeverThrowsException> asErrorResponse(
      final Throwable t, final HTTPClientConnection clientConnection)
  {
    return asErrorResponse(t, clientConnection, DisconnectReason.SERVER_ERROR, true);
  }
  private Promise<Response, NeverThrowsException> asErrorResponse(final Throwable t,
      final HTTPClientConnection clientConnection, final DisconnectReason reason, final boolean logError)
  {
    final ResourceException ex = Rest2LDAP.asResourceException(t);
    try
    {
      // The user could not be authenticated. Send an HTTP Basic authentication
      // challenge if HTTP Basic authentication is enabled.
      ResourceException unauthorizedException =
          ResourceException.getException(statusCode, "Invalid Credentials");
      sendErrorReponse(ctx.response, ctx.prettyPrint, unauthorizedException);
      ctx.clientConnection.disconnect(DisconnectReason.INVALID_CREDENTIALS,
          false, null);
    }
    finally
      LocalizableMessage message = null;
      if (logError)
    {
      ctx.clientConnection.log(statusCode);
      if (ctx.asyncContext != null)
      {
        ctx.asyncContext.complete();
      }
    }
  }
  private void onException(Exception e, HTTPRequestContext ctx)
  {
    ResourceException ex = Rest2LDAP.asResourceException(e);
    try
    {
      logger.traceException(e);
      sendErrorReponse(ctx.response, ctx.prettyPrint, ex);
      LocalizableMessage message =
          INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(ctx.clientConnection
              .getClientHostPort(), ctx.clientConnection.getServerHostPort(),
              getExceptionMessage(e));
        logger.traceException(ex);
        message = INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(
            clientConnection.getClientHostPort(), clientConnection.getServerHostPort(), getExceptionMessage(ex));
      logger.debug(message);
      ctx.clientConnection.disconnect(DisconnectReason.SERVER_ERROR, false,
          message);
      }
      clientConnection.disconnect(reason, false, message);
    }
    finally
    {
      ctx.clientConnection.log(ex.getCode());
      if (ctx.asyncContext != null)
      {
        ctx.asyncContext.complete();
      }
    }
      clientConnection.log(ex.getCode());
  }
  private boolean canProcessRequest(HttpServletRequest request,
      final HTTPClientConnection clientConnection) throws UnknownHostException
  {
    InetAddress clientAddr = InetAddress.getByName(request.getRemoteAddr());
    // Check to see if the core server rejected the
    // connection (e.g., already too many connections
    // established).
    if (clientConnection.getConnectionID() < 0)
    {
      clientConnection.disconnect(DisconnectReason.ADMIN_LIMIT_EXCEEDED, true,
          ERR_CONNHANDLER_REJECTED_BY_SERVER.get());
      return false;
    return resourceExceptionToPromise(ex);
    }
    // Check to see if the client is on the denied list.
    // If so, then reject it immediately.
    ConnectionHandlerCfg config = this.connectionHandler.getCurrentConfig();
    Collection<AddressMask> allowedClients = config.getAllowedClient();
    Collection<AddressMask> deniedClients = config.getDeniedClient();
    if (!deniedClients.isEmpty()
        && AddressMask.matchesAny(deniedClients, clientAddr))
  Promise<Response, NeverThrowsException> resourceExceptionToPromise(final ResourceException e)
    {
      clientConnection.disconnect(DisconnectReason.CONNECTION_REJECTED, false,
          ERR_CONNHANDLER_DENIED_CLIENT.get(clientConnection
              .getClientHostPort(), clientConnection.getServerHostPort()));
      return false;
    }
    // Check to see if there is an allowed list and if
    // there is whether the client is on that list. If
    // not, then reject the connection.
    if (!allowedClients.isEmpty()
        && !AddressMask.matchesAny(allowedClients, clientAddr))
    final Response response = new Response().setStatus(Status.valueOf(e.getCode()))
                                            .setEntity(e.toJsonValue().getObject());
    if (e.getCode() == 401 && authConfig.isBasicAuthenticationSupported())
    {
      clientConnection.disconnect(DisconnectReason.CONNECTION_REJECTED, false,
          ERR_CONNHANDLER_DISALLOWED_CLIENT.get(clientConnection
              .getClientHostPort(), clientConnection.getServerHostPort()));
      return false;
      response.getHeaders().add("WWW-Authenticate", "Basic realm=\"org.forgerock.opendj\"");
    }
    return true;
    return Promises.newResultPromise(response);
  }
  /**
@@ -395,8 +346,7 @@
   * @throws ResourceException
   *           if any error occur
   */
  String[] extractUsernamePassword(HttpServletRequest request)
      throws ResourceException
  String[] extractUsernamePassword(Request request) throws ResourceException
  {
    // TODO Use session to reduce hits with search + bind?
    // Use proxied authorization control for session.
@@ -404,10 +354,8 @@
    // Security: How can we remove the password held in the request headers?
    if (authConfig.isCustomHeadersAuthenticationSupported())
    {
      final String userName =
          request.getHeader(authConfig.getCustomHeaderUsername());
      final String password =
          request.getHeader(authConfig.getCustomHeaderPassword());
      final String userName = request.getHeaders().getFirst(authConfig.getCustomHeaderUsername());
      final String password = request.getHeaders().getFirst(authConfig.getCustomHeaderPassword());
      if (userName != null && password != null)
      {
        return new String[] { userName, password };
@@ -416,7 +364,7 @@
    if (authConfig.isBasicAuthenticationSupported())
    {
      String httpBasicAuthHeader = request.getHeader(HTTP_BASIC_AUTH_HEADER);
      String httpBasicAuthHeader = request.getHeaders().getFirst(HTTP_BASIC_AUTH_HEADER);
      if (httpBasicAuthHeader != null)
      {
        String[] userCredentials = parseUsernamePassword(httpBasicAuthHeader);
@@ -431,77 +379,6 @@
  }
  /**
   * Sends an error response back to the client. If the error response is
   * "Unauthorized", then it will send a challenge for HTTP Basic authentication
   * if HTTP Basic authentication is enabled.
   *
   * @param response
   *          where to send the Unauthorized status code.
   * @param prettyPrint
   *          whether to format the JSON document output
   * @param re
   *          the resource exception with the error response content
   */
  void sendErrorReponse(HttpServletResponse response, boolean prettyPrint,
      ResourceException re)
  {
    response.setStatus(re.getCode());
    if (re.getCode() == HttpServletResponse.SC_UNAUTHORIZED
        && authConfig.isBasicAuthenticationSupported())
    {
      response.setHeader("WWW-Authenticate",
          "Basic realm=\"org.forgerock.opendj\"");
    }
    try
    {
      // Send error JSON document out
      response.setHeader("Content-Type", "application/json");
      response.getOutputStream().println(toJSON(prettyPrint, re));
    }
    catch (IOException ignore)
    {
      // nothing else we can do in this case
      logger.traceException(ignore);
    }
  }
  /**
   * Returns a JSON representation of the {@link ResourceException}.
   *
   * @param prettyPrint
   *          whether to format the resulting JSON document
   * @param re
   *          the resource exception to convert to a JSON document
   * @return a String containing the JSON representation of the
   *         {@link ResourceException}.
   */
  private String toJSON(boolean prettyPrint, ResourceException re)
  {
    final String indent = "\n    ";
    final StringBuilder sb = new StringBuilder();
    sb.append("{");
    if (prettyPrint) {
      sb.append(indent);
    }
    sb.append("\"code\": ").append(re.getCode()).append(",");
    if (prettyPrint) {
      sb.append(indent);
    }
    sb.append("\"message\": \"").append(re.getMessage()).append("\",");
    if (prettyPrint) {
      sb.append(indent);
    }
    sb.append("\"reason\": \"").append(re.getReason()).append("\"");
    if (prettyPrint) {
      sb.append("\n");
    }
    sb.append("}");
    return sb.toString();
  }
  /**
   * Parses username and password from the authentication header used in HTTP
   * basic authentication.
   *
@@ -520,12 +397,12 @@
      // We received authentication info
      // Example received header:
      // "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
      String base64UserPassword = authHeader.substring("basic".length() + 1);
      String base64UserCredentials = authHeader.substring("basic".length() + 1);
      try
      {
        // Example usage of base64:
        // Base64("Aladdin:open sesame") = "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
        String userCredentials = new String(Base64.decode(base64UserPassword));
        String userCredentials = new String(Base64.decode(base64UserCredentials));
        String[] split = userCredentials.split(":");
        if (split.length == 2)
        {
@@ -540,25 +417,6 @@
    return null;
  }
  private AsyncContext getAsyncContext(ServletRequest request)
  {
    return request.isAsyncStarted() ? request.getAsyncContext() : request
        .startAsync();
  }
  private SearchRequest buildSearchRequest(String userName)
  {
    // use configured rights to find the user DN
    final Filter filter =
        Filter.format(authConfig.getSearchFilterTemplate(), userName);
    return Requests.newSearchRequest(authConfig.getSearchBaseDN(), authConfig
        .getSearchScope(), filter, SchemaConstants.NO_ATTRIBUTES);
  }
  /** {@inheritDoc} */
  @Override
  public void destroy()
  {
    // nothing to do
  }
  public void close() throws IOException {}
}
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPClientConnection.java
@@ -25,6 +25,12 @@
 */
package org.opends.server.protocols.http;
import static org.forgerock.opendj.adapter.server3x.Converters.from;
import static org.forgerock.opendj.adapter.server3x.Converters.getResponseResult;
import static org.forgerock.opendj.ldap.LdapException.newLdapException;
import static org.opends.messages.ProtocolMessages.WARN_CLIENT_DISCONNECT_IN_PROGRESS;
import static org.opends.server.loggers.AccessLogger.logDisconnect;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
@@ -34,16 +40,19 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import javax.servlet.http.HttpServletRequest;
import org.forgerock.http.Context;
import org.forgerock.http.MutableUri;
import org.forgerock.http.context.ClientContext;
import org.forgerock.http.context.AttributesContext;
import org.forgerock.http.protocol.Request;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.spi.LdapPromiseImpl;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldap.spi.LdapPromiseImpl;
import org.opends.server.api.ClientConnection;
import org.opends.server.core.AddOperation;
import org.opends.server.core.BindOperation;
@@ -79,11 +88,6 @@
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchResultReference;
import static org.forgerock.opendj.adapter.server3x.Converters.*;
import static org.forgerock.opendj.ldap.LdapException.*;
import static org.opends.messages.ProtocolMessages.*;
import static org.opends.server.loggers.AccessLogger.*;
/**
 * This class defines an HTTP client connection, which is a type of client
 * connection that will be accepted by an instance of the HTTP connection
@@ -234,7 +238,7 @@
  private final InetAddress localAddress;
  /** Whether this connection is secure. */
  private final boolean isSecure;
  private boolean isSecure;
  /** Security-Strength Factor extracted from the request attribute. */
  private final int securityStrengthFactor;
@@ -244,26 +248,29 @@
   *
   * @param connectionHandler
   *          the connection handler that accepted this connection
   * @param request
   *          represents this client connection.
   * @param context
   *          represents the context of this client connection.
   */
  public HTTPClientConnection(HTTPConnectionHandler connectionHandler, HttpServletRequest request)
  public HTTPClientConnection(HTTPConnectionHandler connectionHandler, Context context, Request request)
  {
    this.connectionHandler = connectionHandler;
    final ClientContext clientCtx = context.asContext(ClientContext.class);
    // Memorize all the fields we need from the request before Grizzly decides to recycle it
    this.clientAddress = request.getRemoteAddr();
    this.clientPort = request.getRemotePort();
    this.serverAddress = request.getLocalAddr();
    this.serverPort = request.getLocalPort();
    this.remoteAddress = toInetAddress(request.getRemoteAddr());
    this.localAddress = toInetAddress(request.getLocalAddr());
    this.isSecure = request.isSecure();
    this.securityStrengthFactor = calcSSF(request.getAttribute(SERVLET_SSF_CONSTANT));
    this.clientAddress = clientCtx.getRemoteAddress();
    this.remoteAddress = toInetAddress(clientAddress);
    this.clientPort = clientCtx.getRemotePort();
    this.isSecure = clientCtx.isSecure();
    final MutableUri uri = request.getUri();
    this.serverAddress = uri.getHost();
    this.localAddress = toInetAddress(serverAddress);
    this.serverPort = uri.getPort();
    this.securityStrengthFactor = calcSSF(
            context.asContext(AttributesContext.class).getAttributes().get(SERVLET_SSF_CONSTANT));
    this.method = request.getMethod();
    this.query = computeQuery(request);
    this.protocol = request.getProtocol();
    this.userAgent = request.getHeader("User-Agent");
    this.query = uri.getQuery();
    this.protocol = request.getVersion();
    this.userAgent = clientCtx.getUserAgent();
    this.statTracker = this.connectionHandler.getStatTracker();
@@ -277,15 +284,6 @@
    this.connectionID = DirectoryServer.newConnectionAccepted(this);
  }
  private String computeQuery(HttpServletRequest request)
  {
    if (request.getQueryString() != null)
    {
      return request.getRequestURI() + "?" + request.getQueryString();
    }
    return request.getRequestURI();
  }
  @Override
  public String getAuthUser()
  {
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java
@@ -25,46 +25,38 @@
 */
package org.opends.server.protocols.http;
import static org.opends.messages.ConfigMessages.*;
import static org.opends.messages.ConfigMessages.WARN_CONFIG_LOGGER_NO_ACTIVE_HTTP_ACCESS_LOGGERS;
import static org.opends.messages.ProtocolMessages.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;
import static org.opends.server.util.ServerConstants.ALERT_DESCRIPTION_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES;
import static org.opends.server.util.ServerConstants.ALERT_TYPE_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES;
import static org.opends.server.util.StaticUtils.getExceptionMessage;
import static org.opends.server.util.StaticUtils.isAddressInUse;
import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
import java.io.File;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import java.io.IOException;
import java.net.InetAddress;
import java.util.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.ServletException;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.forgerock.http.servlet.HttpFrameworkServlet;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.CollectionResourceProvider;
import org.forgerock.json.resource.ConnectionFactory;
import org.forgerock.json.resource.Resources;
import org.forgerock.json.resource.Router;
import org.forgerock.json.resource.servlet.HttpServlet;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.rest2ldap.AuthorizationPolicy;
import org.forgerock.opendj.rest2ldap.Rest2LDAP;
import org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory;
import org.glassfish.grizzly.http.HttpProbe;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.grizzly.http.server.NetworkListener;
@@ -78,13 +70,21 @@
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.server.ConnectionHandlerCfg;
import org.opends.server.admin.std.server.HTTPConnectionHandlerCfg;
import org.opends.server.api.*;
import org.opends.server.api.AlertGenerator;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.ConnectionHandler;
import org.opends.server.api.KeyManagerProvider;
import org.opends.server.api.ServerShutdownListener;
import org.opends.server.api.TrustManagerProvider;
import org.opends.server.core.DirectoryServer;
import org.opends.server.extensions.NullKeyManagerProvider;
import org.opends.server.extensions.NullTrustManagerProvider;
import org.opends.server.loggers.HTTPAccessLogger;
import org.opends.server.monitors.ClientConnectionMonitorProvider;
import org.opends.server.types.*;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.HostPort;
import org.opends.server.types.InitializationException;
import org.opends.server.util.SelectableCertificateKeyManager;
import org.opends.server.util.StaticUtils;
@@ -107,8 +107,6 @@
  /** SSL instance name used in context creation. */
  private static final String SSL_CONTEXT_INSTANCE_NAME = "TLS";
  private static final ObjectMapper JSON_MAPPER = new ObjectMapper().configure(JsonParser.Feature.ALLOW_COMMENTS, true);
  /** The initialization configuration. */
  private HTTPConnectionHandlerCfg initConfig;
@@ -779,81 +777,12 @@
  private void createAndRegisterServlet(final String servletName, final String... urlPatterns) throws Exception
  {
    // Parse and use JSON config
    File jsonConfigFile = getFileForPath(this.currentConfig.getConfigFile());
    final JsonValue configuration = parseJsonConfiguration(jsonConfigFile).recordKeyAccesses();
    final HTTPAuthenticationConfig authenticationConfig = getAuthenticationConfig(configuration);
    final ConnectionFactory connFactory = getConnectionFactory(configuration);
    configuration.verifyAllKeysAccessed();
    Filter filter = new CollectClientConnectionsFilter(this, authenticationConfig);
    // Used for hooking our HTTPClientConnection in Rest2LDAP
    final HttpServlet servlet = new HttpServlet(connFactory, Rest2LDAPContextFactory.getHttpServletContextFactory());
    // Create and deploy the Web app context
    final WebappContext ctx = new WebappContext(servletName);
    ctx.addFilter("collectClientConnections", filter)
       .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, urlPatterns);
    ctx.addServlet(servletName, servlet).addMapping(urlPatterns);
    ctx.addServlet(servletName, new HttpFrameworkServlet(new LdapHttpApplication(this))).addMapping(urlPatterns);
    ctx.deploy(this.httpServer);
  }
  private HTTPAuthenticationConfig getAuthenticationConfig(final JsonValue configuration)
  {
    final HTTPAuthenticationConfig result = new HTTPAuthenticationConfig();
    final JsonValue val = configuration.get("authenticationFilter");
    result.setBasicAuthenticationSupported(asBool(val, "supportHTTPBasicAuthentication"));
    result.setCustomHeadersAuthenticationSupported(asBool(val, "supportAltAuthentication"));
    result.setCustomHeaderUsername(val.get("altAuthenticationUsernameHeader").asString());
    result.setCustomHeaderPassword(val.get("altAuthenticationPasswordHeader").asString());
    final String searchBaseDN = asString(val, "searchBaseDN");
    result.setSearchBaseDN(org.forgerock.opendj.ldap.DN.valueOf(searchBaseDN));
    result.setSearchScope(SearchScope.valueOf(asString(val, "searchScope")));
    result.setSearchFilterTemplate(asString(val, "searchFilterTemplate"));
    return result;
  }
  private String asString(JsonValue value, String key)
  {
    return value.get(key).required().asString();
  }
  private boolean asBool(JsonValue value, String key)
  {
    return value.get(key).defaultTo(false).asBoolean();
  }
  private ConnectionFactory getConnectionFactory(final JsonValue configuration)
  {
    final Router router = new Router();
    final JsonValue mappings = configuration.get("servlet").get("mappings").required();
    for (final String mappingUrl : mappings.keys())
    {
      final JsonValue mapping = mappings.get(mappingUrl);
      final CollectionResourceProvider provider = Rest2LDAP.builder()
                                                           .authorizationPolicy(AuthorizationPolicy.REUSE)
                                                           .configureMapping(mapping).build();
      router.addRoute(mappingUrl, provider);
    }
    return Resources.newInternalConnectionFactory(router);
  }
  private JsonValue parseJsonConfiguration(File configFile)
      throws IOException, JsonParseException, JsonMappingException, ServletException
  {
    // Parse the config file.
    final Object content = JSON_MAPPER.readValue(configFile, Object.class);
    if (!(content instanceof Map))
    {
      throw new ServletException(
          "Servlet configuration file '" + configFile + "' does not contain a valid JSON configuration");
    }
    return new JsonValue(content);
  }
  private void stopHttpServer()
  {
    if (this.httpServer != null)
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LdapHttpApplication.java
New file
@@ -0,0 +1,177 @@
/*
 * 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 2015 ForgeRock AS
 */
package org.opends.server.protocols.http;
import static org.forgerock.util.Utils.closeSilently;
import static org.opends.server.util.StaticUtils.getFileForPath;
import java.io.File;
import java.io.FileReader;
import java.util.Map;
import org.forgerock.http.Context;
import org.forgerock.http.Handler;
import org.forgerock.http.HttpApplication;
import org.forgerock.http.HttpApplicationException;
import org.forgerock.http.handler.Handlers;
import org.forgerock.http.io.Buffer;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.http.util.Json;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.CollectionResourceProvider;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.Router;
import org.forgerock.json.resource.http.CrestHttp;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.rest2ldap.AuthorizationPolicy;
import org.forgerock.opendj.rest2ldap.Rest2LDAP;
import org.forgerock.util.Factory;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
import org.opends.messages.ProtocolMessages;
/** Main class of the HTTP Connection Handler web application */
class LdapHttpApplication implements HttpApplication
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  /** Http Handler which sets a connection to an OpenDJ server. */
  private static class LdapHttpHandler implements Handler
  {
    private final Handler delegate;
    /**
     * Build a new {@code LdapHttpHandler}.
     *
     * @param configuration
     *            The configuration which will be used to set
     *            the connection and the mappings to the OpenDJ server.
     */
    public LdapHttpHandler(final JsonValue configuration)
    {
      delegate = CrestHttp.newHttpHandler(createRouter(configuration));
    }
    private RequestHandler createRouter(final JsonValue configuration)
    {
      final JsonValue mappings = configuration.get("servlet").get("mappings").required();
      final Router router = new Router();
      for (final String mappingUrl : mappings.keys()) {
        final JsonValue mapping = mappings.get(mappingUrl);
        final CollectionResourceProvider provider = Rest2LDAP.builder()
                .authorizationPolicy(AuthorizationPolicy.REUSE)
                .configureMapping(mapping)
                .build();
        router.addRoute(Router.uriTemplate(mappingUrl), provider);
      }
      return router;
    }
    @Override
    public final Promise<Response, NeverThrowsException> handle(final Context context, final Request request)
    {
      return delegate.handle(context, request);
    }
  }
  private HTTPConnectionHandler connectionHandler;
  private LdapHttpHandler handler;
  private CollectClientConnectionsFilter filter;
  LdapHttpApplication(HTTPConnectionHandler connectionHandler)
  {
    this.connectionHandler = connectionHandler;
  }
  @Override
  public Handler start() throws HttpApplicationException
  {
    try
    {
      final File configFile = getFileForPath(connectionHandler.getCurrentConfig().getConfigFile());
      final Map<String, Object> jsonElems = Json.readJsonLenient(new FileReader(configFile));
      final JsonValue configuration = new JsonValue(jsonElems).recordKeyAccesses();
      handler = new LdapHttpHandler(configuration);
      filter = new CollectClientConnectionsFilter(connectionHandler, getAuthenticationConfig(configuration));
      configuration.verifyAllKeysAccessed();
      return Handlers.chainOf(handler, filter);
    }
    catch (final Exception e)
    {
      final LocalizableMessage errorMsg = ProtocolMessages.ERR_INITIALIZE_HTTP_CONNECTION_HANDLER.get();
      logger.error(errorMsg, e);
      stop();
      throw new HttpApplicationException(errorMsg.toString(), e);
    }
  }
  private HTTPAuthenticationConfig getAuthenticationConfig(final JsonValue configuration)
  {
    final HTTPAuthenticationConfig result = new HTTPAuthenticationConfig();
    final JsonValue val = configuration.get("authenticationFilter");
    result.setBasicAuthenticationSupported(asBool(val, "supportHTTPBasicAuthentication"));
    result.setCustomHeadersAuthenticationSupported(asBool(val, "supportAltAuthentication"));
    result.setCustomHeaderUsername(val.get("altAuthenticationUsernameHeader").asString());
    result.setCustomHeaderPassword(val.get("altAuthenticationPasswordHeader").asString());
    final String searchBaseDN = asString(val, "searchBaseDN");
    result.setSearchBaseDN(org.forgerock.opendj.ldap.DN.valueOf(searchBaseDN));
    result.setSearchScope(SearchScope.valueOf(asString(val, "searchScope")));
    result.setSearchFilterTemplate(asString(val, "searchFilterTemplate"));
    return result;
  }
  private String asString(JsonValue value, String key)
  {
    return value.get(key).required().asString();
  }
  private boolean asBool(JsonValue value, String key)
  {
    return value.get(key).defaultTo(false).asBoolean();
  }
  @Override
  public Factory<Buffer> getBufferFactory()
  {
    return null;
  }
  @Override
  public void stop()
  {
    closeSilently(filter);
    handler = null;
    filter = null;
  }
}
opendj-server-legacy/src/messages/org/opends/messages/protocol.properties
@@ -916,3 +916,4 @@
  Verify that the keystore is properly configured
ERR_INVALID_KEYSTORE_1527=No usable key was found for '%s'. Verify the keystore content
INFO_DISABLE_CONNECTION_1528=Disabling %s
ERR_INITIALIZE_HTTP_CONNECTION_HANDLER_1529=Failed to initialize Http Connection Handler
opendj-server-legacy/src/test/java/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
@@ -26,18 +26,17 @@
package org.opends.server.protocols.http;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.opends.server.protocols.http.CollectClientConnectionsFilter.*;
import java.io.IOException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.assertj.core.api.SoftAssertions;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.json.resource.ResourceException;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.util.Base64;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -48,9 +47,15 @@
  private static final String PASSWORD = "open sesame";
  private static final String BASE64_USERPASS = Base64.encode((USERNAME + ":" + PASSWORD).getBytes());
  private HTTPAuthenticationConfig authConfig = new HTTPAuthenticationConfig();
  private HTTPAuthenticationConfig authConfig;
  private CollectClientConnectionsFilter filter;
  private CollectClientConnectionsFilter filter = new CollectClientConnectionsFilter(null, authConfig);
  @BeforeMethod
  private void createConfigAndFilter()
  {
    authConfig = new HTTPAuthenticationConfig();
    filter = new CollectClientConnectionsFilter(null, authConfig);
  }
  @DataProvider(name = "Invalid HTTP basic auth strings")
  public Object[][] getInvalidHttpBasicAuthStrings()
@@ -80,45 +85,35 @@
  public void sendUnauthorizedResponseWithHttpBasicAuthWillChallengeUserAgent() throws Exception
  {
    authConfig.setBasicAuthenticationSupported(true);
    final Response response = sendUnauthorizedResponseWithHTTPBasicAuthChallenge();
    ServletOutputStream oStream = mock(ServletOutputStream.class);
    HttpServletResponse response = mock(HttpServletResponse.class);
    when(response.getOutputStream()).thenReturn(oStream);
    sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
    verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    verify(response).setHeader("WWW-Authenticate", "Basic realm=\"org.forgerock.opendj\"");
    verifyUnauthorizedOutputMessage(response, oStream);
    assertThat(response.getHeaders().getFirst("WWW-Authenticate")).isEqualTo("Basic realm=\"org.forgerock.opendj\"");
    verifyUnauthorizedOutputMessage(response);
  }
  @Test
  public void sendUnauthorizedResponseWithoutHttpBasicAuthWillNotChallengeUserAgent() throws Exception
  {
    authConfig.setBasicAuthenticationSupported(true);
    authConfig.setBasicAuthenticationSupported(false);
    final Response response = sendUnauthorizedResponseWithHTTPBasicAuthChallenge();
    HttpServletResponse response = mock(HttpServletResponse.class);
    ServletOutputStream oStream = mock(ServletOutputStream.class);
    when(response.getOutputStream()).thenReturn(oStream);
    sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
    verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    verifyUnauthorizedOutputMessage(response, oStream);
    assertThat(response.getHeaders().getFirst("WWW-Authenticate")).isNull();
    verifyUnauthorizedOutputMessage(response);
  }
  private void sendUnauthorizedResponseWithHTTPBasicAuthChallenge(HttpServletResponse response)
  private Response sendUnauthorizedResponseWithHTTPBasicAuthChallenge() throws Exception
  {
    filter.sendErrorReponse(
        response, true, ResourceException.getException(HttpServletResponse.SC_UNAUTHORIZED, "Invalid Credentials"));
    return filter.resourceExceptionToPromise(ResourceException.getException(401, "Invalid Credentials")).get();
  }
  private void verifyUnauthorizedOutputMessage(HttpServletResponse response, ServletOutputStream oStream)
      throws IOException
  private void verifyUnauthorizedOutputMessage(Response response) throws IOException
  {
    verify(response).getOutputStream();
    verify(oStream).println(
        "{\n" + "    \"code\": 401,\n"
              + "    \"message\": \"Invalid Credentials\",\n"
              + "    \"reason\": \"Unauthorized\"\n" + "}");
    final SoftAssertions softly = new SoftAssertions();
    softly.assertThat(response.getStatus().getCode()).isEqualTo(401);
    softly.assertThat(response.getStatus().getReasonPhrase()).isEqualTo("Unauthorized");
    softly.assertThat(response.getEntity().getJson().toString()).isEqualTo(
            "{code=401, reason=Unauthorized, message=Invalid Credentials}");
    softly.assertAll();
  }
  @Test
@@ -126,9 +121,8 @@
  {
    authConfig.setBasicAuthenticationSupported(true);
    HttpServletRequest request = mock(HttpServletRequest.class);
    when(request.getHeader(HTTP_BASIC_AUTH_HEADER)).thenReturn("Basic " + BASE64_USERPASS);
    final Request request = new Request();
    request.getHeaders().add(HTTP_BASIC_AUTH_HEADER, "Basic " + BASE64_USERPASS);
    assertThat(filter.extractUsernamePassword(request)).containsExactly(USERNAME, PASSWORD);
  }
@@ -142,9 +136,9 @@
    authConfig.setCustomHeaderUsername(customHeaderUsername);
    authConfig.setCustomHeaderPassword(customHeaderPassword);
    HttpServletRequest request = mock(HttpServletRequest.class);
    when(request.getHeader(customHeaderUsername)).thenReturn(USERNAME);
    when(request.getHeader(customHeaderPassword)).thenReturn(PASSWORD);
    final Request request = new Request();
    request.getHeaders().add(customHeaderUsername, USERNAME);
    request.getHeaders().add(customHeaderPassword, PASSWORD);
    assertThat(filter.extractUsernamePassword(request)).containsExactly(USERNAME, PASSWORD);
  }
pom.xml
@@ -114,7 +114,8 @@
    <i18nFrameworkVersion>1.4.2-SNAPSHOT</i18nFrameworkVersion>
    <grizzlyFrameworkVersion>2.3.14</grizzlyFrameworkVersion>
    <slf4jVersion>1.7.5</slf4jVersion>
    <forgerockRestVersion>2.1.0-SNAPSHOT</forgerockRestVersion>
    <forgerockRestVersion>3.0.0-SNAPSHOT</forgerockRestVersion>
    <forgerockHttpVersion>0.0.1-SNAPSHOT</forgerockHttpVersion>
    <frDocPluginVersion>3.1.0-SNAPSHOT</frDocPluginVersion>
    <!-- OSGi bundles properties -->