org.forgerock.openidm.servlet.internal.ServletConnectionFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.forgerock.openidm.servlet.internal.ServletConnectionFactory.java

Source

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2014-2015 ForgeRock AS. All Rights Reserved
 *
 * 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
 * http://forgerock.org/license/CDDLv1.0.html
 * See the License for the specific language governing
 * permission and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at http://forgerock.org/license/CDDLv1.0.html
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 */

package org.forgerock.openidm.servlet.internal;

import static org.forgerock.json.resource.Requests.copyOfCreateRequest;
import static org.forgerock.util.promise.Promises.newResultPromise;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import javax.script.ScriptException;
import javax.servlet.ServletException;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.JsonValueException;
import org.forgerock.json.resource.AbstractConnectionWrapper;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.Connection;
import org.forgerock.json.resource.ConnectionFactory;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
import org.forgerock.json.resource.Filter;
import org.forgerock.json.resource.FilterChain;
import org.forgerock.json.resource.FilterCondition;
import org.forgerock.json.resource.Filters;
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.RequestType;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.Resources;
import org.forgerock.json.resource.Response;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.openidm.audit.filter.AuditFilter;
import org.forgerock.openidm.config.enhanced.EnhancedConfig;
import org.forgerock.openidm.core.ServerConstants;
import org.forgerock.openidm.smartevent.EventEntry;
import org.forgerock.openidm.smartevent.Name;
import org.forgerock.openidm.smartevent.Publisher;
import org.forgerock.script.Script;
import org.forgerock.script.ScriptEntry;
import org.forgerock.script.ScriptRegistry;
import org.forgerock.services.context.Context;
import org.forgerock.util.promise.ExceptionHandler;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.ResultHandler;
import org.osgi.framework.Constants;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The ConnectionFactory responsible for providing Connections to routing requests initiated
 * from an external request on the api servlet.
 */
@Component(name = ServletConnectionFactory.PID, policy = ConfigurationPolicy.OPTIONAL, configurationFactory = false, immediate = true)
@Service
@Properties({ @Property(name = Constants.SERVICE_VENDOR, value = ServerConstants.SERVER_VENDOR_NAME),
        @Property(name = Constants.SERVICE_DESCRIPTION, value = "OpenIDM Common REST Servlet Connection Factory") })
public class ServletConnectionFactory implements ConnectionFactory {

    public static final String PID = "org.forgerock.openidm.router";

    /** Event name prefix for monitoring the router */
    public final static String EVENT_ROUTER_PREFIX = "openidm/internal/router/";

    /**
     * Setup logging for the {@link org.forgerock.openidm.servlet.internal.ServletConnectionFactory}.
     */
    private final static Logger logger = LoggerFactory.getLogger(ServletConnectionFactory.class);

    // the created connection factory
    protected ConnectionFactory connectionFactory;

    /** the Request Handler (Router) */
    @Reference(target = "(org.forgerock.openidm.router=*)")
    protected RequestHandler requestHandler = null;

    /** Script Registry service. */
    @Reference(policy = ReferencePolicy.DYNAMIC)
    private ScriptRegistry scriptRegistry = null;

    /** Enhanced configuration service. */
    @Reference(policy = ReferencePolicy.DYNAMIC)
    private EnhancedConfig enhancedConfig = null;

    @Reference(policy = ReferencePolicy.DYNAMIC, target = "(service.pid=org.forgerock.openidm.maintenance)")
    private Filter maintenanceFilter = null;

    @Activate
    protected void activate(ComponentContext context) throws ServletException, NamespaceException {
        logger.debug("Creating servlet router/connection factory");
        String factoryPid = enhancedConfig.getConfigurationFactoryPid(context);
        if (StringUtils.isNotBlank(factoryPid)) {
            throw new IllegalArgumentException("Factory configuration not allowed, must not have property: "
                    + ServerConstants.CONFIG_FACTORY_PID);
        }
        try {
            final AuditFilter auditFilter = new AuditFilter(connectionFactory);
            connectionFactory = newWrappedInternalConnectionFactory(Resources.newInternalConnectionFactory(
                    init(enhancedConfig.getConfigurationAsJson(context), requestHandler, auditFilter)));
            auditFilter.setConnectionFactory(connectionFactory);
        } catch (Throwable t) {
            logger.error("Failed to configure the Filtered Router service", t);
        }

        logger.info("Servlet ConnectionFactory created.");
    }

    @Deactivate
    protected synchronized void deactivate(ComponentContext context) {
    }

    private ConnectionFactory newWrappedInternalConnectionFactory(final ConnectionFactory connectionFactory) {
        return new ConnectionFactory() {
            @Override
            public void close() {
                connectionFactory.close();
            }

            @Override
            public Connection getConnection() throws ResourceException {
                return new AbstractConnectionWrapper<Connection>(connectionFactory.getConnection()) {

                    @Override
                    public ResourceResponse create(Context context, CreateRequest request)
                            throws ResourceException {
                        EventEntry measure = Publisher.start(getRouterEventName(request), request, null);
                        try {
                            return super.create(context, copyOfCreateRequest(request));
                        } finally {
                            measure.end();
                        }
                    }

                    @Override
                    public Promise<ResourceResponse, ResourceException> createAsync(Context context,
                            CreateRequest request) {
                        final EventEntry measure = Publisher.start(getRouterEventName(request), request, null);
                        return super.createAsync(context, copyOfCreateRequest(request)).thenAlways(new Runnable() {
                            @Override
                            public void run() {
                                measure.end();
                            }
                        });
                    }

                    @Override
                    public ResourceResponse read(Context context, ReadRequest request) throws ResourceException {
                        EventEntry measure = Publisher.start(getRouterEventName(request), request, null);
                        try {
                            return super.read(context, request);
                        } finally {
                            measure.end();
                        }
                    }

                    @Override
                    public Promise<ResourceResponse, ResourceException> readAsync(Context context,
                            ReadRequest request) {
                        final EventEntry measure = Publisher.start(getRouterEventName(request), request, null);
                        return super.readAsync(context, request).thenAlways(new Runnable() {
                            @Override
                            public void run() {
                                measure.end();
                            }
                        });
                    }

                    @Override
                    public ResourceResponse update(Context context, UpdateRequest request)
                            throws ResourceException {
                        final EventEntry measure = Publisher.start(getRouterEventName(request), request, null);
                        try {
                            return super.update(context, request);
                        } finally {
                            measure.end();
                        }
                    }

                    @Override
                    public Promise<ResourceResponse, ResourceException> updateAsync(Context context,
                            UpdateRequest request) {
                        final EventEntry measure = Publisher.start(getRouterEventName(request), request, null);
                        return super.updateAsync(context, request).thenAlways(new Runnable() {
                            @Override
                            public void run() {
                                measure.end();
                            }
                        });
                    }

                    @Override
                    public ResourceResponse delete(Context context, DeleteRequest request)
                            throws ResourceException {
                        EventEntry measure = Publisher.start(getRouterEventName(request), request, null);
                        try {
                            return super.delete(context, request);
                        } finally {
                            measure.end();
                        }
                    }

                    @Override
                    public Promise<ResourceResponse, ResourceException> deleteAsync(Context context,
                            DeleteRequest request) {
                        final EventEntry measure = Publisher.start(getRouterEventName(request), request, null);
                        return super.deleteAsync(context, request).thenAlways(new Runnable() {
                            @Override
                            public void run() {
                                measure.end();
                            }
                        });
                    }

                    @Override
                    public ResourceResponse patch(Context context, PatchRequest request) throws ResourceException {
                        EventEntry measure = Publisher.start(getRouterEventName(request), request, null);
                        try {
                            return super.patch(context, request);
                        } finally {
                            measure.end();
                        }
                    }

                    @Override
                    public Promise<ResourceResponse, ResourceException> patchAsync(Context context,
                            PatchRequest request) {
                        final EventEntry measure = Publisher.start(getRouterEventName(request), request, null);
                        return super.patchAsync(context, request).thenAlways(new Runnable() {
                            @Override
                            public void run() {
                                measure.end();
                            }
                        });
                    }

                    @Override
                    public ActionResponse action(Context context, ActionRequest request) throws ResourceException {
                        EventEntry measure = Publisher.start(getRouterEventName(request), request, null);
                        try {
                            return super.action(context, request);
                        } finally {
                            measure.end();
                        }
                    }

                    @Override
                    public Promise<ActionResponse, ResourceException> actionAsync(Context context,
                            ActionRequest request) {
                        final EventEntry measure = Publisher.start(getRouterEventName(request), request, null);
                        return super.actionAsync(context, request).thenAlways(new Runnable() {
                            @Override
                            public void run() {
                                measure.end();
                            }
                        });
                    }

                    @Override
                    public QueryResponse query(Context context, QueryRequest request, QueryResourceHandler handler)
                            throws ResourceException {
                        EventEntry measure = Publisher.start(getRouterEventName(request), request, null);
                        try {
                            return super.query(context, request, handler);
                        } finally {
                            measure.end();
                        }
                    }

                    @Override
                    public Promise<QueryResponse, ResourceException> queryAsync(Context context,
                            QueryRequest request, QueryResourceHandler handler) {
                        final EventEntry measure = Publisher.start(getRouterEventName(request), request, null);
                        return super.queryAsync(context, request, handler).thenAlways(new Runnable() {
                            @Override
                            public void run() {
                                measure.end();
                            }
                        });
                    }
                };
            }

            @Override
            public Promise<Connection, ResourceException> getConnectionAsync() {
                try {
                    return newResultPromise(getConnection());
                } catch (ResourceException e) {
                    return e.asPromise();
                }
            }

            /**
             * @param request the router request
             * @return an event name For monitoring purposes
             */
            private Name getRouterEventName(Request request) {
                RequestType requestType = request.getRequestType();
                String idContext;

                // For query and action group statistics by full URI
                // Create has only the component name in the getResourceName to start with
                if (RequestType.QUERY.equals(requestType) || RequestType.ACTION.equals(requestType)
                        || RequestType.CREATE.equals(requestType)) {
                    idContext = request.getResourcePath();
                } else {
                    // For RUD, patch group statistics without the local resource identifier
                    idContext = request.getResourcePathObject().head(request.getResourcePathObject().size() - 1)
                            .toString();
                }

                String eventName = new StringBuilder(EVENT_ROUTER_PREFIX).append(idContext).append("/")
                        .append(requestType.toString().toLowerCase()).toString();

                return Name.get(eventName);
            }
        };
    }

    /**
     * Initialize the router with configuration. Supports modifying router configuration.
     *
     * @param configuration the router configuration listing filters that are installed
     * @param handler the request handler (router)
     * @param auditFilter the audit filter to attach to the request handler
     * @return the RequestHandler decorated with a FilterChain consisting of any filters that are configured
     */
    RequestHandler init(JsonValue configuration, final RequestHandler handler, final Filter auditFilter)
            throws ScriptException, ResourceException {
        final JsonValue filterConfig = configuration.get("filters").expect(List.class);
        // # filters = config filters + maintenance + logging + audit
        final List<Filter> filters = new ArrayList<>(filterConfig.size() + 3);

        filters.add(
                Filters.conditionalFilter(Filters.matchResourcePath("((?!(audit|updates)).)*"), maintenanceFilter));
        filters.add(newLoggingFilter());
        filters.add(Filters.conditionalFilter(Filters.matchResourcePath("^(?!.*(^audit/)).*$"), auditFilter));

        for (JsonValue jv : filterConfig) {
            Filter filter = newFilter(jv);
            if (null != filter) {
                filters.add(filter);
            }
        }

        // filters will always have at least the logging filter
        return new FilterChain(handler, filters);
    }

    /**
     * Create a Filter from the filter configuration.
     *
     * @param config
     *            the configuration describing a single filter.
     * @return a Filter
     * @throws org.forgerock.json.JsonValueException
     *             TODO.
     */
    private Filter newFilter(JsonValue config) throws JsonValueException, ScriptException {
        FilterCondition filterCondition = null;

        final Pair<JsonPointer, ScriptEntry> condition = getScript(config.get("condition"));
        final Pair<JsonPointer, ScriptEntry> onRequest = getScript(config.get("onRequest"));
        final Pair<JsonPointer, ScriptEntry> onResponse = getScript(config.get("onResponse"));
        final Pair<JsonPointer, ScriptEntry> onFailure = getScript(config.get("onFailure"));

        // Require at least one of the following
        if (null == onRequest && null == onResponse && null == onFailure) {
            return null;
        }

        // Check for condition on pattern
        Pattern pattern = config.get("pattern").asPattern();
        if (null != pattern) {
            filterCondition = Filters.matchResourcePath(pattern);
        }

        // Check for condition on type
        final EnumSet<RequestType> requestTypes = EnumSet.noneOf(RequestType.class);
        for (JsonValue method : config.get("methods").expect(List.class)) {
            requestTypes.add(method.asEnum(RequestType.class));
        }
        if (!requestTypes.isEmpty()) {
            filterCondition = (null == filterCondition) ? Filters.matchRequestType(requestTypes)
                    : Filters.and(filterCondition, Filters.matchRequestType(requestTypes));
        }

        // Create the filter
        Filter filter = (null == filterCondition) ? new ScriptedFilter(onRequest, onResponse, onFailure)
                : Filters.conditionalFilter(filterCondition, new ScriptedFilter(onRequest, onResponse, onFailure));

        // Check for a condition script
        if (null != condition) {
            FilterCondition conditionFilterCondition = new FilterCondition() {
                @Override
                public boolean matches(final Context context, final Request request) {
                    try {
                        final Script script = condition.getValue().getScript(context);
                        script.put("request", request);
                        script.put("context", context);
                        return (Boolean) script.eval();
                    } catch (ScriptException e) {
                        logger.warn("Failed to evaluate filter condition: ", e.getMessage(), e);
                    }
                    return false;
                }
            };
            filter = Filters.conditionalFilter(conditionFilterCondition, filter);
        }
        return filter;
    }

    private Pair<JsonPointer, ScriptEntry> getScript(JsonValue scriptJson) throws ScriptException {
        if (scriptJson.expect(Map.class).isNull()) {
            return null;
        }

        ScriptEntry entry = scriptRegistry.takeScript(scriptJson);
        return Pair.of(scriptJson.getPointer(), entry);
    }

    private static final ResultHandler<Response> LOGGING_RESULT_HANDLER = new ResultHandler<Response>() {
        @Override
        public void handleResult(Response response) {
            logger.trace("Result: {}", response);
        }
    };

    private static final ExceptionHandler<ResourceException> LOGGING_EXCEPTION_HANDLER = new ExceptionHandler<ResourceException>() {
        @Override
        public void handleException(ResourceException exception) {
            int code = exception.getCode();
            if (logger.isTraceEnabled()) {
                logger.trace("Resource exception: {} {}: \"{}\"", exception.getCode(), exception.getReason(),
                        exception.getMessage(), exception);
            } else if (code >= 500 && code <= 599) { // log server-side errors
                logger.warn("Resource exception: {} {}: \"{}\"", exception.getCode(), exception.getReason(),
                        exception.getMessage(), exception);
            }
        }
    };

    private Filter newLoggingFilter() {
        return new Filter() {
            @Override
            public Promise<ActionResponse, ResourceException> filterAction(Context context, ActionRequest request,
                    RequestHandler next) {
                logger.trace("Request: {}", request);
                return next.handleAction(context, request).thenOnResultOrException(LOGGING_RESULT_HANDLER,
                        LOGGING_EXCEPTION_HANDLER);
            }

            @Override
            public Promise<ResourceResponse, ResourceException> filterCreate(Context context, CreateRequest request,
                    RequestHandler next) {
                logger.trace("Request: {}", request);
                return next.handleCreate(context, request).thenOnResultOrException(LOGGING_RESULT_HANDLER,
                        LOGGING_EXCEPTION_HANDLER);
            }

            @Override
            public Promise<ResourceResponse, ResourceException> filterDelete(Context context, DeleteRequest request,
                    RequestHandler next) {
                logger.trace("Request: {}", request);
                return next.handleDelete(context, request).thenOnResultOrException(LOGGING_RESULT_HANDLER,
                        LOGGING_EXCEPTION_HANDLER);
            }

            @Override
            public Promise<ResourceResponse, ResourceException> filterPatch(Context context, PatchRequest request,
                    RequestHandler next) {
                logger.trace("Request: {}", request);
                return next.handlePatch(context, request).thenOnResultOrException(LOGGING_RESULT_HANDLER,
                        LOGGING_EXCEPTION_HANDLER);
            }

            @Override
            public Promise<QueryResponse, ResourceException> filterQuery(Context context, QueryRequest request,
                    QueryResourceHandler handler, RequestHandler next) {
                logger.trace("Request: {}", request);
                return next.handleQuery(context, request, handler).thenOnResultOrException(LOGGING_RESULT_HANDLER,
                        LOGGING_EXCEPTION_HANDLER);
            }

            @Override
            public Promise<ResourceResponse, ResourceException> filterRead(Context context, ReadRequest request,
                    RequestHandler next) {
                logger.trace("Request: {}", request);
                return next.handleRead(context, request).thenOnResultOrException(LOGGING_RESULT_HANDLER,
                        LOGGING_EXCEPTION_HANDLER);
            }

            @Override
            public Promise<ResourceResponse, ResourceException> filterUpdate(Context context, UpdateRequest request,
                    RequestHandler next) {
                logger.trace("Request: {}", request);
                return next.handleUpdate(context, request).thenOnResultOrException(LOGGING_RESULT_HANDLER,
                        LOGGING_EXCEPTION_HANDLER);
            }
        };
    }

    // ----- Implementation of ConnectionFactory

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

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

    @Override
    public void close() {
        connectionFactory.close();
    }

}