From e4c0edea06c8fee28369f03f393b7d54b2b6235c Mon Sep 17 00:00:00 2001
From: Jean-Noël Rouvignac <jean-noel.rouvignac@forgerock.com>
Date: Fri, 16 Sep 2016 13:25:06 +0000
Subject: [PATCH] OPENDJ-3246 Return the CREST descriptor over REST for rest2ldap endpoints

---
 opendj-core/src/main/java/com/forgerock/opendj/util/ManifestUtil.java                                |   87 ++++
 opendj-server-legacy/src/main/java/org/opends/server/util/BuildVersion.java                          |   11 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimplePropertyMapper.java              |   38 +
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonConstantPropertyMapper.java        |   75 ++
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java             |   19 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java          |   27 +
 opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java |   28 +
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java              |   52 ++
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ResourceTypePropertyMapper.java        |   17 
 opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java     |  109 +++++
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java           |   21 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java                       |    7 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java                         |   53 --
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java                          |  409 +++++++++++++++++++
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLdapPropertyMapper.java        |   32 +
 opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java       |   21 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/PropertyMapper.java                    |    6 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java              |    7 
 opendj-server-legacy/src/main/java/org/opends/server/core/HttpEndpointConfigManager.java             |    5 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractRequestHandler.java            |   29 +
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DescribableRequestHandler.java         |  132 ++++++
 opendj-cli/src/main/java/com/forgerock/opendj/cli/ToolVersionHandler.java                            |   23 -
 22 files changed, 1,098 insertions(+), 110 deletions(-)

diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/ToolVersionHandler.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/ToolVersionHandler.java
index cbb713e..a2d03dd 100644
--- a/opendj-cli/src/main/java/com/forgerock/opendj/cli/ToolVersionHandler.java
+++ b/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);
     }
 }
diff --git a/opendj-core/src/main/java/com/forgerock/opendj/util/ManifestUtil.java b/opendj-core/src/main/java/com/forgerock/opendj/util/ManifestUtil.java
new file mode 100644
index 0000000..e11d841
--- /dev/null
+++ b/opendj-core/src/main/java/com/forgerock/opendj/util/ManifestUtil.java
@@ -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
+    }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLdapPropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLdapPropertyMapper.java
index 0feff58..f529243 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLdapPropertyMapper.java
+++ b/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;
+        }
+    }
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractRequestHandler.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractRequestHandler.java
index 4b02050..8a71663 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractRequestHandler.java
+++ b/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
+    }
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DescribableRequestHandler.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DescribableRequestHandler.java
new file mode 100644
index 0000000..8770aaf
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DescribableRequestHandler.java
@@ -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);
+        }
+    }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonConstantPropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonConstantPropertyMapper.java
index 01525ff..dc6400b 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonConstantPropertyMapper.java
+++ b/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;
+        }
+    }
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java
index 615c7a6..249cfb5 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java
+++ b/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;
+    }
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/PropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/PropertyMapper.java
index d428e09..a496a2b 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/PropertyMapper.java
+++ b/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();
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java
index a1496b3..69169a4 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java
+++ b/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();
+    }
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java
index ab76ccd..07421a0 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java
+++ b/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();
+    }
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ResourceTypePropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ResourceTypePropertyMapper.java
index 4f79fd8..d667f91 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ResourceTypePropertyMapper.java
+++ b/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")));
+    }
 }
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 0ed6cba..ae42e86 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
@@ -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);
             }
         };
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
index fd5ac89..2afe7d0 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java
+++ b/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);
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimplePropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimplePropertyMapper.java
index 2d82561..525e33d 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimplePropertyMapper.java
+++ b/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";
+    }
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java
index 503bba4..0e61043 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java
+++ b/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);
+        }
     }
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
index bf79ec4..7713ced 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
+++ b/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;
+        }
     }
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
index 81a3b65..77976f2 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
+++ b/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);
+        }
     }
 }
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java
new file mode 100644
index 0000000..238f4a6
--- /dev/null
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java
@@ -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));
+        }
+    }
+}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/core/HttpEndpointConfigManager.java b/opendj-server-legacy/src/main/java/org/opends/server/core/HttpEndpointConfigManager.java
index 6d51156..0381a28 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/core/HttpEndpointConfigManager.java
+++ b/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;
   }
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 88ebe1a..9efb145 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
@@ -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. */
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java
index b328186..4334233 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java
+++ b/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()
     {
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/util/BuildVersion.java b/opendj-server-legacy/src/main/java/org/opends/server/util/BuildVersion.java
index 2312c91..afc63c8 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/util/BuildVersion.java
+++ b/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);
   }
 

--
Gitblit v1.10.0