From 1df4f51adf614210ca4a9b9728327090ec5ea264 Mon Sep 17 00:00:00 2001
From: Gaetan Boismal <gaetan.boismal@forgerock.com>
Date: Fri, 11 Sep 2015 20:33:53 +0000
Subject: [PATCH] OPENDJ-1666 PR-19 CREST-3.0.0 Migration
---
opendj-rest2ldap-servlet/pom.xml | 262 --
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LdapHttpApplication.java | 177 ++
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java | 273 +-
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java | 20
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java | 384 ++++
opendj-rest2ldap/pom.xml | 10
opendj-server-legacy/src/messages/org/opends/messages/protocol.properties | 1
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java | 167 +
opendj-server-legacy/src/test/java/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java | 72
pom.xml | 3
opendj-server-legacy/pom.xml | 24
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPClientConnection.java | 64
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java | 64
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml | 63
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java | 45
opendj-rest2ldap-servlet/src/main/webapp/META-INF/services/org.forgerock.http.HttpApplication | 16
opendj-server-legacy/src/main/assembly/opendj-archive-component.xml | 1
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java | 330 +-
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/FilterType.java | 10
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java | 126 -
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java | 269 +-
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java | 171 +-
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java | 129 -
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java | 55
/dev/null | 20
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java | 45
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java | 1537 ++++++++---------
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java | 27
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json | 0
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java | 128
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/CollectClientConnectionsFilter.java | 556 ++----
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java | 26
32 files changed, 2,562 insertions(+), 2,513 deletions(-)
diff --git a/opendj-rest2ldap-servlet/pom.xml b/opendj-rest2ldap-servlet/pom.xml
index c6c5249..7d745cf 100644
--- a/opendj-rest2ldap-servlet/pom.xml
+++ b/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">
- <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>
- <properties>
- <jacksonVersion>1.9.2</jacksonVersion>
- <checkstyleHeaderLocation>org/forgerock/checkstyle/default-java-header</checkstyleHeaderLocation>
- </properties>
- <dependencies>
- <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>
+<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>war</packaging>
+
+ <properties>
+ <!-- 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>
+ </dependency>
+ </dependencies>
+
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-maven-plugin</artifactId>
+ <version>9.2.11.v20150529</version>
+ <configuration>
+ <scanIntervalSeconds>10</scanIntervalSeconds>
+ <webAppConfig>
+ <contextPath>/</contextPath>
+ </webAppConfig>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-war-plugin</artifactId>
+ <configuration>
+ <webResources>
<resource>
- <directory>src/main/resources</directory>
- <filtering>true</filtering>
+ <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>
- </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>
- <configuration>
- <lifecycleMappingMetadata>
- <pluginExecutions>
- <pluginExecution>
- <pluginExecutionFilter>
- <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>
- </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>
+ <resource>
+ <targetPath>/</targetPath>
+ <directory>src/main/webapp/</directory>
+ <excludes>
+ <exclude>web.xml</exclude>
+ </excludes>
+ </resource>
+ </webResources>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
</project>
diff --git a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPAuthnFilter.java b/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPAuthnFilter.java
deleted file mode 100644
index 70ed0f6..0000000
--- a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPAuthnFilter.java
+++ /dev/null
@@ -1,485 +0,0 @@
-/*
- * 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.servlet;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.StringTokenizer;
-import java.util.concurrent.atomic.AtomicReference;
-
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.codehaus.jackson.JsonParser;
-import org.codehaus.jackson.map.ObjectMapper;
-import org.forgerock.json.fluent.JsonValue;
-import org.forgerock.json.fluent.JsonValueException;
-import org.forgerock.json.resource.ResourceException;
-import org.forgerock.json.resource.servlet.ServletApiVersionAdapter;
-import org.forgerock.json.resource.servlet.ServletSynchronizer;
-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.Connections;
-import org.forgerock.opendj.ldap.DN;
-import org.forgerock.opendj.ldap.EntryNotFoundException;
-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.SearchRequest;
-import org.forgerock.opendj.ldap.responses.BindResult;
-import org.forgerock.opendj.ldap.responses.SearchResultEntry;
-import org.forgerock.opendj.ldap.schema.Schema;
-import org.forgerock.opendj.rest2ldap.Rest2LDAP;
-import org.forgerock.util.AsyncFunction;
-import org.forgerock.util.promise.ExceptionHandler;
-import org.forgerock.util.promise.Promise;
-import org.forgerock.util.promise.ResultHandler;
-
-import static org.forgerock.json.resource.SecurityContext.*;
-import static org.forgerock.json.resource.servlet.SecurityContextFactory.*;
-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.servlet.Rest2LDAPContextFactory.*;
-
-/**
- * An LDAP based authentication Servlet filter.
- * <p>
- * TODO: this is a work in progress. In particular, in order to embed this into
- * the OpenDJ HTTP listener it will need to provide a configuration API.
- */
-public final class Rest2LDAPAuthnFilter implements Filter {
- /** Indicates how authentication should be performed. */
- private static enum AuthenticationMethod {
- SASL_PLAIN, SEARCH_SIMPLE, SIMPLE;
- }
-
- private static final String INIT_PARAM_CONFIG_FILE = "config-file";
- private static final ObjectMapper JSON_MAPPER = new ObjectMapper().configure(
- JsonParser.Feature.ALLOW_COMMENTS, true);
-
- private String altAuthenticationPasswordHeader;
- private String altAuthenticationUsernameHeader;
- private AuthenticationMethod authenticationMethod = AuthenticationMethod.SEARCH_SIMPLE;
- private ConnectionFactory bindLDAPConnectionFactory;
- /** Indicates whether or not authentication should be performed. */
- private boolean isEnabled;
- private boolean reuseAuthenticatedConnection = true;
- private String saslAuthzIdTemplate;
- private final Schema schema = Schema.getDefaultSchema();
- private DN searchBaseDN;
- private String searchFilterTemplate;
- private ConnectionFactory searchLDAPConnectionFactory;
- private SearchScope searchScope = SearchScope.WHOLE_SUBTREE;
- private boolean supportAltAuthentication;
- private boolean supportHTTPBasicAuthentication = true;
- private ServletApiVersionAdapter syncFactory;
-
- @Override
- public void destroy() {
- if (searchLDAPConnectionFactory != null) {
- searchLDAPConnectionFactory.close();
- }
- if (bindLDAPConnectionFactory != null) {
- bindLDAPConnectionFactory.close();
- }
- }
-
- @Override
- public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
- throws IOException, ServletException {
- // Skip this filter if authentication has not been configured.
- if (!isEnabled) {
- chain.doFilter(request, response);
- return;
- }
-
- // First of all parse the HTTP headers for authentication credentials.
- if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
- // This should never happen.
- throw new ServletException("non-HTTP request or response");
- }
-
- // TODO: support logout, sessions, reauth?
- final HttpServletRequest req = (HttpServletRequest) request;
- final HttpServletResponse res = (HttpServletResponse) response;
-
- /*
- * Store the authenticated connection so that it can be re-used by the
- * servlet if needed. However, make sure that it is closed on
- * completion.
- */
- final AtomicReference<Connection> savedConnection = new AtomicReference<>();
- final ServletSynchronizer sync = syncFactory.createServletSynchronizer(req, res);
-
- sync.addAsyncListener(new Runnable() {
- @Override
- public void run() {
- closeConnection(savedConnection);
- }
- });
-
- try {
- final String headerUsername = supportAltAuthentication ? req.getHeader(altAuthenticationUsernameHeader)
- : null;
- final String headerPassword = supportAltAuthentication ? req.getHeader(altAuthenticationPasswordHeader)
- : null;
- final String headerAuthorization = supportHTTPBasicAuthentication ? req.getHeader("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 || !HttpServletRequest.BASIC_AUTH.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);
- doBind(req, res, newSimpleBindRequest(username, password), chain, savedConnection, sync, username,
- authzid);
- break;
- }
- 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);
- }
- doBind(req, res, newPlainSASLBindRequest(bindId, password), chain, savedConnection, sync, username,
- authzid);
- break;
- }
- default: // SEARCH_SIMPLE
- {
- /*
- * First do a search to find the user's entry and then perform a
- * bind request using the user's DN.
- */
- final org.forgerock.opendj.ldap.Filter filter = org.forgerock.opendj.ldap.Filter.format(
- searchFilterTemplate, username);
- final SearchRequest searchRequest = newSearchRequest(searchBaseDN, searchScope, filter, "1.1");
- searchLDAPConnectionFactory.getConnectionAsync()
- .thenAsync(new AsyncFunction<Connection, SearchResultEntry, LdapException>() {
- @Override
- public Promise<SearchResultEntry, LdapException> apply(Connection connection)
- throws LdapException {
- savedConnection.set(connection);
- // Do the search.
- return connection.searchSingleEntryAsync(searchRequest);
- }
- }).thenOnResult(new ResultHandler<SearchResultEntry>() {
- @Override
- public void handleResult(final SearchResultEntry result) {
- savedConnection.get().close();
- final String bindDN = result.getName().toString();
- final Map<String, Object> authzid = new LinkedHashMap<>(2);
- authzid.put(AUTHZID_DN, bindDN);
- authzid.put(AUTHZID_ID, username);
- doBind(req, res, newSimpleBindRequest(bindDN, password), chain, savedConnection, sync,
- username, authzid);
- }
- }).thenOnException(new ExceptionHandler<LdapException>() {
- @Override
- public void handleException(final LdapException exception) {
- LdapException normalizedError = exception;
- if (savedConnection.get() != null) {
- savedConnection.get().close();
- /*
- * The search error should not be passed
- * as-is back to the user.
- */
- if (exception instanceof EntryNotFoundException
- || exception instanceof MultipleEntriesFoundException) {
- normalizedError = newLdapException(ResultCode.INVALID_CREDENTIALS, exception);
- } else if (exception instanceof AuthenticationException
- || exception instanceof AuthorizationException) {
- normalizedError =
- newLdapException(ResultCode.CLIENT_SIDE_LOCAL_ERROR, exception);
- } else {
- normalizedError = exception;
- }
- }
- sync.signalAndComplete(asResourceException(normalizedError));
- }
- });
- break;
- }
- }
- sync.awaitIfNeeded();
- if (!sync.isAsync()) {
- chain.doFilter(request, response);
- }
- } catch (final Throwable t) {
- sync.signalAndComplete(t);
- } finally {
- if (!sync.isAsync()) {
- closeConnection(savedConnection);
- }
- }
- }
-
- @Override
- public void init(final FilterConfig config) throws ServletException {
- // FIXME: make it possible to configure the filter externally, especially
- // connection factories.
- final String configFileName = config.getInitParameter(INIT_PARAM_CONFIG_FILE);
- if (configFileName == null) {
- throw new ServletException("Authentication filter initialization parameter '"
- + INIT_PARAM_CONFIG_FILE + "' not specified");
- }
- final InputStream configFile =
- config.getServletContext().getResourceAsStream(configFileName);
- if (configFile == null) {
- throw new ServletException("Servlet filter configuration file '" + configFileName
- + "' not found");
- }
- try {
- // Parse the config file.
- final Object content = JSON_MAPPER.readValue(configFile, Object.class);
- if (!(content instanceof Map)) {
- throw new ServletException("Servlet filter configuration file '" + configFileName
- + "' does not contain a valid JSON configuration");
- }
-
- // Parse the authentication configuration.
- final JsonValue configuration = new JsonValue(content);
- final JsonValue authnConfig = configuration.get("authenticationFilter");
- if (!authnConfig.isNull()) {
- 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();
- }
-
- // 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 SIMPLE:
- // Nothing to do.
- break;
- case SASL_PLAIN:
- saslAuthzIdTemplate =
- authnConfig.get("saslAuthzIdTemplate").required().asString();
- 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);
- 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);
-
- // Set the completion handler factory based on the Servlet API version.
- syncFactory = ServletApiVersionAdapter.getInstance(config.getServletContext());
-
- isEnabled = true;
- }
- } catch (final ServletException e) {
- // Rethrow.
- throw e;
- } catch (final Exception e) {
- throw new ServletException("Servlet filter configuration file '" + configFileName
- + "' could not be read: " + e.getMessage());
- } finally {
- try {
- configFile.close();
- } catch (final Exception e) {
- // Ignore.
- }
- }
- }
-
- private void closeConnection(final AtomicReference<Connection> savedConnection) {
- final Connection connection = savedConnection.get();
- if (connection != null) {
- connection.close();
- }
- }
-
- /**
- * Get a bind connection and then perform the bind operation, setting the
- * cached connection and authorization credentials on completion.
- */
- private void doBind(final HttpServletRequest request, final ServletResponse response,
- final BindRequest bindRequest, final FilterChain chain, final AtomicReference<Connection> savedConnection,
- final ServletSynchronizer sync, final String authcid, final Map<String, Object> authzid) {
- bindLDAPConnectionFactory.getConnectionAsync()
- .thenAsync(new AsyncFunction<Connection, BindResult, LdapException>() {
- @Override
- public Promise<BindResult, LdapException> apply(Connection connection)
- throws LdapException {
- savedConnection.set(connection);
- return connection.bindAsync(bindRequest);
- }
- }).thenOnResult(new ResultHandler<BindResult>() {
- @Override
- public void handleResult(final BindResult result) {
- /*
- * Cache the pre-authenticated connection and prevent
- * downstream components from closing it since this
- * filter will close it.
- */
- if (reuseAuthenticatedConnection) {
- request.setAttribute(ATTRIBUTE_AUTHN_CONNECTION,
- Connections.uncloseable(savedConnection.get()));
- }
-
- // Pass through the authentication ID and authorization principals.
- request.setAttribute(ATTRIBUTE_AUTHCID, authcid);
- request.setAttribute(ATTRIBUTE_AUTHZID, authzid);
-
- // Invoke the remainder of the filter chain.
- sync.signal();
- if (sync.isAsync()) {
- try {
- chain.doFilter(request, response);
-
- /*
- * Fix for OPENDJ-1105: Jetty 8 a bug where
- * synchronous downstream completion (i.e. in
- * the servlet) is ignored due to upstream
- * active async context. The following code
- * should be benign in other containers.
- */
- if (response.isCommitted()) {
- sync.signalAndComplete();
- }
- } catch (Throwable t) {
- sync.signalAndComplete(asResourceException(t));
- }
- }
- }
- }).thenOnException(new ExceptionHandler<LdapException>() {
- @Override
- public void handleException(final LdapException exception) {
- sync.signalAndComplete(asResourceException(exception));
- }
- });
- }
-
- private 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 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;
- }
- }
-
-}
diff --git a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java b/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java
deleted file mode 100644
index 4658711..0000000
--- a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * 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 ForgeRock AS.
- */
-package org.forgerock.opendj.rest2ldap.servlet;
-
-import static org.forgerock.json.resource.Resources.*;
-import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
-
-import java.io.InputStream;
-import java.util.Map;
-
-import javax.servlet.ServletConfig;
-import javax.servlet.ServletException;
-
-import org.codehaus.jackson.JsonParser;
-import org.codehaus.jackson.map.ObjectMapper;
-import org.forgerock.json.fluent.JsonValue;
-import org.forgerock.json.resource.CollectionResourceProvider;
-import org.forgerock.json.resource.Connection;
-import org.forgerock.json.resource.ConnectionFactory;
-import org.forgerock.json.resource.FutureResult;
-import org.forgerock.json.resource.ResourceException;
-import org.forgerock.json.resource.ResultHandler;
-import org.forgerock.json.resource.Router;
-import org.forgerock.opendj.rest2ldap.AuthorizationPolicy;
-import org.forgerock.opendj.rest2ldap.Rest2LDAP;
-import org.forgerock.opendj.rest2ldap.Rest2LDAP.Builder;
-import org.forgerock.util.Utils;
-
-/**
- * The connection factory provider which is used by the OpenDJ Commons REST LDAP
- * Gateway.
- */
-public final class Rest2LDAPConnectionFactoryProvider {
- private static final String INIT_PARAM_CONFIG_FILE = "config-file";
- private static final ObjectMapper JSON_MAPPER = new ObjectMapper().configure(
- JsonParser.Feature.ALLOW_COMMENTS, true);
-
- /**
- * Returns a JSON resource connection factory configured using the
- * configuration file named in the {@code config-file} Servlet
- * initialization parameter. See the sample configuration file for a
- * detailed description of its content.
- *
- * @param config
- * The Servlet configuration.
- * @return The configured JSON resource connection factory.
- * @throws ServletException
- * If the connection factory could not be initialized.
- * @see Rest2LDAP#configureConnectionFactory(JsonValue, String)
- * @see Builder#configureMapping(JsonValue)
- */
- public static ConnectionFactory getConnectionFactory(final ServletConfig config)
- throws ServletException {
- final String configFileName = config.getInitParameter(INIT_PARAM_CONFIG_FILE);
- if (configFileName == null) {
- throw new ServletException("Servlet initialization parameter '"
- + INIT_PARAM_CONFIG_FILE + "' not specified");
- }
- final InputStream configFile =
- config.getServletContext().getResourceAsStream(configFileName);
- if (configFile == null) {
- throw new ServletException("Servlet configuration file '" + configFileName
- + "' not found");
- }
- try {
- // Parse the config file.
- final Object content = JSON_MAPPER.readValue(configFile, Object.class);
- if (!(content instanceof Map)) {
- throw new ServletException("Servlet configuration file '" + configFileName
- + "' does not contain a valid JSON configuration");
- }
- final JsonValue configuration = new JsonValue(content);
-
- // Parse the authorization configuration.
- final AuthorizationPolicy authzPolicy =
- configuration.get("servlet").get("authorizationPolicy").required().asEnum(
- AuthorizationPolicy.class);
- final String proxyAuthzTemplate =
- configuration.get("servlet").get("proxyAuthzIdTemplate").asString();
-
- // Parse the connection factory if present.
- final String ldapFactoryName =
- configuration.get("servlet").get("ldapConnectionFactory").asString();
- final org.forgerock.opendj.ldap.ConnectionFactory ldapFactory;
- if (ldapFactoryName != null) {
- ldapFactory =
- configureConnectionFactory(configuration.get("ldapConnectionFactories")
- .required(), ldapFactoryName);
- } else {
- ldapFactory = null;
- }
-
- // Create the router.
- 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().ldapConnectionFactory(ldapFactory).authorizationPolicy(
- authzPolicy).proxyAuthzIdTemplate(proxyAuthzTemplate)
- .configureMapping(mapping).build();
- router.addRoute(mappingUrl, provider);
- }
- final ConnectionFactory factory = newInternalConnectionFactory(router);
- if (ldapFactory != null) {
- /*
- * Return a wrapper which will release resources associated with
- * the LDAP connection factory (pooled connections, transport,
- * etc).
- */
- return new ConnectionFactory() {
- @Override
- public FutureResult<Connection> getConnectionAsync(
- ResultHandler<? super Connection> handler) {
- return factory.getConnectionAsync(handler);
- }
-
- @Override
- public Connection getConnection() throws ResourceException {
- return factory.getConnection();
- }
-
- @Override
- public void close() {
- ldapFactory.close();
- }
- };
- } else {
- return factory;
- }
- } catch (final ServletException e) {
- // Rethrow.
- throw e;
- } catch (final Exception e) {
- throw new ServletException("Servlet configuration file '" + configFileName
- + "' could not be read: " + e.getMessage());
- } finally {
- Utils.closeSilently(configFile);
- }
- }
-
- /** Prevent instantiation. */
- private Rest2LDAPConnectionFactoryProvider() {
- // Nothing to do.
- }
-
-}
diff --git a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPContextFactory.java b/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPContextFactory.java
deleted file mode 100644
index d16dc3d..0000000
--- a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPContextFactory.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * 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 2012-2014 ForgeRock AS.
- */
-package org.forgerock.opendj.rest2ldap.servlet;
-
-import javax.servlet.http.HttpServletRequest;
-
-import org.forgerock.json.resource.Context;
-import org.forgerock.json.resource.InternalServerErrorException;
-import org.forgerock.json.resource.ResourceException;
-import org.forgerock.json.resource.RootContext;
-import org.forgerock.json.resource.SecurityContext;
-import org.forgerock.json.resource.servlet.HttpServletContextFactory;
-import org.forgerock.json.resource.servlet.SecurityContextFactory;
-import org.forgerock.opendj.ldap.Connection;
-import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
-
-/**
- * An HTTP servlet context factory which will create a {@link Context} chain
- * comprising of a {@link SecurityContext} and optionally an
- * {@link AuthenticatedConnectionContext}.
- * <p>
- * This class provides integration between Rest2LDAP HTTP Servlets and the
- * {@link Rest2LDAPAuthnFilter}, by providing a mechanism allowing the filter to
- * pass a pre-authenticated LDAP connection through to the underlying Rest2LDAP
- * implementation for use when performing subsequent LDAP operations. The
- * following code illustrates how an authentication Servlet filter can populate
- * the attributes:
- *
- * <pre>
- * public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
- * // Authenticate the user.
- * String username = getUserName(request);
- * String password = getPassword(request);
- * final Connection connection = getLDAPConnection();
- *
- * // Publish the authenticated connection.
- * try {
- * connection.bind(username, password.toCharArray());
- * request.setAttribute(ATTRIBUTE_AUTHN_CONNECTION, connection);
- * } catch (LdapException e) {
- * // Fail the HTTP request.
- * response.setStatus(...);
- * return;
- * }
- *
- * // Invoke the rest of the filter chain and then release the LDAP connection once
- * // processing has completed. Note that this assumes that the filter chain is
- * // processes requests synchronous.
- * try {
- * chain.doFilter(request, response);
- * } finally {
- * connection.close();
- * }
- * }
- * </pre>
- */
-public final class Rest2LDAPContextFactory implements HttpServletContextFactory {
-
- /**
- * The name of the HTTP Servlet Request attribute where this factory expects
- * to find the authenticated user's authentication ID. The name of this
- * attribute is {@code org.forgerock.security.authcid} and it MUST contain a
- * {@code String} if it is present.
- *
- * @see AuthenticatedConnectionContext
- */
- public static final String ATTRIBUTE_AUTHN_CONNECTION =
- "org.forgerock.opendj.rest2ldap.authn-connection";
-
- /** Singleton instance. */
- private static final Rest2LDAPContextFactory INSTANCE = new Rest2LDAPContextFactory();
-
- /**
- * Returns the singleton context factory which can be used for obtaining
- * context information from a HTTP servlet request.
- * <p>
- * This method is named {@code getHttpServletContextFactory} so that it can
- * easily be used for
- * {@link org.forgerock.json.resource.servlet.HttpServlet#getHttpServletContextFactory
- * configuring} JSON Resource Servlets.
- *
- * @return The singleton context factory.
- */
- public static Rest2LDAPContextFactory getHttpServletContextFactory() {
- return INSTANCE;
- }
-
- private Rest2LDAPContextFactory() {
- // Prevent instantiation.
- }
-
- /**
- * Creates a new {@link Context} chain comprising of the provided parent
- * context(s), a {@link SecurityContext} obtained using a
- * {@link SecurityContextFactory} , and optionally a
- * {@code AuthenticatedConnectionContext}. The authenticated connection will
- * be obtained from the {@link #ATTRIBUTE_AUTHN_CONNECTION} attribute
- * contained in the provided HTTP servlet request. If the attribute is not
- * present then the {@code AuthenticatedConnectionContext} will not be
- * created.
- *
- * @param parent
- * The parent context.
- * @param request
- * The HTTP servlet request from which the security and
- * authenticated connection attributes should be obtained.
- * @return A new {@link Context} chain comprising of the provided parent
- * context(s), a {@link SecurityContext} obtained using a
- * {@link SecurityContextFactory} , and optionally a
- * {@code AuthenticatedConnectionContext}.
- * @throws ResourceException
- * If one of the attributes was present but had the wrong type.
- */
- public Context createContext(final Context parent, final HttpServletRequest request)
- throws ResourceException {
- // First get the security context.
- final Context securityContext =
- SecurityContextFactory.getHttpServletContextFactory()
- .createContext(parent, request);
-
- // Now append the pre-authenticated connection context if required.
- final Connection connection;
- try {
- connection = (Connection) request.getAttribute(ATTRIBUTE_AUTHN_CONNECTION);
- } catch (final ClassCastException e) {
- throw new InternalServerErrorException(
- "The rest2ldap authenticated connection context could not be "
- + "created because the connection attribute, "
- + ATTRIBUTE_AUTHN_CONNECTION + ", contained in the HTTP "
- + "servlet request did not have the correct type", e);
- }
- if (connection != null) {
- return new AuthenticatedConnectionContext(securityContext, connection);
- } else {
- return securityContext;
- }
- }
-
- /**
- * Creates a new {@link Context} chain comprising of a {@link RootContext},
- * a {@link SecurityContext} obtained using a {@link SecurityContextFactory}
- * , and optionally a {@code AuthenticatedConnectionContext}. The
- * authenticated connection will be obtained from the
- * {@link #ATTRIBUTE_AUTHN_CONNECTION} attribute contained in the provided
- * HTTP servlet request. If the attribute is not present then the
- * {@code AuthenticatedConnectionContext} will not be created.
- *
- * @param request
- * The HTTP servlet request from which the security and
- * authenticated connection attributes should be obtained.
- * @return A new {@link Context} chain comprising of a {@link RootContext},
- * a {@link SecurityContext} obtained using a
- * {@link SecurityContextFactory} , and optionally a
- * {@code AuthenticatedConnectionContext}.
- * @throws ResourceException
- * If one of the attributes was present but had the wrong type.
- */
- @Override
- public Context createContext(final HttpServletRequest request) throws ResourceException {
- return createContext(new RootContext(), request);
- }
-
-}
diff --git a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/package-info.java b/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/package-info.java
deleted file mode 100644
index 53ce09f..0000000
--- a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/package-info.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * 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 ForgeRock AS.
- */
-
-/**
- * Provides integration between the OpenDJ Commons REST Adapter and Servlet APIs.
- */
-package org.forgerock.opendj.rest2ldap.servlet;
diff --git a/opendj-rest2ldap-servlet/src/main/webapp/META-INF/services/org.forgerock.http.HttpApplication b/opendj-rest2ldap-servlet/src/main/webapp/META-INF/services/org.forgerock.http.HttpApplication
new file mode 100644
index 0000000..40ee013
--- /dev/null
+++ b/opendj-rest2ldap-servlet/src/main/webapp/META-INF/services/org.forgerock.http.HttpApplication
@@ -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
diff --git a/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
similarity index 100%
rename from opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
rename to opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
diff --git a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml
index 2ba6b1c..42655ef 100644
--- a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml
+++ b/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>
-
\ No newline at end of file
diff --git a/opendj-rest2ldap/pom.xml b/opendj-rest2ldap/pom.xml
index c800614..06605ec 100644
--- a/opendj-rest2ldap/pom.xml
+++ b/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,13 +39,13 @@
</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>
- <version>${forgerockRestVersion}</version>
+ <groupId>org.forgerock.commons</groupId>
+ <artifactId>json-resource-http</artifactId>
+ <version>${forgerockRestVersion}</version>
</dependency>
<dependency>
<groupId>org.forgerock</groupId>
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
index 3e7803e..dcf269f 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
+++ b/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);
- }
- }
- };
- }
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
index 53a7602..d0fd363 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
+++ b/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.
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
index 19a5f93..9a2efb2 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
+++ b/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.
*
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/FilterType.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/FilterType.java
index 90b8b9b..6e226db 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/FilterType.java
+++ b/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
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java
new file mode 100644
index 0000000..2c5bee5
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java
@@ -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);
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
index dc1faca..f916a0d 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
+++ b/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());
}
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
index 264918a..15d7996 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
+++ b/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,511 +125,663 @@
}
@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() {
- @Override
- public void run() {
- // Calculate entry content.
- attributeMapper.create(c, new JsonPointer(), request.getContent(),
- new ResultHandler<List<Attribute>>() {
- @Override
- public void handleError(final ResourceException error) {
- h.handleError(error);
- }
-
- @Override
- public void handleResult(final List<Attribute> result) {
- // Perform add operation.
- final AddRequest addRequest = newAddRequest(DN.rootDN());
- for (final Attribute attribute : additionalLDAPAttributes) {
- addRequest.addAttribute(attribute);
- }
- for (final Attribute attribute : result) {
- addRequest.addAttribute(attribute);
- }
- try {
- nameStrategy.setResourceId(c, getBaseDN(c), request.getNewResourceId(), addRequest);
- } catch (final ResourceException e) {
- h.handleError(e);
- return;
- }
- if (config.readOnUpdatePolicy() == CONTROLS) {
- final String[] attributes = getLDAPAttributes(c, request.getFields());
- addRequest.addControl(PostReadRequestControl.newControl(false, attributes));
- }
- c.getConnection().applyChangeAsync(addRequest)
- .thenOnResult(postUpdateResultHandler(c, h))
- .thenOnException(postUpdateExceptionHandler(h));
- }
- });
- }
- });
- }
-
- @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>() {
- @Override
- public void handleError(final ResourceException error) {
- h.handleError(error);
- }
-
- @Override
- public void handleResult(final DN dn) {
- try {
- final ChangeRecord deleteRequest = newDeleteRequest(dn);
- if (config.readOnUpdatePolicy() == CONTROLS) {
- final String[] attributes = getLDAPAttributes(c, 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));
- } catch (final Exception e) {
- h.handleError(asResourceException(e));
- }
- }
- }));
- }
-
- @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);
-
- if (request.getPatchOperations().isEmpty()) {
- /*
- * This patch is a no-op so just read the entry and check its version.
- */
- c.run(h, new Runnable() {
+ return requestState.getConnection().thenAsync(
+ new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
@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);
- }
-
- @Override
- public void handleResult(final DN dn) {
- // 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.when(promises).thenOnResult(
- new org.forgerock.util.promise.ResultHandler<List<List<Modification>>>() {
- @Override
- public void handleResult(final List<List<Modification>> result) {
- // The patch operations have been converted successfully.
- try {
- final ModifyRequest modifyRequest = newModifyRequest(dn);
-
- // Add the modifications.
- for (final List<Modification> modifications : result) {
- if (modifications != null) {
- modifyRequest.getModifications().addAll(modifications);
- }
- }
-
- final List<String> attributes = asList(getLDAPAttributes(c, 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));
- } else {
- // Add controls and perform the modify request.
- if (config.readOnUpdatePolicy() == CONTROLS) {
- modifyRequest.addControl(
- PostReadRequestControl.newControl(false, attributes));
- }
- if (config.usePermissiveModify()) {
- modifyRequest.addControl(PermissiveModifyRequestControl.newControl(true));
- }
- addAssertionControl(modifyRequest, request.getRevision());
- c.getConnection().applyChangeAsync(modifyRequest)
- .thenOnResult(postUpdateResultHandler(c, h))
- .thenOnException(postUpdateExceptionHandler(h));
- }
- } catch (final Exception e) {
- h.handleError(asResourceException(e));
- }
- }
- }).thenOnException(new ExceptionHandler<ResourceException>() {
- @Override
- public void handleException(ResourceException exception) {
- h.handleError(asResourceException(exception));
- }
- });
- }
- }));
- }
- }
-
- @Override
- public void queryCollection(final ServerContext context, final QueryRequest request,
- final QueryResultHandler handler) {
- final Context c = wrap(context);
- final QueryResultHandler h = wrap(c, handler);
-
- /*
- * Get the connection, then calculate the search filter, then perform the search.
- */
- c.run(h, new Runnable() {
- @Override
- public void run() {
- // Calculate the filter (this may require the connection).
- getLDAPFilter(c, request.getQueryFilter(), new ResultHandler<Filter>() {
- /**
- * 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;
- private ResourceException pendingResult;
- private int pendingResourceCount;
- private boolean resultSent;
- 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.
- */
- if (ldapFilter == null || ldapFilter == alwaysFalse()) {
- h.handleResult(new QueryResult());
- } else {
- // 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);
-
- /*
- * 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) {
- final int pageResultEndIndex;
- if (request.getPagedResultsOffset() > 0) {
- pageResultStartIndex = request.getPagedResultsOffset() * pageSize;
- pageResultEndIndex = pageResultStartIndex + pageSize;
- } else {
- pageResultStartIndex = 0;
- pageResultEndIndex = pageSize;
- }
- final ByteString cookie =
- request.getPagedResultsCookie() != null ? ByteString
- .valueOfBase64(request.getPagedResultsCookie())
- : ByteString.empty();
- final SimplePagedResultsControl control =
- SimplePagedResultsControl.newControl(true, pageResultEndIndex, cookie);
- searchRequest.addControl(control);
- } else {
- pageResultStartIndex = 0;
- }
-
- c.getConnection().searchAsync(searchRequest, new SearchResultHandler() {
+ public Promise<ResourceResponse, ResourceException> apply(final Connection connection)
+ throws ResourceException {
+ // Calculate entry content.
+ return attributeMapper.create(requestState, new JsonPointer(), request.getContent())
+ .thenAsync(new AsyncFunction<List<Attribute>, ResourceResponse, ResourceException>() {
@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.
- */
- synchronized (sequenceLock) {
- if (pendingResult != null) {
- return false;
- }
- if (totalResourceCount++ < pageResultStartIndex) {
- // Haven't reached paged results threshold yet.
- return true;
- }
- pendingResourceCount++;
+ 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);
}
-
- /*
- * 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 revision = getRevisionFromEntry(entry);
- attributeMapper.read(c, new JsonPointer(), entry, new ResultHandler<JsonValue>() {
- @Override
- public void handleError(final ResourceException e) {
- synchronized (sequenceLock) {
- pendingResourceCount--;
- completeIfNecessary(e);
- }
- }
-
- @Override
- public void handleResult(final JsonValue result) {
- synchronized (sequenceLock) {
- pendingResourceCount--;
- if (!resultSent) {
- h.handleResource(new Resource(id, revision, result));
- }
- completeIfNecessary();
- }
- }
- });
- return true;
- }
-
- @Override
- public boolean handleReference(final SearchResultReference reference) {
- // 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>() {
- @Override
- public void handleResult(Result result) {
- synchronized (sequenceLock) {
- if (request.getPageSize() > 0) {
- try {
- final SimplePagedResultsControl control =
- result.getControl(SimplePagedResultsControl.DECODER,
- DECODE_OPTIONS);
- if (control != null && !control.getCookie().isEmpty()) {
- cookie = control.getCookie().toBase64String();
- }
- } catch (final DecodeException e) {
- // FIXME: need some logging.
- }
- }
- completeIfNecessary(SUCCESS);
+ for (final Attribute attribute : attributes) {
+ addRequest.addAttribute(attribute);
}
- }
- }).thenOnException(new ExceptionHandler<LdapException>() {
- @Override
- public void handleException(LdapException exception) {
- synchronized (sequenceLock) {
- completeIfNecessary(asResourceException(exception));
+ try {
+ nameStrategy.setResourceId(requestState, getBaseDN(requestState),
+ request.getNewResourceId(), addRequest);
+ } catch (final ResourceException e) {
+ return Promises.newExceptionPromise(e);
}
+ if (config.readOnUpdatePolicy() == CONTROLS) {
+ addRequest.addControl(PostReadRequestControl.newControl(
+ false, getLDAPAttributes(requestState, request.getFields())));
+ }
+ return connection.applyChangeAsync(addRequest)
+ .thenAsync(postUpdateResultAsyncFunction(requestState),
+ ldapExceptionToResourceException());
}
});
- }
- }
+ }
+ }).thenFinally(close(requestState));
+ }
- /**
- * This method must be invoked with the sequenceLock held.
- */
- private void completeIfNecessary(final ResourceException e) {
- if (pendingResult == null) {
- pendingResult = e;
- }
- completeIfNecessary();
- }
-
- /**
- * 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() {
- if (pendingResourceCount == 0 && pendingResult != null && !resultSent) {
- if (pendingResult == SUCCESS) {
- h.handleResult(new QueryResult(cookie, -1));
- } else {
- h.handleError(pendingResult);
+ @Override
+ 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 Promise<ResourceResponse, ResourceException> apply(DN dn) throws ResourceException {
+ try {
+ final ChangeRecord deleteRequest = newDeleteRequest(dn);
+ if (config.readOnUpdatePolicy() == CONTROLS) {
+ final String[] attributes = getLDAPAttributes(requestState, request.getFields());
+ deleteRequest.addControl(PreReadRequestControl.newControl(false, attributes));
}
- resultSent = true;
+ if (config.useSubtreeDelete()) {
+ deleteRequest.addControl(SubtreeDeleteRequestControl.newControl(true));
+ }
+ addAssertionControl(deleteRequest, request.getRevision());
+ return connectionHolder.get().applyChangeAsync(deleteRequest)
+ .thenAsync(postUpdateResultAsyncFunction(requestState),
+ ldapExceptionToResourceException());
+
+ } catch (final Exception e) {
+ return Promises.newExceptionPromise((asResourceException(e)));
+ }
+ }
+ }).thenFinally(close(requestState));
+ }
+
+ @Override
+ public Promise<ResourceResponse, ResourceException> patchInstance(
+ final Context context, final String resourceId, final PatchRequest request) {
+ final RequestState requestState = wrap(context);
+
+ if (request.getPatchOperations().isEmpty()) {
+ 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 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()) {
+ promises.add(attributeMapper.patch(requestState, new JsonPointer(), operation));
+ }
+
+ return Promises.when(promises).thenAsync(
+ new AsyncFunction<List<List<Modification>>, ResourceResponse, ResourceException>() {
+ @Override
+ public Promise<ResourceResponse, ResourceException> apply(
+ final List<List<Modification>> result) {
+ // The patch operations have been converted successfully.
+ try {
+ final ModifyRequest modifyRequest = newModifyRequest(dn);
+
+ // Add the modifications.
+ for (final List<Modification> modifications : result) {
+ if (modifications != null) {
+ modifyRequest.getModifications().addAll(modifications);
+ }
+ }
+
+ 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.
+ return connectionHolder.get()
+ .readEntryAsync(dn, attributes)
+ .thenAsync(postEmptyPatchAsyncFunction(requestState, request),
+ ldapExceptionToResourceException());
+ } else {
+ // Add controls and perform the modify request.
+ if (config.readOnUpdatePolicy() == CONTROLS) {
+ modifyRequest.addControl(
+ PostReadRequestControl.newControl(false, attributes));
+ }
+ if (config.usePermissiveModify()) {
+ modifyRequest.addControl(
+ PermissiveModifyRequestControl.newControl(true));
+ }
+ addAssertionControl(modifyRequest, request.getRevision());
+ return connectionHolder.get()
+ .applyChangeAsync(modifyRequest)
+ .thenAsync(postUpdateResultAsyncFunction(requestState),
+ ldapExceptionToResourceException());
+ }
+ } catch (final Exception e) {
+ return Promises.newExceptionPromise(asResourceException(e));
+ }
+ }
+ });
+ }
+ }).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 Promise<QueryResponse, ResourceException> queryCollection(
+ final Context context, final QueryRequest request, final QueryResourceHandler resourceHandler) {
+ final RequestState requestState = wrap(context);
+
+ return requestState.getConnection()
+ .thenAsync(new AsyncFunction<Connection, QueryResponse, ResourceException>() {
+ @Override
+ public Promise<QueryResponse, ResourceException> apply(final Connection connection)
+ throws ResourceException {
+ // Calculate the filter (this may require the connection).
+ 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.
+ */
+ private final Object sequenceLock = new Object();
+ private String cookie;
+ private ResourceException pendingResult;
+ private int pendingResourceCount;
+ private boolean resultSent;
+ private int totalResourceCount;
+
+ @Override
+ public Promise<QueryResponse, ResourceException> apply(final Filter ldapFilter) {
+ if (ldapFilter == null || ldapFilter == alwaysFalse()) {
+ // 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(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.
+ final int pageResultStartIndex;
+ final int pageSize = request.getPageSize();
+ if (request.getPageSize() > 0) {
+ final int pageResultEndIndex;
+ if (request.getPagedResultsOffset() > 0) {
+ pageResultStartIndex = request.getPagedResultsOffset() * pageSize;
+ pageResultEndIndex = pageResultStartIndex + pageSize;
+ } else {
+ pageResultStartIndex = 0;
+ pageResultEndIndex = pageSize;
+ }
+ final ByteString cookie = request.getPagedResultsCookie() != null
+ ? ByteString.valueOfBase64(request.getPagedResultsCookie()) : ByteString.empty();
+ final SimplePagedResultsControl control =
+ SimplePagedResultsControl.newControl(true, pageResultEndIndex, cookie);
+ searchRequest.addControl(control);
+ } else {
+ pageResultStartIndex = 0;
+ }
+
+ 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.
+ synchronized (sequenceLock) {
+ if (pendingResult != null) {
+ return false;
+ }
+ if (totalResourceCount++ < pageResultStartIndex) {
+ // Haven't reached paged results threshold yet.
+ return true;
+ }
+ pendingResourceCount++;
+ }
+
+ /*
+ * 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(requestState, entry);
+ final String revision = getRevisionFromEntry(entry);
+ attributeMapper.read(requestState, new JsonPointer(), entry)
+ .thenOnResult(new ResultHandler<JsonValue>() {
+ @Override
+ public void handleResult(final JsonValue result) {
+ synchronized (sequenceLock) {
+ pendingResourceCount--;
+ if (!resultSent) {
+ resourceHandler.handleResource(
+ Responses.newResourceResponse(id, revision, result));
+ }
+ completeIfNecessary(promise);
+ }
+ }
+ }).thenOnException(new ExceptionHandler<ResourceException>() {
+ @Override
+ public void handleException(ResourceException exception) {
+ synchronized (sequenceLock) {
+ pendingResourceCount--;
+ completeIfNecessary(exception, promise);
+ }
+ }
+ });
+ return true;
+ }
+
+ @Override
+ public boolean handleReference(final SearchResultReference reference) {
+ // TODO: should this be classed as an error since
+ // rest2ldap assumes entries are all colocated?
+ return true;
+ }
+
+ }).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);
+ if (control != null && !control.getCookie().isEmpty()) {
+ cookie = control.getCookie().toBase64String();
+ }
+ } catch (final DecodeException e) {
+ // FIXME: need some logging.
+ }
+ }
+ completeIfNecessary(SUCCESS, promise);
+ }
+ }
+ }).thenOnException(new ExceptionHandler<LdapException>() {
+ @Override
+ public void handleException(LdapException exception) {
+ synchronized (sequenceLock) {
+ completeIfNecessary(asResourceException(exception), promise);
}
}
});
+
+ return promise;
}
- });
- }
- @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);
-
- // Get connection then, search for the existing entry, then modify.
- c.run(h, new Runnable() {
- @Override
- public void run() {
- final String[] attributes = getLDAPAttributes(c, Collections.<JsonPointer> emptyList());
- final SearchRequest searchRequest = nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId)
- .addAttribute(attributes);
-
- c.getConnection().searchSingleEntryAsync(searchRequest)
- .thenOnResult(new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() {
- @Override
- public void handleResult(final SearchResultEntry entry) {
- try {
- // Fail-fast if there is a version mismatch.
- ensureMVCCVersionMatches(entry, request.getRevision());
-
- // 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));
- }
- if (config.usePermissiveModify()) {
- 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>>() {
- @Override
- public void handleError(final ResourceException error) {
- h.handleError(error);
- }
-
- @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));
- }
- }
- });
- } catch (final Exception e) {
- h.handleError(asResourceException(e));
- }
- }
- }).thenOnException(new ExceptionHandler<LdapException>() {
- @Override
- public void handleException(final LdapException exception) {
- h.handleError(asResourceException(exception));
- }
- });
+ /** 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(handler);
}
- });
- }
- private void adaptEntry(final Context c, final Entry entry, final ResultHandler<Resource> handler) {
- final String actualResourceId = nameStrategy.getResourceId(c, entry);
- final String revision = getRevisionFromEntry(entry);
- attributeMapper.read(c, new JsonPointer(), entry, transform(
- new Function<JsonValue, Resource, NeverThrowsException>() {
- @Override
- public Resource apply(final JsonValue value) {
- return new Resource(actualResourceId, revision, new JsonValue(value));
+ /**
+ * 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(final PromiseImpl<QueryResponse, ResourceException> handler) {
+ if (pendingResourceCount == 0 && pendingResult != null && !resultSent) {
+ if (pendingResult == SUCCESS) {
+ handler.handleResult(Responses.newQueryResponse(cookie));
+ } else {
+ handler.handleException(pendingResult);
}
- }, handler));
+ resultSent = true;
+ }
+ }
+ };
+ }
+
+ @Override
+ public Promise<ResourceResponse, ResourceException> readInstance(
+ final Context context, final String resourceId, final ReadRequest request) {
+ final RequestState requestState = wrap(context);
+
+ return requestState.getConnection()
+ .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
+ @Override
+ 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);
+
+ 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 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());
+
+ // Create the modify request.
+ final ModifyRequest modifyRequest = newModifyRequest(entry.getName());
+ if (config.readOnUpdatePolicy() == CONTROLS) {
+ final String[] attributes =
+ getLDAPAttributes(requestState, request.getFields());
+ modifyRequest.addControl(
+ PostReadRequestControl.newControl(false, attributes));
+ }
+ if (config.usePermissiveModify()) {
+ modifyRequest.addControl(
+ PermissiveModifyRequestControl.newControl(true));
+ }
+ addAssertionControl(modifyRequest, request.getRevision());
+
+ // 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 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);
+ }
+ // Perform the modify operation.
+ modifyRequest.getModifications().addAll(modifications);
+ return connection.applyChangeAsync(modifyRequest).thenAsync(
+ postUpdateResultAsyncFunction(requestState),
+ ldapExceptionToResourceException());
+ }
+ });
+ } catch (final Exception e) {
+ return Promises.newExceptionPromise(asResourceException(e));
+ }
+ }
+ }, ldapExceptionToResourceException());
+ }
+ }).thenFinally(close(requestState));
+ }
+
+ private Promise<ResourceResponse, ResourceException> adaptEntry(
+ final RequestState requestState, final Entry entry) {
+ final String actualResourceId = nameStrategy.getResourceId(requestState, entry);
+ final String revision = getRevisionFromEntry(entry);
+ return attributeMapper.read(requestState, new JsonPointer(), entry)
+ .then(new Function<JsonValue, ResourceResponse, ResourceException>() {
+ @Override
+ public ResourceResponse apply(final JsonValue value) {
+ return Responses.newResourceResponse(
+ actualResourceId, revision, new JsonValue(value));
+ }
+ });
}
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.
+ * 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);
- }
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java
index 323f962..69e764e 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java
+++ b/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;
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
index 9b4124c..03cc53a 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
+++ b/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>() {
- @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.
- */
- 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) {
- result.put(e.getKey(), e.getValue().getObject());
- }
- return new JsonValue(result);
- }
- }
- }, h));
+ final List<Promise<Map.Entry<String, JsonValue>, ResourceException>> promises =
+ new ArrayList<>(mappings.size());
for (final Mapping mapping : mappings.values()) {
- mapping.mapper.read(c, path.child(mapping.name), e, transform(
- new Function<JsonValue, Map.Entry<String, JsonValue>, NeverThrowsException>() {
+ 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 value != null ? new SimpleImmutableEntry<String, JsonValue>(mapping.name, value)
+ : null;
}
- }, handler));
+ }));
}
+
+ 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.
+ */
+ 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);
+ }
+ }
+ });
}
@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);
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
index a0f515c..f259cb2 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
+++ b/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,64 +130,67 @@
}
@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) {
- @Override
- public void handleResult(final Filter result) {
- // Search for all referenced entries and construct a filter.
- final SearchRequest request = createSearchRequest(result);
- final List<Filter> subFilters = new LinkedList<>();
+ return mapper.getLDAPFilter(requestState, path, subPath, type, operator, valueAssertion)
+ .thenAsync(new AsyncFunction<Filter, Filter, ResourceException>() {
+ @Override
+ 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>() {
- @Override
- public void handleException(LdapException exception) {
- h.handleError(asResourceException(exception)); // Propagate.
- }
- };
+ return requestState.getConnection().thenAsync(
+ new AsyncFunction<Connection, Filter, ResourceException>() {
+ @Override
+ 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()));
+ return true;
+ } else {
+ // No point in continuing - maximum candidates reached.
+ return false;
+ }
+ }
- c.getConnection().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()));
- return true;
- } else {
- // No point in continuing - maximum candidates reached.
- return false;
- }
+ @Override
+ public boolean handleReference(final SearchResultReference reference) {
+ // Ignore references.
+ return true;
+ }
+ }).then(new Function<Result, Filter, ResourceException>() {
+ @Override
+ public Filter apply(Result result) throws ResourceException {
+ if (subFilters.size() >= SEARCH_MAX_CANDIDATES) {
+ throw asResourceException(
+ newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED));
+ } else if (subFilters.size() == 1) {
+ return subFilters.get(0);
+ } else {
+ return Filter.or(subFilters);
+ }
+ }
+ }, new Function<LdapException, Filter, ResourceException>() {
+ @Override
+ public Filter apply(LdapException exception) throws ResourceException {
+ throw asResourceException(exception);
+ }
+ });
+ }
+ });
}
-
- @Override
- public boolean handleReference(final SearchResultReference reference) {
- // Ignore references.
- return true;
- }
- }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Result>() {
- @Override
- public void handleResult(Result result) {
- if (subFilters.size() >= SEARCH_MAX_CANDIDATES) {
- exceptionHandler.handleException(newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED));
- } else if (subFilters.size() == 1) {
- h.handleResult(subFilters.get(0));
- } else {
- h.handleResult(Filter.or(subFilters));
- }
- }
- }).thenOnException(exceptionHandler);
- }
- });
+ });
}
@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,68 +213,72 @@
}
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>() {
- @Override
- public void handleResult(final SearchResultEntry result) {
- synchronized (newLDAPAttribute) {
- newLDAPAttribute.add(result.getName());
- }
- completeIfNecessary();
- }
- }).thenOnException(new ExceptionHandler<LdapException>() {
- @Override
- public void handleException(final LdapException error) {
- ResourceException re;
- try {
- 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",
- 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",
- primaryKeyValue.toString(), path));
- } catch (final LdapException e) {
- re = asResourceException(e);
- }
- exception.compareAndSet(null, re);
- completeIfNecessary();
- }
- });
+ 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) {
+ newLDAPAttribute.add(result.getName());
+ }
+ completeIfNecessary();
+ }
+ }).thenOnException(new ExceptionHandler<LdapException>() {
+ @Override
+ public void handleException(final LdapException error) {
+ ResourceException re;
+ try {
+ 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",
+ 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",
+ primaryKeyValue.toString(), path));
+ } catch (final LdapException e) {
+ re = asResourceException(e);
+ }
+ exception.compareAndSet(null, re);
+ completeIfNecessary();
+ }
+ });
+ }
+ });
}
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,47 +287,47 @@
}
@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>() {
- @Override
- public JsonValue apply(final List<JsonValue> value) {
- if (value.isEmpty()) {
- /*
- * No values, so omit the entire JSON object
- * from the resource.
- */
- return null;
- } else {
- // Combine values into a single JSON array.
- final List<Object> result = new ArrayList<>(value.size());
- for (final JsonValue e : value) {
- result.add(e.getObject());
- }
- return new JsonValue(result);
- }
- }
- }, h));
+
+ final List<Promise<JsonValue, ResourceException>> promises = new ArrayList<>(dns.size());
for (final DN dn : dns) {
- readEntry(c, path, dn, handler);
+ 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.
+ return null;
+ } else {
+ // Combine values into a single JSON array.
+ final List<Object> result = new ArrayList<>(value.size());
+ for (final JsonValue e : value) {
+ result.add(e.getObject());
+ }
+ return new JsonValue(result);
+ }
+ }
+ });
} 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>() {
- @Override
- public void handleResult(final SearchResultEntry result) {
- mapper.read(c, path, result, handler);
- }
- }).thenOnException(new ExceptionHandler<LdapException>() {
- @Override
- public void handleException(final LdapException error) {
- if (!(error instanceof EntryNotFoundException)) {
- handler.handleError(asResourceException(error));
- } else {
- /*
- * The referenced entry does not exist so ignore it
- * since it cannot be mapped.
- */
- handler.handleResult(null);
- }
- }
- });
+ mapper.getLDAPAttributes(requestState, path, new JsonPointer(), requestedLDAPAttributes);
+ return requestState.getConnection().thenAsync(new AsyncFunction<Connection, JsonValue, ResourceException>() {
+ @Override
+ 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);
+ }
+ }, new AsyncFunction<LdapException, JsonValue, ResourceException>() {
+ @Override
+ public Promise<JsonValue, ResourceException> apply(final LdapException error) {
+ if (!(error instanceof EntryNotFoundException)) {
+ return Promises.newExceptionPromise(asResourceException(error));
+ } else {
+ // The referenced entry does not exist so ignore it since it cannot be mapped.
+ return Promises.newResultPromise(null);
+ }
+ }
+ });
+ }
+ });
}
-
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java
similarity index 90%
rename from opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java
rename to opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java
index a27e3e0..825736a 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.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")));
}
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
index 74d250d..a6e3501 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
+++ b/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,
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
new file mode 100644
index 0000000..5b7588d
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
@@ -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;
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
index a8f99bd..0035d55 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
+++ b/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;
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
index 0a441de..2cc8b2a 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
+++ b/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;
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
index 6311a92..9c9d9c4 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
+++ b/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.
- }
+ connection.read(ctx(), newReadRequest("/test1"));
}
- @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.
- }
+ connection.read(ctx(), newReadRequest("/test1"));
}
@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),
- increment("/multiNumber", 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")))));
}
}
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java
index cef00f4..a3aa64c 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java
+++ b/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);
}
/**
diff --git a/opendj-server-legacy/pom.xml b/opendj-server-legacy/pom.xml
index 89be8db..f5cf65a 100644
--- a/opendj-server-legacy/pom.xml
+++ b/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>
@@ -129,6 +129,28 @@
<groupId>org.slf4j</groupId>
<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>
diff --git a/opendj-server-legacy/src/main/assembly/opendj-archive-component.xml b/opendj-server-legacy/src/main/assembly/opendj-archive-component.xml
index 7e5925c..1bf90a3 100644
--- a/opendj-server-legacy/src/main/assembly/opendj-archive-component.xml
+++ b/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>
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/CollectClientConnectionsFilter.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
index 950b5c5..fbbc2c4 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
+++ b/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,269 +96,242 @@
* 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)
- {
- final HttpServletRequest request = (HttpServletRequest) req;
- final HttpServletResponse response = (HttpServletResponse) resp;
-
- final HTTPRequestContext ctx = new HTTPRequestContext();
-
- ctx.request = request;
- ctx.response = new HttpServletResponseWrapper(response)
+ if (connectionHandler.keepStats())
{
-
- /** {@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>() {
- @Override
- public Promise<BindResult, LdapException> apply(SearchResultEntry resultEntry) throws LdapException
- {
- 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);
- }
- }
-
- }
- ).thenOnResult(new ResultHandler<BindResult>() {
- @Override
- public void handleResult(BindResult result)
- {
- ctx.clientConnection.setAuthUser(ctx.userName);
- try
- {
- doFilter(ctx);
- }
- catch (Exception e)
- {
- onException(e, ctx);
- }
- }
- }).thenOnException(new ExceptionHandler<LdapException>(){
- @Override
- public void handleException(LdapException exception)
- {
- final ResultCode rc = exception.getResult().getResultCode();
- if (ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED.equals(rc)
- || ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED.equals(rc))
- {
- // Avoid information leak:
- // do not hint to the user that it is the username that is invalid
- sendAuthenticationFailure(ctx);
- }
- else
- {
- onException(exception, ctx);
- }
- }
- });
+ 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
- doFilter(ctx);
+ // Use unauthenticated user
+ return doFilter(context, request, next, connection);
}
else
{
- sendAuthenticationFailure(ctx);
+ return authenticationFailure(clientConnection);
}
}
catch (Exception e)
{
- onException(e, ctx);
+ return asErrorResponse(e, clientConnection);
}
}
- private void doFilter(HTTPRequestContext ctx)
- throws Exception
+ private boolean canProcessRequest(final HTTPClientConnection connection) throws UnknownHostException
{
- /*
- * 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);
+ final InetAddress clientAddr = connection.getRemoteAddress();
- // send the request further down the filter chain or pass to servlet
- ctx.chain.doFilter(ctx.request, ctx.response);
- }
-
- private void sendAuthenticationFailure(HTTPRequestContext ctx)
- {
- final int statusCode = HttpServletResponse.SC_UNAUTHORIZED;
- try
+ // Check to see if the core server rejected the connection (e.g. already too many connections established).
+ if (connection.getConnectionID() < 0)
{
- // 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
- {
- 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.debug(message);
-
- ctx.clientConnection.disconnect(DisconnectReason.SERVER_ERROR, false,
- message);
- }
- finally
- {
- ctx.clientConnection.log(ex.getCode());
-
- if (ctx.asyncContext != null)
- {
- ctx.asyncContext.complete();
- }
- }
- }
-
- 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());
+ 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.
- ConnectionHandlerCfg config = this.connectionHandler.getCurrentConfig();
- Collection<AddressMask> allowedClients = config.getAllowedClient();
- Collection<AddressMask> deniedClients = config.getDeniedClient();
+ // 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))
{
- clientConnection.disconnect(DisconnectReason.CONNECTION_REJECTED, false,
- ERR_CONNHANDLER_DENIED_CLIENT.get(clientConnection
- .getClientHostPort(), clientConnection.getServerHostPort()));
+ 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.
+
+ // 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))
{
- clientConnection.disconnect(DisconnectReason.CONNECTION_REJECTED, false,
- ERR_CONNHANDLER_DISALLOWED_CLIENT.get(clientConnection
- .getClientHostPort(), clientConnection.getServerHostPort()));
+ 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<Response, NeverThrowsException> apply(final SearchResultEntry resultEntry)
+ {
+ final DN bindDN = resultEntry.getName();
+ if (bindDN == null)
+ {
+ 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));
+ }
+ };
+ }
+
+ private AsyncFunction<BindResult, Response, NeverThrowsException> doChain(
+ final Context context, final Request request, final Handler next, final String userName,
+ final HTTPClientConnection clientConnection, final Connection connection)
+ {
+ return new AsyncFunction<BindResult, Response, NeverThrowsException>()
+ {
+ @Override
+ public Promise<Response, NeverThrowsException> apply(BindResult value) throws NeverThrowsException
+ {
+ clientConnection.setAuthUser(userName);
+ try
+ {
+ return doFilter(context, request, next, connection);
+ }
+ catch (Exception e)
+ {
+ return asErrorResponse(e, clientConnection);
+ }
+ }
+ };
+ }
+
+ 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 Promise<Response, NeverThrowsException> apply(final LdapException exception)
+ {
+ final ResultCode rc = exception.getResult().getResultCode();
+ if (ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED.equals(rc)
+ || ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED.equals(rc))
+ {
+ // Avoid information leak:
+ // do not hint to the user that it is the username that is invalid
+ return authenticationFailure(clientConnection);
+ }
+ else
+ {
+ return asErrorResponse(exception, clientConnection);
+ }
+ }
+ };
+ }
+
+ private AsyncFunction<LdapException, Response, NeverThrowsException> returnErrorAfterFailedBind(
+ final HTTPClientConnection clientConnection)
+ {
+ return new AsyncFunction<LdapException, Response, NeverThrowsException>()
+ {
+ @Override
+ public Promise<Response, NeverThrowsException> apply(final LdapException e)
+ {
+ return asErrorResponse(e, clientConnection);
+ }
+ };
+ }
+
+ private Promise<Response, NeverThrowsException> authenticationFailure(final HTTPClientConnection clientConnection)
+ {
+ 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
+ {
+ LocalizableMessage message = null;
+ if (logError)
+ {
+ logger.traceException(ex);
+ message = INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(
+ clientConnection.getClientHostPort(), clientConnection.getServerHostPort(), getExceptionMessage(ex));
+ logger.debug(message);
+ }
+ clientConnection.disconnect(reason, false, message);
+ }
+ finally
+ {
+ clientConnection.log(ex.getCode());
+ }
+
+ return resourceExceptionToPromise(ex);
+ }
+
+ Promise<Response, NeverThrowsException> resourceExceptionToPromise(final ResourceException e)
+ {
+ final Response response = new Response().setStatus(Status.valueOf(e.getCode()))
+ .setEntity(e.toJsonValue().getObject());
+ if (e.getCode() == 401 && authConfig.isBasicAuthenticationSupported())
+ {
+ response.getHeaders().add("WWW-Authenticate", "Basic realm=\"org.forgerock.opendj\"");
+ }
+ return Promises.newResultPromise(response);
+ }
+
/**
* Extracts the username and password from the request using one of the
* enabled authentication mechanism: HTTP Basic authentication or HTTP Custom
@@ -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 {}
}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPClientConnection.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPClientConnection.java
index 5251ee6..b2447ef 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPClientConnection.java
+++ b/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()
{
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java
index e76f317..934b78f 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java
+++ b/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)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LdapHttpApplication.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LdapHttpApplication.java
new file mode 100644
index 0000000..efd5942
--- /dev/null
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LdapHttpApplication.java
@@ -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;
+ }
+}
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/protocol.properties b/opendj-server-legacy/src/messages/org/opends/messages/protocol.properties
index 91d6c42..9560963 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/protocol.properties
+++ b/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
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java b/opendj-server-legacy/src/test/java/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
index c207865..ad82137 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
+++ b/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);
}
diff --git a/pom.xml b/pom.xml
index 7afa5ad..0d266c3 100644
--- a/pom.xml
+++ b/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 -->
--
Gitblit v1.10.0