Resources.java

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2012-2016 ForgeRock AS.
 */

package org.forgerock.json.resource;

import static org.forgerock.api.models.Parameter.*;
import static org.forgerock.api.models.Resource.AnnotatedTypeVariant.*;
import static org.forgerock.api.models.SubResources.*;
import static org.forgerock.http.routing.RoutingMode.*;
import static org.forgerock.json.resource.Responses.*;
import static org.forgerock.json.resource.RouteMatchers.*;
import static org.forgerock.util.promise.Promises.*;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

import org.forgerock.api.annotations.CollectionProvider;
import org.forgerock.api.annotations.Path;
import org.forgerock.api.annotations.SingletonProvider;
import org.forgerock.api.enums.ParameterSource;
import org.forgerock.api.models.ApiDescription;
import org.forgerock.api.models.Items;
import org.forgerock.api.models.Parameter;
import org.forgerock.api.models.Resource;
import org.forgerock.api.models.SubResources;
import org.forgerock.http.ApiProducer;
import org.forgerock.http.routing.UriRouterContext;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.services.context.AbstractContext;
import org.forgerock.services.context.Context;
import org.forgerock.services.descriptor.Describable;
import org.forgerock.util.Reject;
import org.forgerock.util.promise.Promise;

/**
 * This class contains methods for creating and manipulating connection
 * factories and connections.
 */
public final class Resources {

    private static final class InternalConnectionFactory implements ConnectionFactory {
        private final RequestHandler handler;

        private InternalConnectionFactory(final RequestHandler handler) {
            this.handler = handler;
        }

        @Override
        public void close() {
            // Do nothing.
        }

        @Override
        public Connection getConnection() {
            return newInternalConnection(handler);
        }

        public Promise<Connection, ResourceException> getConnectionAsync() {
            return newSuccessfulPromise(getConnection());
        }
    }

    /**
     * Adapts the provided {@link SynchronousRequestHandler} as a
     * {@link RequestHandler}.
     *
     * @param syncHandler
     *            The synchronous request handler to be adapted.
     * @return The adapted synchronous request handler.
     */
    public static RequestHandler asRequestHandler(final SynchronousRequestHandler syncHandler) {
        return syncHandler instanceof Describable
                ? new DescribedSyncRequestHandlerAdapter(syncHandler)
                : new SynchronousRequestHandlerAdapter(syncHandler);
    }

    /**
     * Returns a JSON object containing only the specified fields from the
     * provided JSON value. If the list of fields is empty then the value is
     * returned unchanged.
     * <p>
     * <b>NOTE:</b> this method only performs a shallow copy of extracted
     * fields, so changes to the filtered JSON value may impact the original
     * JSON value, and vice-versa.
     *
     * @param resource
     *            The JSON value whose fields are to be filtered.
     * @param fields
     *            The list of fields to be extracted.
     * @return The filtered JSON value.
     */
    public static JsonValue filterResource(final JsonValue resource,
            final Collection<JsonPointer> fields) {
        if (fields.isEmpty() || resource.isNull() || resource.size() == 0) {
            return resource;
        } else {
            final Map<String, Object> filtered = new LinkedHashMap<>(fields.size());
            for (final JsonPointer field : fields) {
                if (field.isEmpty()) {
                    // Special case - copy resource fields (assumes Map).
                    filtered.putAll(resource.asMap());
                } else {
                    // FIXME: what should we do if the field refers to an array element?
                    final JsonValue value = resource.get(field);
                    if (value != null) {
                        final String key = field.leaf();
                        filtered.put(key, value.getObject());
                    }
                }
            }
            return new JsonValue(filtered);
        }
    }

    /**
     * Returns a JSON object containing only the specified fields from the
     * provided resource. If the list of fields is empty then the resource is
     * returned unchanged.
     * <p>
     * <b>NOTE:</b> this method only performs a shallow copy of extracted
     * fields, so changes to the filtered resource may impact the original
     * resource, and vice-versa.
     *
     * @param resource
     *            The resource whose fields are to be filtered.
     * @param fields
     *            The list of fields to be extracted.
     * @return The filtered resource.
     */
    public static ResourceResponse filterResource(final ResourceResponse resource,
            final Collection<JsonPointer> fields) {
        final JsonValue unfiltered = resource.getContent();
        final Collection<JsonPointer> filterFields = resource.hasFields()
                ? resource.getFields()
                : fields;
        final JsonValue filtered = filterResource(unfiltered, filterFields);
        if (filtered == unfiltered) {
            return resource; // Unchanged.
        } else {
            return newResourceResponse(resource.getId(), resource.getRevision(), filtered);
        }
    }

    /**
     * Creates a new connection to a {@link RequestHandler}.
     *
     * @param handler
     *            The request handler to which client requests should be
     *            forwarded.
     * @return The new internal connection.
     * @throws NullPointerException
     *             If {@code handler} was {@code null}.
     */
    public static Connection newInternalConnection(final RequestHandler handler) {
        return new InternalConnection(handler);
    }

    /**
     * Creates a new connection factory which binds internal client connections
     * to {@link RequestHandler}s.
     *
     * @param handler
     *            The request handler to which client requests should be
     *            forwarded.
     * @return The new internal connection factory.
     * @throws NullPointerException
     *             If {@code handler} was {@code null}.
     */
    public static ConnectionFactory newInternalConnectionFactory(final RequestHandler handler) {
        return new InternalConnectionFactory(handler);
    }

    /**
     * Creates a new {@link RequestHandler} backed by the supplied provider. The provider can be an instance of an
     * interface resource handler, and annotated resource handler or an annotated request handler. The type of the
     * provider will be determined from the {@code org.forgerock.api.annotations.RequestHandler#variant}
     * annotation property, or from the type of interface implemented.
     * <p>
     * Sub-paths that are declared using the {@code org.forgerock.api.annotations.Path} annotation will also be routed
     * through the returned handler.
     * <p>
     * This method uses the same logic as {@link #newCollection(Object)}, {@link #newSingleton(Object)} and
     * {@link #newAnnotatedRequestHandler(Object)} to create the underlying {@link RequestHandler}s.
     * @param provider The provider instance.
     * @return The constructed handler.
     */
    public static RequestHandler newHandler(Object provider) {
        Router router;
        if (provider instanceof Describable) {
            router = new Router();
            addHandlers(provider, router, "", null);
        } else {
            final DescribableResourceHandler descriptorProvider = new DescribableResourceHandler();
            router = new Router() {
                @Override
                protected ApiDescription buildApi(ApiProducer<ApiDescription> producer) {
                    return descriptorProvider.api(producer);
                }
            };
            descriptorProvider.describes(addHandlers(provider, router, "",
                    descriptorProvider.getDefinitionDescriptions()));
        }
        Path path = provider.getClass().getAnnotation(Path.class);
        if (path != null) {
            Router pathRouter = new Router();
            pathRouter.addRoute(requestUriMatcher(STARTS_WITH, path.value()), router);
            router = pathRouter;
        }
        return router;
    }

    private static Resource addHandlers(Object provider, Router router, String basePath,
            ApiDescription definitions, Parameter... pathParameters) {
        HandlerVariant variant = deduceHandlerVariant(provider);
        Parameter[] nextPathParameters = pathParameters;
        switch (variant) {
        case SINGLETON_RESOURCE:
            addSingletonHandlerToRouter(provider, router, basePath);
            break;
        case COLLECTION_RESOURCE:
            nextPathParameters = new Parameter[pathParameters.length + 1];
            System.arraycopy(pathParameters, 0, nextPathParameters, 0, pathParameters.length);
            nextPathParameters[pathParameters.length] = addCollectionHandlersToRouter(provider, router, basePath);
            String pathParameter = nextPathParameters[pathParameters.length].getName();
            basePath = (basePath.isEmpty() ? "" : basePath + "/") + "{" + pathParameter + "}";
            break;
        case REQUEST_HANDLER:
            addRequestHandlerToRouter(provider, router, basePath);
            break;
        default:
            return null;
        }

        SubResources.Builder subResourcesBuilder = null;
        for (Method m : provider.getClass().getMethods()) {
            Path subpathAnnotation = m.getAnnotation(Path.class);
            if (subpathAnnotation != null) {
                if (subResourcesBuilder == null) {
                    subResourcesBuilder = subresources();
                }
                String subpath = subpathAnnotation.value().replaceAll("^/", "");
                try {
                    Resource subResource = addHandlers(m.invoke(provider), router,
                            basePath.isEmpty() ? subpath : basePath + "/" + subpath, definitions, nextPathParameters);
                    if (subResource != null) {
                        subResourcesBuilder.put(subpath, subResource);
                    }
                } catch (IllegalAccessException | InvocationTargetException e) {
                    throw new IllegalArgumentException("Could not construct handler tree", e);
                }
            }
        }
        SubResources subResources = subResourcesBuilder == null ? null : subResourcesBuilder.build();
        return makeDescriptor(provider, variant, subResources, definitions, pathParameters);
    }

    private static HandlerVariant deduceHandlerVariant(Object provider) {
        Class<?> type = provider.getClass();
        org.forgerock.api.annotations.RequestHandler handler =
                provider.getClass().getAnnotation(org.forgerock.api.annotations.RequestHandler.class);
        if (handler != null || provider instanceof RequestHandler) {
            return HandlerVariant.REQUEST_HANDLER;
        } else if (type.getAnnotation(SingletonProvider.class) != null
                || provider instanceof SingletonResourceProvider) {
            return HandlerVariant.SINGLETON_RESOURCE;
        } else if (type.getAnnotation(CollectionProvider.class) != null
                || provider instanceof CollectionResourceProvider) {
            return HandlerVariant.COLLECTION_RESOURCE;
        }
        throw new IllegalArgumentException("Cannot deduce provider variant" + provider.getClass());
    }

    private static void addRequestHandlerToRouter(Object provider, Router router, String basePath) {
        router.addRoute(requestUriMatcher(STARTS_WITH, basePath), new AnnotatedRequestHandler(provider));
    }

    private static Parameter addCollectionHandlersToRouter(Object provider, Router router, String basePath) {
        boolean fromInterface = provider instanceof CollectionResourceProvider;
        // Create a route for the collection.
        final RequestHandler collectionHandler = fromInterface
                ? new InterfaceCollectionHandler((CollectionResourceProvider) provider)
                : new AnnotatedCollectionHandler(provider);
        router.addRoute(requestUriMatcher(EQUALS, basePath), collectionHandler);

        // Create a route for the instances within the collection.
        RequestHandler instanceHandler = fromInterface
                ? new InterfaceCollectionInstance((CollectionResourceProvider) provider)
                : new AnnotationCollectionInstance(provider);
        Class<?> providerClass = provider.getClass();
        CollectionProvider providerAnnotation = providerClass.getAnnotation(CollectionProvider.class);
        Parameter pathParameter;
        if (providerAnnotation != null) {
            pathParameter = Parameter.fromAnnotation(providerClass, providerAnnotation.pathParam());
        } else {
            pathParameter = parameter().name("id").type("string").source(ParameterSource.PATH).required(true).build();
        }
        String pathParam = pathParameter.getName();
        instanceHandler = new FilterChain(instanceHandler, new CollectionInstanceIdContextFilter(pathParam));
        router.addRoute(requestUriMatcher(EQUALS, (basePath.isEmpty() ? "" : basePath + "/") + "{" + pathParam + "}"),
                instanceHandler);
        return pathParameter;
    }

    private static void addSingletonHandlerToRouter(Object provider, Router router, String basePath) {
        router.addRoute(requestUriMatcher(EQUALS, basePath),
                provider instanceof SingletonResourceProvider
                        ? new InterfaceSingletonHandler((SingletonResourceProvider) provider)
                        : new AnnotatedSingletonHandler(provider));
    }

    private static Resource makeDescriptor(Object provider, HandlerVariant variant, SubResources subResources,
            ApiDescription definitions, Parameter[] pathParameters) {
        if (provider instanceof Describable) {
            return null;
        }
        Class<?> type = provider.getClass();
        switch (variant) {
        case SINGLETON_RESOURCE:
            return Resource.fromAnnotatedType(type, SINGLETON_RESOURCE, subResources, definitions, pathParameters);
        case COLLECTION_RESOURCE:
            final Items items = Items.fromAnnotatedType(type, definitions, subResources);
            return Resource.fromAnnotatedType(type, COLLECTION_RESOURCE_COLLECTION, items,
                    definitions, pathParameters);
        case REQUEST_HANDLER:
            return Resource.fromAnnotatedType(type, REQUEST_HANDLER, subResources, definitions, pathParameters);
        default:
            return null;
        }
    }

    /**
     * Returns a new request handler which will forward requests on to the
     * provided collection resource provider. Incoming requests which are not
     * appropriate for a resource collection or resource instance will result in
     * a bad request error being returned to the client.
     *
     * @param provider
     *            The collection resource provider. Either an implementation of {@link CollectionResourceProvider} or
     *            a POJO annotated with annotations from {@link org.forgerock.json.resource.annotations}.
     * @return A new request handler which will forward requests on to the
     *         provided collection resource provider.
     * @deprecated Use {@link #newHandler(Object)} instead.
     */
    @Deprecated
    public static RequestHandler newCollection(final Object provider) {
        return newHandler(provider);
    }

    /**
     * Returns a new request handler which will forward requests on to the
     * provided singleton resource provider. Incoming requests which are not
     * appropriate for a singleton resource (e.g. query) will result in a bad
     * request error being returned to the client.
     *
     * @param provider
     *            The singleton resource provider. Either an implementation of {@link SingletonResourceProvider} or
     *            a POJO annotated with annotations from {@link org.forgerock.json.resource.annotations}.
     * @return A new request handler which will forward requests on to the
     *         provided singleton resource provider.
     * @deprecated Use {@link #newHandler(Object)} instead.
     */
    @Deprecated
    public static RequestHandler newSingleton(final Object provider) {
        return newHandler(provider);
    }

    /**
     * Returns a new request handler which will forward requests on to the
     * provided annotated request handler.
     *
     * @param provider
     *            The POJO annotated with annotations from {@link org.forgerock.json.resource.annotations}.
     * @return A new request handler which will forward requests on to the provided annotated POJO.
     * @deprecated Use {@link #newHandler(Object)} instead.
     */
    @Deprecated
    public static RequestHandler newAnnotatedRequestHandler(final Object provider) {
        Reject.ifTrue(provider instanceof RequestHandler,
                "Refusing to create an annotated request handler using a provider that implements RequestHandler. "
                        + "Use the RequestHandler implementation directly instead");
        return newHandler(provider);
    }

    /**
     * Returns an uncloseable view of the provided connection. Attempts to call
     * {@link Connection#close()} will be ignored.
     *
     * @param connection
     *            The connection whose {@code close} method is to be disabled.
     * @return An uncloseable view of the provided connection.
     */
    public static Connection uncloseable(final Connection connection) {
        return new AbstractConnectionWrapper<Connection>(connection) {
            @Override
            public void close() {
                // Do nothing.
            }
        };
    }

    /**
     * Returns an uncloseable view of the provided connection factory. Attempts
     * to call {@link ConnectionFactory#close()} will be ignored.
     *
     * @param factory
     *            The connection factory whose {@code close} method is to be
     *            disabled.
     * @return An uncloseable view of the provided connection factory.
     */
    public static ConnectionFactory uncloseable(final ConnectionFactory factory) {
        return new ConnectionFactory() {

            @Override
            public Promise<Connection, ResourceException> getConnectionAsync() {
                return factory.getConnectionAsync();
            }

            @Override
            public Connection getConnection() throws ResourceException {
                return factory.getConnection();
            }

            @Override
            public void close() {
                // Do nothing.
            }
        };
    }

    static String idOf(final Context context) {
        String idFieldName = context.asContext(IdFieldContext.class).getIdFieldName();
        return context.asContext(UriRouterContext.class).getUriTemplateVariables().get(idFieldName);
    }

    static ResourceException newBadRequestException(final String fs, final Object... args) {
        final String msg = String.format(fs, args);
        return new BadRequestException(msg);
    }

    private static <V> Promise<V, ResourceException> newSuccessfulPromise(V result) {
        return newResultPromise(result);
    }

    // Strips off the unwanted leaf routing context which was added when routing
    // requests to a collection.
    static Context parentOf(Context context) {
        if (context instanceof IdFieldContext) {
            context = context.getParent();
        }
        assert context instanceof UriRouterContext;
        return context.getParent();
    }

    private static class IdFieldContext extends AbstractContext {

        private static final String ID_FIELD_NAME = "IdFieldName";

        protected IdFieldContext(Context parent, String idFieldName) {
            super(parent, "IdField");
            this.data.put(ID_FIELD_NAME, idFieldName);
        }

        protected IdFieldContext(JsonValue data, ClassLoader loader) {
            super(data, loader);
        }

        String getIdFieldName() {
            return this.data.get(ID_FIELD_NAME).asString();
        }
    }

    private static final class CollectionInstanceIdContextFilter implements Filter {

        private final String idFieldName;

        private CollectionInstanceIdContextFilter(String idFieldName) {
            this.idFieldName = idFieldName;
        }

        @Override
        public Promise<ActionResponse, ResourceException> filterAction(Context context, ActionRequest request,
                RequestHandler next) {
            return next.handleAction(new IdFieldContext(context, idFieldName), request);
        }

        @Override
        public Promise<ResourceResponse, ResourceException> filterCreate(Context context, CreateRequest request,
                RequestHandler next) {
            return next.handleCreate(new IdFieldContext(context, idFieldName), request);
        }

        @Override
        public Promise<ResourceResponse, ResourceException> filterDelete(Context context, DeleteRequest request,
                RequestHandler next) {
            return next.handleDelete(new IdFieldContext(context, idFieldName), request);
        }

        @Override
        public Promise<ResourceResponse, ResourceException> filterPatch(Context context, PatchRequest request,
                RequestHandler next) {
            return next.handlePatch(new IdFieldContext(context, idFieldName), request);
        }

        @Override
        public Promise<QueryResponse, ResourceException> filterQuery(Context context, QueryRequest request,
                QueryResourceHandler handler, RequestHandler next) {
            return next.handleQuery(new IdFieldContext(context, idFieldName), request, handler);
        }

        @Override
        public Promise<ResourceResponse, ResourceException> filterRead(Context context, ReadRequest request,
                RequestHandler next) {
            return next.handleRead(new IdFieldContext(context, idFieldName), request);
        }

        @Override
        public Promise<ResourceResponse, ResourceException> filterUpdate(Context context, UpdateRequest request,
                RequestHandler next) {
            return next.handleUpdate(new IdFieldContext(context, idFieldName), request);
        }
    }

    /**
     * Enumeration of the possible CREST handler variants.
     */
    enum HandlerVariant {
        /** A singleton resource handler. */
        SINGLETON_RESOURCE,
        /** A collection resource handler. */
        COLLECTION_RESOURCE,
        /** A plain request handler. */
        REQUEST_HANDLER
    }

    // Prevent instantiation.
    private Resources() {
        // Nothing to do.
    }
}