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

Jean-Noël Rouvignac
29.48.2016 e4c0edea06c8fee28369f03f393b7d54b2b6235c
OPENDJ-3246 Return the CREST descriptor over REST for rest2ldap endpoints

The CREST descriptor is returned when providing the "_crestApi" URL query parameter

ApiDescription is generated by the rest2ldap datamodel.
3 files added
19 files modified
1208 ■■■■■ changed files
opendj-cli/src/main/java/com/forgerock/opendj/cli/ToolVersionHandler.java 23 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/com/forgerock/opendj/util/ManifestUtil.java 87 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLdapPropertyMapper.java 32 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractRequestHandler.java 29 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DescribableRequestHandler.java 132 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonConstantPropertyMapper.java 75 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java 52 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/PropertyMapper.java 6 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java 21 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java 409 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ResourceTypePropertyMapper.java 17 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java 53 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java 27 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimplePropertyMapper.java 38 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java 7 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java 19 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java 7 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java 109 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/core/HttpEndpointConfigManager.java 5 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java 21 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java 28 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/util/BuildVersion.java 11 ●●●●● patch | view | raw | blame | history
opendj-cli/src/main/java/com/forgerock/opendj/cli/ToolVersionHandler.java
@@ -15,12 +15,7 @@
 */
package com.forgerock.opendj.cli;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import com.forgerock.opendj.util.ManifestUtil;
/** Class that prints the version of the SDK to System.out. */
public final class ToolVersionHandler implements VersionHandler {
@@ -64,20 +59,6 @@
    }
    private String getVersion() {
        try {
            final Enumeration<URL> manifests = getClass().getClassLoader().getResources("META-INF/MANIFEST.MF");
            while (manifests.hasMoreElements()) {
                final URL manifestUrl = manifests.nextElement();
                if (manifestUrl.toString().contains(moduleName)) {
                    try (InputStream manifestStream = manifestUrl.openStream()) {
                        final Attributes attrs = new Manifest(manifestStream).getMainAttributes();
                        return attrs.getValue("Bundle-Version") + " (revision " + attrs.getValue("SCM-Revision") + ")";
                    }
                }
            }
            return null;
        } catch (IOException e) {
            throw new RuntimeException("IOException while determining opendj tool version", e);
        }
        return ManifestUtil.getVersionWithRevision(moduleName);
    }
}
opendj-core/src/main/java/com/forgerock/opendj/util/ManifestUtil.java
New file
@@ -0,0 +1,87 @@
/*
 * 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 2016 ForgeRock AS.
 */
package com.forgerock.opendj.util;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import org.forgerock.util.Pair;
/** Utility methods reading information from {@code opendj-core}'s manifest. */
public final class ManifestUtil {
    private static final String OPENDJ_CORE_VERSION;
    private static final String OPENDJ_CORE_VERSION_WITH_REVISION;
    static {
        final Pair<String, String> versions = getVersions("opendj-core");
        OPENDJ_CORE_VERSION = versions.getFirst();
        OPENDJ_CORE_VERSION_WITH_REVISION = versions.getSecond();
    }
    /**
     * Returns the version with the revision contained in the module manifest whose name is provided.
     *
     * @param moduleName The module name for which to retrieve the version number
     * @return the version with the revision contained in the module manifest whose name is provided.
     */
    public static String getVersionWithRevision(String moduleName) {
        if ("opendj-core".equals(moduleName)) {
            return OPENDJ_CORE_VERSION_WITH_REVISION;
        }
        return getVersions(moduleName).getSecond();
    }
    /**
     * Returns the bundle version contained in the module manifest whose name is provided.
     *
     * @param moduleName The module name for which to retrieve the version number
     * @return the bundle version contained in the module manifest whose name is provided.
     */
    public static String getBundleVersion(String moduleName) {
        if ("opendj-core".equals(moduleName)) {
            return OPENDJ_CORE_VERSION;
        }
        return getVersions(moduleName).getFirst();
    }
    private static Pair<String, String> getVersions(String moduleName) {
        try {
            final Enumeration<URL> manifests = ManifestUtil.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
            while (manifests.hasMoreElements()) {
                final URL manifestUrl = manifests.nextElement();
                if (manifestUrl.toString().contains(moduleName)) {
                    try (InputStream manifestStream = manifestUrl.openStream()) {
                        final Attributes attrs = new Manifest(manifestStream).getMainAttributes();
                        final String bundleVersion = attrs.getValue("Bundle-Version");
                        return Pair.of(bundleVersion,
                                       bundleVersion + " (revision " + attrs.getValue("SCM-Revision") + ")");
                    }
                }
            }
            return null;
        } catch (IOException e) {
            throw new RuntimeException("IOException while determining opendj tool version", e);
        }
    }
    private ManifestUtil() {
        // do not instantiate util classes
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLdapPropertyMapper.java
@@ -18,6 +18,8 @@
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.forgerock.api.enums.WritePolicy.WRITE_ON_CREATE;
import static org.forgerock.opendj.ldap.Attributes.emptyAttribute;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.isNullOrEmpty;
@@ -85,6 +87,16 @@
        return getThis();
    }
    @Override
    boolean isRequired() {
        return isRequired;
    }
    @Override
    boolean isMultiValued() {
        return isMultiValued;
    }
    /**
     * Indicates whether the LDAP attribute supports updates. The default is {@link WritabilityPolicy#READ_WRITE}.
     *
@@ -351,4 +363,24 @@
        }
    }
    void putWritabilityProperties(JsonValue jsonSchema) {
        switch (writabilityPolicy != null ? writabilityPolicy : WritabilityPolicy.READ_WRITE) {
        case CREATE_ONLY:
            jsonSchema.put("writePolicy", WRITE_ON_CREATE.toString());
            jsonSchema.put("errorOnWritePolicyFailure", true);
            break;
        case CREATE_ONLY_DISCARD_WRITES:
            jsonSchema.put("writePolicy", WRITE_ON_CREATE.toString());
            break;
        case READ_ONLY:
            jsonSchema.put("readOnly", true);
            jsonSchema.put("errorOnWritePolicyFailure", true);
            break;
        case READ_ONLY_DISCARD_WRITES:
            jsonSchema.put("readOnly", true);
            break;
        default:
            break;
        }
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractRequestHandler.java
@@ -15,6 +15,8 @@
 */
package org.forgerock.opendj.rest2ldap;
import org.forgerock.api.models.ApiDescription;
import org.forgerock.http.ApiProducer;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.CreateRequest;
@@ -31,13 +33,14 @@
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.services.context.Context;
import org.forgerock.services.descriptor.Describable;
import org.forgerock.util.promise.Promise;
/**
 * An abstract base class from which request handlers may be easily implemented. The default implementation of each
 * method is to invoke the {@link #handleRequest(Context, Request)} method.
 */
public abstract class AbstractRequestHandler implements RequestHandler {
public abstract class AbstractRequestHandler implements RequestHandler, Describable<ApiDescription, Request> {
    /** Creates a new {@code AbstractRequestHandler}. */
    protected AbstractRequestHandler() {
        // Nothing to do.
@@ -96,4 +99,28 @@
    protected <V> Promise<V, ResourceException> handleRequest(final Context context, final Request request) {
        return new NotSupportedException().asPromise();
    }
    @Override
    public ApiDescription api(ApiProducer<ApiDescription> producer) {
        // api descriptions that are null will be ignored
        return null;
    }
    @Override
    public ApiDescription handleApiRequest(Context context, Request request) {
        // api requests are handled at a higher level by org.forgerock.opendj.rest2ldap.DescribableRequestHandler.
        // So this code is never reached.
        throw new UnsupportedOperationException("This should be handled by "
            + "org.forgerock.opendj.rest2ldap.DescribableRequestHandler.handleApiRequest()");
    }
    @Override
    public void addDescriptorListener(Describable.Listener listener) {
        // nothing to do
    }
    @Override
    public void removeDescriptorListener(Describable.Listener listener) {
        // nothing to do
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DescribableRequestHandler.java
New file
@@ -0,0 +1,132 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
import org.forgerock.api.models.ApiDescription;
import org.forgerock.http.ApiProducer;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.QueryResourceHandler;
import org.forgerock.json.resource.QueryResponse;
import org.forgerock.json.resource.ReadRequest;
import org.forgerock.json.resource.Request;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.services.context.Context;
import org.forgerock.services.descriptor.Describable;
import org.forgerock.util.Reject;
import org.forgerock.util.promise.Promise;
/** Decorator for a request handler that can return an api descriptor of the underlying handler. */
public class DescribableRequestHandler implements RequestHandler, Describable<ApiDescription, Request> {
    private final RequestHandler delegate;
    private final Describable<ApiDescription, Request> describableDelegate;
    private ApiDescription api;
    /**
     * Builds an object decorating the provided handler.
     *
     * @param handler
     *          the handler to decorate.
     */
    @SuppressWarnings("unchecked")
    public DescribableRequestHandler(final RequestHandler handler) {
        this.delegate = Reject.checkNotNull(handler);
        this.describableDelegate = delegate instanceof Describable
            ? (Describable<ApiDescription, Request>) delegate
            : null;
    }
    @Override
    public Promise<ActionResponse, ResourceException> handleAction(Context context, ActionRequest request) {
        return delegate.handleAction(wrap(context), request);
    }
    @Override
    public Promise<ResourceResponse, ResourceException> handleCreate(Context context, CreateRequest request) {
        return delegate.handleCreate(wrap(context), request);
    }
    @Override
    public Promise<ResourceResponse, ResourceException> handleDelete(Context context, DeleteRequest request) {
        return delegate.handleDelete(wrap(context), request);
    }
    @Override
    public Promise<ResourceResponse, ResourceException> handlePatch(Context context, PatchRequest request) {
        return delegate.handlePatch(wrap(context), request);
    }
    @Override
    public Promise<QueryResponse, ResourceException> handleQuery(
            Context context, QueryRequest request, QueryResourceHandler handler) {
        return delegate.handleQuery(wrap(context), request, handler);
    }
    @Override
    public Promise<ResourceResponse, ResourceException> handleRead(Context context, ReadRequest request) {
        return delegate.handleRead(wrap(context), request);
    }
    @Override
    public Promise<ResourceResponse, ResourceException> handleUpdate(Context context, UpdateRequest request) {
        return delegate.handleUpdate(wrap(context), request);
    }
    /**
     * Allows sub classes to wrap the provided context and return the wrapping context.
     *
     * @param context
     *          the context to wrap
     * @return the wrapping context that should be used
     */
    protected Context wrap(final Context context) {
        return context;
    }
    @Override
    public ApiDescription api(ApiProducer<ApiDescription> producer) {
        if (describableDelegate != null) {
            api = describableDelegate.api(producer);
        }
        return api;
    }
    @Override
    public ApiDescription handleApiRequest(Context context, Request request) {
        return api;
    }
    @Override
    public void addDescriptorListener(Describable.Listener listener) {
        if (describableDelegate != null) {
            describableDelegate.addDescriptorListener(listener);
        }
    }
    @Override
    public void removeDescriptorListener(Describable.Listener listener) {
        if (describableDelegate != null) {
            describableDelegate.removeDescriptorListener(listener);
        }
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonConstantPropertyMapper.java
@@ -16,6 +16,7 @@
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
import static org.forgerock.json.JsonValue.*;
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.ldap.Filter.alwaysTrue;
import static org.forgerock.opendj.rest2ldap.Utils.isNullOrEmpty;
@@ -50,6 +51,16 @@
    }
    @Override
    boolean isRequired() {
        return false;
    }
    @Override
    boolean isMultiValued() {
        return false;
    }
    @Override
    public String toString() {
        return "constant(" + value + ")";
    }
@@ -75,39 +86,38 @@
                                                     final JsonPointer path, final JsonPointer subPath,
                                                     final FilterType type, final String operator,
                                                     final Object valueAssertion) {
        final Filter filter;
        return newResultPromise(getLdapFilter0(subPath, type, valueAssertion));
    }
    private Filter getLdapFilter0(final JsonPointer subPath, final FilterType type, final Object valueAssertion) {
        final JsonValue subValue = value.get(subPath);
        if (subValue == null) {
            filter = alwaysFalse();
            return alwaysFalse();
        } else if (type == FilterType.PRESENT) {
            filter = alwaysTrue();
            return alwaysTrue();
        } else if (value.isString() && valueAssertion instanceof String) {
            final String v1 = toLowerCase(value.asString());
            final String v2 = toLowerCase((String) valueAssertion);
            switch (type) {
            case CONTAINS:
                filter = toFilter(v1.contains(v2));
                break;
                return toFilter(v1.contains(v2));
            case STARTS_WITH:
                filter = toFilter(v1.startsWith(v2));
                break;
                return toFilter(v1.startsWith(v2));
            default:
                filter = compare(type, v1, v2);
                break;
                return compare(type, v1, v2);
            }
        } else if (value.isNumber() && valueAssertion instanceof Number) {
            final Double v1 = value.asDouble();
            final Double v2 = ((Number) valueAssertion).doubleValue();
            filter = compare(type, v1, v2);
            return compare(type, v1, v2);
        } else if (value.isBoolean() && valueAssertion instanceof Boolean) {
            final Boolean v1 = value.asBoolean();
            final Boolean v2 = (Boolean) valueAssertion;
            filter = compare(type, v1, v2);
            return compare(type, v1, v2);
        } else {
            // This property mapper is a candidate but it does not match.
            filter = alwaysFalse();
            return alwaysFalse();
        }
        return newResultPromise(filter);
    }
    @Override
@@ -148,4 +158,43 @@
            return alwaysFalse(); // Not supported.
        }
    }
    @Override
    JsonValue toJsonSchema() {
        return toJsonSchema(value);
    }
    private static JsonValue toJsonSchema(JsonValue value) {
        if (value.isMap()) {
            final JsonValue jsonSchema = json(object(field("type", "object")));
            final JsonValue jsonProps = json(object());
            for (String key : value.keys()) {
                jsonProps.put(key, toJsonSchema(value.get(key)));
            }
            jsonSchema.put("properties", jsonSchema);
            return jsonSchema;
        } else if (value.isCollection()) {
            final JsonValue jsonSchema = json(object(field("type", "array")));
            final JsonValue firstItem = value.get(value.keys().iterator().next());
            // assume all items have the same schema
            jsonSchema.put("items", toJsonSchema(firstItem));
            if (value.isSet()) {
                jsonSchema.put("uniqueItems", true);
            }
            return jsonSchema;
        } else if (value.isBoolean()) {
            return json(object(field("type", "boolean"),
                               field("default", value)));
        } else if (value.isString()) {
            return json(object(field("type", "string"),
                               field("default", value)));
        } else if (value.isNumber()) {
            return json(object(field("type", "number"),
                               field("default", value)));
        } else if (value.isNull()) {
            return json(object(field("type", "null")));
        } else {
            return null;
        }
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java
@@ -17,6 +17,7 @@
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.simple;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
import static org.forgerock.json.JsonValue.*;
import static org.forgerock.json.resource.PatchOperation.operation;
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException;
@@ -75,11 +76,21 @@
        // Nothing to do.
    }
    @Override
    boolean isRequired() {
        return false;
    }
    @Override
    boolean isMultiValued() {
        return false;
    }
    /**
     * Creates an explicit mapping for a property contained in the JSON object. When user attributes are
     * {@link #includeAllUserAttributesByDefault included} by default, be careful to {@link
     * #excludedDefaultUserAttributes exclude} any attributes which have explicit mappings defined using this method,
     * otherwise they will be duplicated in the JSON representation.
     * {@link #includeAllUserAttributesByDefault(boolean) included} by default, be careful to {@link
     * #excludedDefaultUserAttributes(Collection) exclude} any attributes which have explicit mappings defined using
     * this method, otherwise they will be duplicated in the JSON representation.
     *
     * @param name
     *            The name of the JSON property to be mapped.
@@ -94,8 +105,8 @@
    /**
     * Specifies whether all LDAP user attributes should be mapped by default using the default schema based mapping
     * rules. Individual attributes can be excluded using {@link #excludedDefaultUserAttributes} in order to prevent
     * attributes with explicit mappings being mapped twice.
     * rules. Individual attributes can be excluded using {@link #excludedDefaultUserAttributes(Collection)} in order
     * to prevent attributes with explicit mappings being mapped twice.
     *
     * @param include {@code true} if all LDAP user attributes be mapped by default.
     * @return A reference to this property mapper.
@@ -107,8 +118,8 @@
    /**
     * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when
     * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be
     * excluded in order to prevent duplication.
     * enabled using {@link #includeAllUserAttributesByDefault(boolean)}. Attributes which have explicit mappings
     * should be excluded in order to prevent duplication.
     *
     * @param attributeNames The list of attributes to be excluded.
     * @return A reference to this property mapper.
@@ -119,8 +130,8 @@
    /**
     * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when
     * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be
     * excluded in order to prevent duplication.
     * enabled using {@link #includeAllUserAttributesByDefault(boolean)}. Attributes which have explicit mappings
     * should be excluded in order to prevent duplication.
     *
     * @param attributeNames The list of attributes to be excluded.
     * @return A reference to this property mapper.
@@ -427,4 +438,27 @@
        return includeAllUserAttributesByDefault
                && (excludedDefaultUserAttributes.isEmpty() || !excludedDefaultUserAttributes.contains(attributeName));
    }
    @Override
    JsonValue toJsonSchema() {
        final List<String> requiredFields = new ArrayList<>();
        final JsonValue jsonProps = json(object());
        for (Mapping mapping : mappings.values()) {
            final String attribute = mapping.name;
            PropertyMapper mapper = mapping.mapper;
            jsonProps.put(attribute, mapper.toJsonSchema());
            if (mapper.isRequired()) {
                requiredFields.add(attribute);
            }
        }
        final JsonValue jsonSchema = json(object(field("type", "object")));
        if (!requiredFields.isEmpty()) {
            jsonSchema.put("required", requiredFields);
        }
        if (jsonProps.size() > 0) {
            jsonSchema.put("properties", jsonProps);
        }
        return jsonSchema;
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/PropertyMapper.java
@@ -40,6 +40,10 @@
        // Nothing to do.
    }
    abstract boolean isRequired();
    abstract boolean isMultiValued();
    /**
     * 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
@@ -193,4 +197,6 @@
    // TODO: methods for obtaining schema information (e.g. name, description, type information).
    // TODO: methods for creating sort controls.
    abstract JsonValue toJsonSchema();
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java
@@ -15,14 +15,15 @@
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.json.JsonValue.*;
import static org.forgerock.opendj.ldap.ResultCode.ADMIN_LIMIT_EXCEEDED;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
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.Rest2ldapMessages.*;
import static org.forgerock.opendj.rest2ldap.Utils.connectionFrom;
import static org.forgerock.util.Reject.checkNotNull;
import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
import static org.forgerock.util.Reject.checkNotNull;
import static org.forgerock.util.promise.Promises.newResultPromise;
import java.util.ArrayList;
@@ -67,9 +68,7 @@
 * valued LDAP attribute.
 */
public final class ReferencePropertyMapper extends AbstractLdapPropertyMapper<ReferencePropertyMapper> {
    /**
     * The maximum number of candidate references to allow in search filters.
     */
    /** The maximum number of candidate references to allow in search filters. */
    private static final int SEARCH_MAX_CANDIDATES = 1000;
    private final DnTemplate baseDnTemplate;
@@ -358,4 +357,16 @@
                    }
                });
    }
    @Override
    JsonValue toJsonSchema() {
        if (mapper.isMultiValued()) {
            final JsonValue jsonSchema = json(object(field("type", "array")));
            jsonSchema.put("items", mapper.toJsonSchema());
            jsonSchema.put("uniqueItems", true);
            putWritabilityProperties(jsonSchema);
            return jsonSchema;
        }
        return mapper.toJsonSchema();
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java
@@ -12,11 +12,18 @@
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2016 ForgeRock AS.
 *
 */
package org.forgerock.opendj.rest2ldap;
import static java.util.Arrays.asList;
import static org.forgerock.api.enums.CountPolicy.*;
import static org.forgerock.api.enums.PagingMode.*;
import static org.forgerock.api.enums.ParameterSource.*;
import static org.forgerock.api.enums.PatchOperation.*;
import static org.forgerock.api.enums.Stability.*;
import static org.forgerock.api.models.VersionedPath.*;
import static org.forgerock.json.JsonValue.*;
import static org.forgerock.json.resource.ResourceException.*;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ABSTRACT_TYPE_IN_CREATE;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_MISSING_TYPE_PROPERTY_IN_CREATE;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE;
@@ -34,6 +41,25 @@
import java.util.Map;
import java.util.Set;
import org.forgerock.api.enums.CreateMode;
import org.forgerock.api.enums.QueryType;
import org.forgerock.api.models.ApiDescription;
import org.forgerock.api.models.ApiError;
import org.forgerock.api.models.Create;
import org.forgerock.api.models.Definitions;
import org.forgerock.api.models.Delete;
import org.forgerock.api.models.Errors;
import org.forgerock.api.models.Items;
import org.forgerock.api.models.Parameter;
import org.forgerock.api.models.Patch;
import org.forgerock.api.models.Paths;
import org.forgerock.api.models.Query;
import org.forgerock.api.models.Read;
import org.forgerock.api.models.Reference;
import org.forgerock.api.models.Schema;
import org.forgerock.api.models.Services;
import org.forgerock.api.models.Update;
import org.forgerock.http.ApiProducer;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
@@ -43,11 +69,33 @@
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.LinkedAttribute;
import org.forgerock.util.i18n.LocalizableString;
/**
 * Defines the characteristics of a resource, including its properties, inheritance, and sub-resources.
 */
public final class Resource {
    // Commons errors
    private static final String ERROR_BAD_REQUEST = "frapi:common#/errors/badRequest";
    private static final String ERROR_FORBIDDEN = "frapi:common#/errors/forbidden";
    private static final String ERROR_INTERNAL_SERVER_ERROR = "frapi:common#/errors/internalServerError";
    private static final String ERROR_NOT_FOUND = "frapi:common#/errors/notFound";
    private static final String ERROR_REQUEST_ENTITY_TOO_LARGE = "frapi:common#/errors/requestEntityTooLarge";
    private static final String ERROR_REQUEST_TIMEOUT = "frapi:common#/errors/requestTimeout";
    private static final String ERROR_UNAUTHORIZED = "frapi:common#/errors/unauthorized";
    private static final String ERROR_UNAVAILABLE = "frapi:common#/errors/unavailable";
    private static final String ERROR_VERSION_MISMATCH = "frapi:common#/errors/versionMismatch";
    // rest2ldap errors
    private static final String ERROR_ADMIN_LIMIT_EXCEEDED = "#/errors/adminLimitExceeded";
    private static final String ERROR_READ_FOUND_MULTIPLE_ENTRIES = "#/errors/readFoundMultipleEntries";
    private static final String ERROR_PASSWORD_MODIFY_REQUIRES_HTTPS = "#/errors/passwordModifyRequiresHttps";
    private static final String ERROR_PASSWORD_MODIFY_REQUIRES_AUTHENTICATION = "#/errors/passwordModifyRequiresAuthn";
    /** All fields are queryable, but the directory server may reject some requests (unindexed?). */
    private static final String ALL_FIELDS = "*";
    /** The resource ID. */
    private final String id;
    /** {@code true} if only sub-types of this resource can be created. */
@@ -444,4 +492,363 @@
    PropertyMapper getPropertyMapper() {
        return propertyMapper;
    }
    /**
     * Returns the api description that describes a single instance resource.
     *
     * @param isReadOnly
     *          whether the associated resource is read only
     * @return a new api description that describes a single instance resource.
     */
    ApiDescription instanceApi(boolean isReadOnly) {
        if (allProperties.isEmpty()) {
            return null;
        }
        org.forgerock.api.models.Resource.Builder resource = org.forgerock.api.models.Resource.
            resource()
            .resourceSchema(schemaRef("#/definitions/" + id))
            .mvccSupported(isMvccSupported());
        resource.read(readOperation());
        if (!isReadOnly) {
            resource.update(updateOperation());
            resource.patch(patchOperation());
            for (Action action : supportedActions) {
                resource.action(actions(action));
            }
        }
        return ApiDescription.apiDescription()
                      .id("unused").version("unused")
                      .definitions(definitions())
                      .errors(errors())
                      .build();
    }
    /**
     * Returns the api description that describes a collection resource.
     *
     * @param isReadOnly
     *          whether the associated resource is read only
     * @return a new api description that describes a collection resource.
     */
    ApiDescription collectionApi(boolean isReadOnly) {
        org.forgerock.api.models.Resource.Builder resource = org.forgerock.api.models.Resource.
            resource()
            .resourceSchema(schemaRef("#/definitions/" + id))
            .mvccSupported(isMvccSupported());
        resource.items(buildItems(isReadOnly));
        resource.create(createOperation(CreateMode.ID_FROM_SERVER));
        resource.query(Query.query()
                           .stability(EVOLVING)
                           .type(QueryType.FILTER)
                           .queryableFields(ALL_FIELDS)
                           .pagingModes(COOKIE, OFFSET)
                           .countPolicies(NONE)
                           .error(errorRef(ERROR_BAD_REQUEST))
                           .error(errorRef(ERROR_UNAUTHORIZED))
                           .error(errorRef(ERROR_FORBIDDEN))
                           .error(errorRef(ERROR_REQUEST_TIMEOUT))
                           .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
                           .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
                           .error(errorRef(ERROR_UNAVAILABLE))
                           .build());
        return ApiDescription.apiDescription()
                             .id("unused").version("unused")
                             .definitions(definitions())
                             .services(Services.services()
                                               .put(id, resource.build())
                                               .build())
                             .paths(getPaths())
                             .errors(errors())
                             .build();
    }
    private Paths getPaths() {
        return Paths.paths()
                     // do not put anything in the path to avoid unfortunate string concatenation
                     // also use UNVERSIONED and rely on the router to stamp the version
                     .put("", versionedPath().put(UNVERSIONED, resourceRef("#/services/" + id)).build())
                     .build();
    }
    private Definitions definitions() {
        final Definitions.Builder definitions = Definitions.definitions();
        definitions.put(id, buildJsonSchema());
        for (Resource subType : subTypes) {
            definitions.put(subType.id, subType.buildJsonSchema());
        }
        return definitions.build();
    }
    /**
     * Returns the api description that describes a resource with sub resources.
     *
     * @param producer
     *          the api producer
     * @return a new api description that describes a resource with sub resources.
     */
    ApiDescription subResourcesApi(ApiProducer<ApiDescription> producer) {
        return subResourceRouter.api(producer);
    }
    private boolean isMvccSupported() {
        return allProperties.containsKey("_rev");
    }
    private Items buildItems(boolean isReadOnly) {
        final Items.Builder builder = Items.items();
        builder.pathParameter(Parameter
                              .parameter()
                              .name("id")
                              .type("string")
                              .source(PATH)
                              .required(true)
                              .build())
               .read(readOperation());
        if (!isReadOnly) {
            builder.create(createOperation(CreateMode.ID_FROM_CLIENT));
            builder.update(updateOperation());
            builder.delete(deleteOperation());
            builder.patch(patchOperation());
            for (Action action : supportedActions) {
                builder.action(actions(action));
            }
        }
        return builder.build();
    }
    private org.forgerock.api.models.Action actions(Action action) {
        switch (action) {
        case MODIFY_PASSWORD:
            return modifyPasswordAction();
        case RESET_PASSWORD:
            return resetPasswordAction();
        default:
            throw new RuntimeException("Not implemented for action " + action);
        }
    }
    private static Create createOperation(CreateMode createMode) {
        return Create.create()
                     .stability(EVOLVING)
                     .mode(createMode)
                     .error(errorRef(ERROR_BAD_REQUEST))
                     .error(errorRef(ERROR_UNAUTHORIZED))
                     .error(errorRef(ERROR_FORBIDDEN))
                     .error(errorRef(ERROR_NOT_FOUND))
                     .error(errorRef(ERROR_REQUEST_TIMEOUT))
                     .error(errorRef(ERROR_VERSION_MISMATCH))
                     .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
                     .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
                     .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
                     .error(errorRef(ERROR_UNAVAILABLE))
                     .build();
    }
    private static Delete deleteOperation() {
        return Delete.delete()
                     .stability(EVOLVING)
                     .error(errorRef(ERROR_BAD_REQUEST))
                     .error(errorRef(ERROR_UNAUTHORIZED))
                     .error(errorRef(ERROR_FORBIDDEN))
                     .error(errorRef(ERROR_NOT_FOUND))
                     .error(errorRef(ERROR_REQUEST_TIMEOUT))
                     .error(errorRef(ERROR_VERSION_MISMATCH))
                     .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
                     .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
                     .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
                     .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
                     .error(errorRef(ERROR_UNAVAILABLE))
                     .build();
    }
    private static Patch patchOperation() {
        return Patch.patch()
                    .stability(EVOLVING)
                    .operations(ADD, REMOVE, REPLACE, INCREMENT)
                    .error(errorRef(ERROR_BAD_REQUEST))
                    .error(errorRef(ERROR_UNAUTHORIZED))
                    .error(errorRef(ERROR_FORBIDDEN))
                    .error(errorRef(ERROR_NOT_FOUND))
                    .error(errorRef(ERROR_REQUEST_TIMEOUT))
                    .error(errorRef(ERROR_VERSION_MISMATCH))
                    .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
                    .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
                    .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
                    .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
                    .error(errorRef(ERROR_UNAVAILABLE))
                    .build();
    }
    private static Read readOperation() {
        return Read.read()
                   .stability(EVOLVING)
                   .error(errorRef(ERROR_BAD_REQUEST))
                   .error(errorRef(ERROR_UNAUTHORIZED))
                   .error(errorRef(ERROR_FORBIDDEN))
                   .error(errorRef(ERROR_NOT_FOUND))
                   .error(errorRef(ERROR_REQUEST_TIMEOUT))
                   .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
                   .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
                   .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
                   .error(errorRef(ERROR_UNAVAILABLE))
                   .build();
    }
    private static Update updateOperation() {
        return Update.update()
                     .stability(EVOLVING)
                     .error(errorRef(ERROR_BAD_REQUEST))
                     .error(errorRef(ERROR_UNAUTHORIZED))
                     .error(errorRef(ERROR_FORBIDDEN))
                     .error(errorRef(ERROR_NOT_FOUND))
                     .error(errorRef(ERROR_REQUEST_TIMEOUT))
                     .error(errorRef(ERROR_VERSION_MISMATCH))
                     .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
                     .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
                     .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
                     .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
                     .error(errorRef(ERROR_UNAVAILABLE))
                     .build();
    }
    private static org.forgerock.api.models.Action modifyPasswordAction() {
        return org.forgerock.api.models.Action.action()
               .stability(EVOLVING)
               .name("modifyPassword")
               .request(passwordModifyRequest())
               .description("Modify a user password. This action requires HTTPS.")
               .error(errorRef(ERROR_BAD_REQUEST))
               .error(errorRef(ERROR_UNAUTHORIZED))
               .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_HTTPS))
               .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_AUTHENTICATION))
               .error(errorRef(ERROR_FORBIDDEN))
               .error(errorRef(ERROR_NOT_FOUND))
               .error(errorRef(ERROR_REQUEST_TIMEOUT))
               .error(errorRef(ERROR_VERSION_MISMATCH))
               .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
               .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
               .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
               .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
               .error(errorRef(ERROR_UNAVAILABLE))
               .build();
    }
    private static org.forgerock.api.models.Schema passwordModifyRequest() {
        final JsonValue jsonSchema = json(object(
            field("type", "object"),
            field("description", "Supply the old password and new password."),
            field("required", array("oldPassword", "newPassword")),
            field("properties", object(
                field("oldPassword", object(
                    field("type", "string"),
                    field("name", "Old Password"),
                    field("description", "Current password as a UTF-8 string."),
                    field("format", "password"))),
                field("newPassword", object(
                    field("type", "string"),
                    field("name", "New Password"),
                    field("description", "New password as a UTF-8 string."),
                    field("format", "password")))))));
        return Schema.schema().schema(jsonSchema).build();
    }
    private static org.forgerock.api.models.Action resetPasswordAction() {
        return org.forgerock.api.models.Action.action()
               .stability(EVOLVING)
               .name("resetPassword")
               .response(resetPasswordResponse())
               .description("Reset a user password to a generated value. This action requires HTTPS.")
               .error(errorRef(ERROR_BAD_REQUEST))
               .error(errorRef(ERROR_UNAUTHORIZED))
               .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_HTTPS))
               .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_AUTHENTICATION))
               .error(errorRef(ERROR_FORBIDDEN))
               .error(errorRef(ERROR_NOT_FOUND))
               .error(errorRef(ERROR_REQUEST_TIMEOUT))
               .error(errorRef(ERROR_VERSION_MISMATCH))
               .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
               .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
               .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
               .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
               .error(errorRef(ERROR_UNAVAILABLE))
               .build();
    }
    private static org.forgerock.api.models.Schema resetPasswordResponse() {
        final JsonValue jsonSchema = json(object(
            field("type", "object"),
            field("properties", object(
                field("generatedPassword", object(
                    field("type", "string"),
                    field("description", "Generated password to communicate to the user.")))))));
        return Schema.schema().schema(jsonSchema).build();
    }
    private Schema buildJsonSchema() {
        final List<String> requiredFields = new ArrayList<>();
        JsonValue properties = json(JsonValue.object());
        for (Map.Entry<String, PropertyMapper> prop : allProperties.entrySet()) {
            final String propertyName = prop.getKey();
            final PropertyMapper mapper = prop.getValue();
            if (mapper.isRequired()) {
                requiredFields.add(propertyName);
            }
            final JsonValue jsonSchema = mapper.toJsonSchema();
            if (jsonSchema != null) {
                properties.put(propertyName, jsonSchema);
            }
        }
        final JsonValue jsonSchema = json(object(field("type", "object")));
        if (!requiredFields.isEmpty()) {
            jsonSchema.put("required", requiredFields);
        }
        if (properties.size() > 0) {
            jsonSchema.put("properties", properties);
        }
        return Schema.schema().schema(jsonSchema).build();
    }
    private Errors errors() {
        return Errors
            .errors()
            .put("passwordModifyRequiresHttps",
                error(FORBIDDEN, "Password modify requires a secure connection."))
            .put("passwordModifyRequiresAuthn",
                error(FORBIDDEN, "Password modify requires user to be authenticated."))
            .put("readFoundMultipleEntries",
                error(INTERNAL_ERROR, "Multiple entries where found when trying to read a single entry."))
            .put("adminLimitExceeded",
                error(INTERNAL_ERROR, "The request exceeded an administrative limit."))
            .build();
    }
    static ApiError error(int code, String description) {
        return ApiError.apiError().code(code).description(description).build();
    }
    static ApiError error(int code, LocalizableString description) {
        return ApiError.apiError().code(code).description(description).build();
    }
    static ApiError errorRef(String referenceValue) {
        return ApiError.apiError().reference(ref(referenceValue)).build();
    }
    static org.forgerock.api.models.Resource resourceRef(String referenceValue) {
        return org.forgerock.api.models.Resource.resource().reference(ref(referenceValue)).build();
    }
    static org.forgerock.api.models.Schema schemaRef(String referenceValue) {
        return Schema.schema().reference(ref(referenceValue)).build();
    }
    static Reference ref(String referenceValue) {
        return Reference.reference().value(referenceValue).build();
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ResourceTypePropertyMapper.java
@@ -17,6 +17,8 @@
package org.forgerock.opendj.rest2ldap;
import static java.util.Collections.singletonList;
import static org.forgerock.json.JsonValue.*;
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.ldap.Filter.alwaysTrue;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ILLEGAL_FILTER_ASSERTION_VALUE;
@@ -57,6 +59,16 @@
    }
    @Override
    boolean isRequired() {
        return false;
    }
    @Override
    boolean isMultiValued() {
        return false;
    }
    @Override
    Promise<List<Attribute>, ResourceException> create(final Context context,
                                                       final Resource resource, final JsonPointer path,
                                                       final JsonValue v) {
@@ -120,4 +132,9 @@
            return newResultPromise(Collections.<Modification>emptyList());
        }
    }
    @Override
    JsonValue toJsonSchema() {
        return json(object(field("type", "string")));
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
@@ -26,28 +26,17 @@
import java.util.LinkedHashMap;
import java.util.Map;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
import org.forgerock.json.resource.ForbiddenException;
import org.forgerock.json.resource.InternalServerErrorException;
import org.forgerock.json.resource.NotFoundException;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.PermanentException;
import org.forgerock.json.resource.PreconditionFailedException;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.QueryResourceHandler;
import org.forgerock.json.resource.QueryResponse;
import org.forgerock.json.resource.ReadRequest;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.RetryableException;
import org.forgerock.json.resource.Router;
import org.forgerock.json.resource.ServiceUnavailableException;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.opendj.ldap.AssertionFailureException;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.AuthenticationException;
@@ -65,7 +54,6 @@
import org.forgerock.util.Option;
import org.forgerock.util.Options;
import org.forgerock.util.Reject;
import org.forgerock.util.promise.Promise;
/**
 * Provides methods for constructing Rest2Ldap protocol gateways. Applications construct a new Rest2Ldap
@@ -381,44 +369,9 @@
    }
    private RequestHandler rest2LdapContext(final RequestHandler delegate) {
        return new RequestHandler() {
            public Promise<ActionResponse, ResourceException> handleAction(final Context context,
                                                                           final ActionRequest request) {
                return delegate.handleAction(wrap(context), request);
            }
            public Promise<ResourceResponse, ResourceException> handleCreate(final Context context,
                                                                             final CreateRequest request) {
                return delegate.handleCreate(wrap(context), request);
            }
            public Promise<ResourceResponse, ResourceException> handleDelete(final Context context,
                                                                             final DeleteRequest request) {
                return delegate.handleDelete(wrap(context), request);
            }
            public Promise<ResourceResponse, ResourceException> handlePatch(final Context context,
                                                                            final PatchRequest request) {
                return delegate.handlePatch(wrap(context), request);
            }
            public Promise<QueryResponse, ResourceException> handleQuery(final Context context,
                                                                         final QueryRequest request,
                                                                         final QueryResourceHandler handler) {
                return delegate.handleQuery(wrap(context), request, handler);
            }
            public Promise<ResourceResponse, ResourceException> handleRead(final Context context,
                                                                           final ReadRequest request) {
                return delegate.handleRead(wrap(context), request);
            }
            public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context,
                                                                             final UpdateRequest request) {
                return delegate.handleUpdate(wrap(context), request);
            }
            private Context wrap(final Context context) {
        return new DescribableRequestHandler(delegate) {
            @Override
            protected Context wrap(final Context context) {
                return new Rest2LdapContext(context, Rest2Ldap.this);
            }
        };
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java
@@ -22,7 +22,6 @@
import static org.forgerock.json.JsonValueFunctions.duration;
import static org.forgerock.json.JsonValueFunctions.enumConstant;
import static org.forgerock.json.JsonValueFunctions.setOf;
import static org.forgerock.json.resource.http.CrestHttp.newHttpHandler;
import static org.forgerock.opendj.ldap.KeyManagers.useSingleCertificate;
import static org.forgerock.opendj.rest2ldap.Rest2LdapJsonConfigurator.*;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
@@ -69,7 +68,10 @@
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.CrestApplication;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.Resources;
import org.forgerock.json.resource.http.CrestHttp;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.DN;
@@ -94,6 +96,8 @@
import org.forgerock.util.time.Duration;
import org.forgerock.util.time.TimeService;
import com.forgerock.opendj.util.ManifestUtil;
/** Rest2ldap HTTP application. */
public class Rest2LdapHttpApplication implements HttpApplication {
    private static final String DEFAULT_ROOT_FACTORY = "root";
@@ -231,6 +235,27 @@
        return configureEndpoints(endpointsDirectory, options);
    }
    private static Handler newHttpHandler(final RequestHandler requestHandler) {
        final org.forgerock.json.resource.ConnectionFactory factory =
                Resources.newInternalConnectionFactory(requestHandler);
        return CrestHttp.newHttpHandler(new CrestApplication() {
            @Override
            public org.forgerock.json.resource.ConnectionFactory getConnectionFactory() {
                return factory;
            }
            @Override
            public String getApiId() {
                return "frapi:opendj:rest2ldap";
            }
            @Override
            public String getApiVersion() {
                return ManifestUtil.getVersionWithRevision("opendj-core");
            }
        });
    }
    private void configureSecurity(final JsonValue configuration) {
        trustManager = configureTrustManager(configuration);
        keyManager = configureKeyManager(configuration);
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimplePropertyMapper.java
@@ -30,6 +30,8 @@
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.schema.AttributeType;
import org.forgerock.opendj.ldap.schema.CoreSchema;
import org.forgerock.services.context.Context;
import org.forgerock.util.Function;
import org.forgerock.util.promise.NeverThrowsException;
@@ -37,6 +39,7 @@
import static java.util.Collections.*;
import static org.forgerock.json.JsonValue.*;
import static org.forgerock.opendj.ldap.Filter.*;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.*;
@@ -198,4 +201,39 @@
        return encoder == null ? jsonToByteString(ldapAttributeName) : encoder;
    }
    @Override
    JsonValue toJsonSchema() {
        final AttributeType attrType = ldapAttributeName.getAttributeType();
        final JsonValue jsonSchema = json(object(field("type", toJsonSchemaType(attrType))));
        final String description = attrType.getDescription();
        if (description != null && !"".equals(description)) {
            jsonSchema.put("title", description);
        }
        putWritabilityProperties(jsonSchema);
        return jsonSchema;
    }
    private static String toJsonSchemaType(AttributeType attrType) {
        if (attrType.isPlaceHolder()) {
            return "string";
        }
        // TODO JNR cannot use switch + SchemaConstants.SYNTAX_DIRECTORY_STRING_OID
        // because the class is not public
        // this is not nice :(
        // TODO JNR not so sure about these mappings
        final String oid = attrType.getSyntax().getOID();
        if (CoreSchema.getDirectoryStringSyntax().getOID().equals(oid)
                || CoreSchema.getOctetStringSyntax().getOID().equals(oid)) {
            return "string";
        } else if (CoreSchema.getBooleanSyntax().getOID().equals(oid)) {
            return "boolean";
        } else if (CoreSchema.getIntegerSyntax().getOID().equals(oid)) {
            return "integer";
        } else if (CoreSchema.getNumericStringSyntax().getOID().equals(oid)) {
            return "number";
        }
        return "string";
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java
@@ -18,6 +18,8 @@
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE;
import org.forgerock.api.models.ApiDescription;
import org.forgerock.http.ApiProducer;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.json.resource.ActionRequest;
@@ -213,5 +215,10 @@
                }
            });
        }
        @Override
        public ApiDescription api(ApiProducer<ApiDescription> producer) {
            return resource.subResourcesApi(producer);
        }
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
@@ -28,6 +28,8 @@
import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
import static org.forgerock.util.promise.Promises.newResultPromise;
import org.forgerock.api.models.ApiDescription;
import org.forgerock.http.ApiProducer;
import org.forgerock.http.routing.UriRouterContext;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.json.resource.ActionRequest;
@@ -378,6 +380,11 @@
        protected <V> Promise<V, ResourceException> handleRequest(final Context context, final Request request) {
            return new BadRequestException(ERR_UNSUPPORTED_REQUEST_AGAINST_COLLECTION.get().toString()).asPromise();
        }
        @Override
        public ApiDescription api(ApiProducer<ApiDescription> producer) {
            return resource.collectionApi(isReadOnly);
        }
    }
    /**
@@ -444,5 +451,17 @@
        private <T> Function<ResourceException, T, ResourceException> convert404To400() {
            return SubResource.convert404To400(ERR_UNSUPPORTED_REQUEST_AGAINST_INSTANCE.get());
        }
        /**
         * Returns {@code null} because the corresponding {@link ApiDescription}
         * is returned by the {@link CollectionHandler#api(ApiProducer)} method.
         * <p>
         * This avoids problems when trying to {@link ApiProducer#merge(java.util.List) merge}
         * {@link ApiDescription}s with the same path.
         */
        @Override
        public ApiDescription api(ApiProducer<ApiDescription> producer) {
            return null;
        }
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
@@ -25,6 +25,8 @@
import static org.forgerock.opendj.rest2ldap.RoutingContext.newRoutingContext;
import static org.forgerock.util.promise.Promises.newResultPromise;
import org.forgerock.api.models.ApiDescription;
import org.forgerock.http.ApiProducer;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.BadRequestException;
@@ -203,5 +205,10 @@
        private <T> Function<ResourceException, T, ResourceException> convert404To400() {
            return SubResource.convert404To400(ERR_UNSUPPORTED_REQUEST_AGAINST_SINGLETON.get());
        }
        @Override
        public ApiDescription api(ApiProducer<ApiDescription> producer) {
            return getResource().instanceApi(isReadOnly);
        }
    }
}
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java
New file
@@ -0,0 +1,109 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
import static org.assertj.core.api.Assertions.*;
import static org.forgerock.http.util.Json.*;
import static org.forgerock.json.resource.Requests.*;
import static org.forgerock.json.resource.ResourcePath.*;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.*;
import static org.forgerock.util.Options.*;
import java.io.File;
import java.io.StringReader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import org.forgerock.api.CrestApiProducer;
import org.forgerock.api.models.ApiDescription;
import org.forgerock.http.routing.UriRouterContext;
import org.forgerock.http.util.Json;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.Request;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.RootContext;
import org.forgerock.testng.ForgeRockTestCase;
import org.forgerock.util.Options;
import org.testng.annotations.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
/**
 * This class tests that the {@link Rest2LdapJsonConfigurator} class can successfully create its
 * model and generate its API description from the json configuration files.
 */
@Test
@SuppressWarnings("javadoc")
public class Rest2LdapJsonConfiguratorTest extends ForgeRockTestCase {
    private static final String ID = "frapi:opendj:rest2ldap";
    private static final String VERSION = "4.0.0";
    private static final Path CONFIG_DIR = Paths.get(
        "../opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap");
    @Test
    public void testConfigureEndpointsWithApiDescription() throws Exception {
        final DescribableRequestHandler handler = configureEndpoints(CONFIG_DIR.resolve("endpoints").toFile());
        final ApiDescription api = requestApi(handler, "api/users/bjensen");
        assertThat(api).isNotNull();
        // Ensure we can can pretty print and parse back the generated api description
        parseJson(prettyPrint(api));
        assertThat(api.getId()).isEqualTo(ID);
        assertThat(api.getVersion()).isEqualTo(VERSION);
        assertThat(api.getPaths().getNames()).containsOnly("/api/users", "/api/groups");
        assertThat(api.getDefinitions().getNames()).containsOnly(
            "frapi:opendj:rest2ldap:group:1.0",
            "frapi:opendj:rest2ldap:user:1.0",
            "frapi:opendj:rest2ldap:posixUser:1.0");
    }
    private DescribableRequestHandler configureEndpoints(final File endpointsDir) throws Exception {
        final RequestHandler rh = Rest2LdapJsonConfigurator.configureEndpoints(endpointsDir, Options.defaultOptions());
        DescribableRequestHandler handler = new DescribableRequestHandler(rh);
        handler.api(new CrestApiProducer(ID, VERSION));
        return handler;
    }
    private ApiDescription requestApi(final DescribableRequestHandler handler, String uriPath) {
        Context context = newRouterContext(uriPath);
        Request request = newApiRequest(resourcePath(uriPath));
        return handler.handleApiRequest(context, request);
    }
    private Context newRouterContext(final String uriPath) {
        Context ctx = new RootContext();
        ctx = new Rest2LdapContext(ctx, rest2Ldap(defaultOptions()));
        ctx = new UriRouterContext(ctx, null, uriPath, Collections.<String, String> emptyMap());
        return ctx;
    }
    private String prettyPrint(Object o) throws Exception {
        final ObjectMapper objectMapper =
            new ObjectMapper().registerModules(new Json.LocalizableStringModule(), new Json.JsonValueModule());
        final ObjectWriter writer = objectMapper.writer().withDefaultPrettyPrinter();
        return writer.writeValueAsString(o);
    }
    static JsonValue parseJson(final String json) throws Exception {
        try (StringReader r = new StringReader(json)) {
            return new JsonValue(readJsonLenient(r));
        }
    }
}
opendj-server-legacy/src/main/java/org/opends/server/core/HttpEndpointConfigManager.java
@@ -157,7 +157,6 @@
      return ccr;
    }
    final RouteMatcher<Request> route = newRoute(configuration.getBasePath());
    try
    {
      final HttpApplication application = loadEndpoint(configuration).newHttpApplication();
@@ -170,13 +169,13 @@
    {
      ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode());
      ccr.addMessage(ERR_CONFIG_HTTPENDPOINT_UNABLE_TO_START.get(configuration.dn(), stackTraceToSingleLineString(e)));
      router.addRoute(route, ErrorHandler.INTERNAL_SERVER_ERROR);
      router.addRoute(newRoute(configuration.getBasePath()), ErrorHandler.INTERNAL_SERVER_ERROR);
    }
    catch (InitializationException | ConfigException ie)
    {
      ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode());
      ccr.addMessage(ie.getMessageObject());
      router.addRoute(route, ErrorHandler.INTERNAL_SERVER_ERROR);
      router.addRoute(newRoute(configuration.getBasePath()), ErrorHandler.INTERNAL_SERVER_ERROR);
    }
    return ccr;
  }
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java
@@ -42,15 +42,17 @@
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import org.forgerock.http.ApiProducer;
import org.forgerock.http.DescribedHttpApplication;
import org.forgerock.http.Filter;
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.protocol.Status;
import org.forgerock.http.swagger.SwaggerApiProducer;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigChangeResult;
@@ -97,6 +99,8 @@
import org.opends.server.util.SelectableCertificateKeyManager;
import org.opends.server.util.StaticUtils;
import io.swagger.models.Swagger;
/**
 * This class defines a connection handler that will be used for communicating
 * with clients over HTTP. The connection handler is responsible for
@@ -899,11 +903,11 @@
  }
  /**
   * This is the root {@link HttpApplication} handling all the requests from the
   * {@link HTTPConnectionHandler}. If accepted, requests are audited and then
   * forwarded to the global {@link ServerContext#getHTTPRouter()}.
   * This is the root {@link org.forgerock.http.HttpApplication} handling all the requests from the
   * {@link HTTPConnectionHandler}. If accepted, requests are audited and then forwarded to the
   * global {@link ServerContext#getHTTPRouter()}.
   */
  private final class RootHttpApplication implements HttpApplication
  private final class RootHttpApplication implements DescribedHttpApplication
  {
    @Override
    public Handler start() throws HttpApplicationException
@@ -934,6 +938,13 @@
    {
      return null;
    }
    @Override
    public ApiProducer<Swagger> getApiProducer()
    {
      // Needed to enforce generation of CREST APIs
      return new SwaggerApiProducer(null, null, null);
    }
  }
  /** Moves the processing of the request in this Directory Server's worker thread. */
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java
@@ -15,7 +15,6 @@
 */
package org.opends.server.protocols.http.rest2ldap;
import static org.forgerock.json.resource.http.CrestHttp.newHttpHandler;
import static org.forgerock.opendj.rest2ldap.Rest2LdapJsonConfigurator.configureEndpoint;
import static org.forgerock.util.Options.defaultOptions;
import static org.opends.messages.ConfigMessages.ERR_CONFIG_REST2LDAP_INVALID;
@@ -32,12 +31,18 @@
import org.forgerock.http.HttpApplicationException;
import org.forgerock.http.io.Buffer;
import org.forgerock.json.JsonValueException;
import org.forgerock.json.resource.CrestApplication;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.Resources;
import org.forgerock.json.resource.http.CrestHttp;
import org.forgerock.opendj.rest2ldap.DescribableRequestHandler;
import org.forgerock.opendj.server.config.server.Rest2ldapEndpointCfg;
import org.forgerock.util.Factory;
import org.opends.server.api.HttpEndpoint;
import org.opends.server.core.ServerContext;
import org.opends.server.protocols.http.LocalizedHttpApplicationException;
import org.opends.server.types.InitializationException;
import org.opends.server.util.BuildVersion;
/**
 * Encapsulates configuration required to start a REST2LDAP application embedded
@@ -95,6 +100,27 @@
      }
    }
    private Handler newHttpHandler(final RequestHandler requestHandler) {
        final DescribableRequestHandler handler = new DescribableRequestHandler(requestHandler);
        final org.forgerock.json.resource.ConnectionFactory factory = Resources.newInternalConnectionFactory(handler);
        return CrestHttp.newHttpHandler(new CrestApplication() {
            @Override
            public org.forgerock.json.resource.ConnectionFactory getConnectionFactory() {
                return factory;
            }
            @Override
            public String getApiId() {
                return "frapi:opendj:rest2ldap";
            }
            @Override
            public String getApiVersion() {
                return BuildVersion.binaryVersion().toStringNoRevision();
            }
        });
    }
    @Override
    public void stop()
    {
opendj-server-legacy/src/main/java/org/opends/server/util/BuildVersion.java
@@ -276,6 +276,17 @@
    {
      return Utils.joinAsString(".", major, minor, point, rev);
    }
    return toStringNoRevision();
  }
  /**
   * Returns a string representation of the BuildVersion including the major, minor and point
   * versions, but excluding the revision number.
   *
   * @return a string representation excluding the revision number
   */
  public String toStringNoRevision()
  {
    return Utils.joinAsString(".", major, minor, point);
  }