org.restexpress.route.RouteBuilder.java Source code

Java tutorial

Introduction

Here is the source code for org.restexpress.route.RouteBuilder.java

Source

/**
 *        Licensed to the Apache Software Foundation (ASF) under one
 *        or more contributor license agreements.  See the NOTICE file
 *        distributed with this work for additional information
 *        regarding copyright ownership.  The ASF licenses this file
 *        to you under the Apache License, Version 2.0 (the
 *        "License"); you may not use this file except in compliance
 *        with the License.  You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *        Unless required by applicable law or agreed to in writing,
 *        software distributed under the License is distributed on an
 *        "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *        KIND, either express or implied.  See the License for the
 *        specific language governing permissions and limitations
 *        under the License.
 *
 */
package org.restexpress.route;

import static org.jboss.netty.handler.codec.http.HttpMethod.DELETE;
import static org.jboss.netty.handler.codec.http.HttpMethod.GET;
import static org.jboss.netty.handler.codec.http.HttpMethod.HEAD;
import static org.jboss.netty.handler.codec.http.HttpMethod.OPTIONS;
import static org.jboss.netty.handler.codec.http.HttpMethod.POST;
import static org.jboss.netty.handler.codec.http.HttpMethod.PUT;
import static org.reflections.ReflectionUtils.getAllMethods;
import static org.reflections.ReflectionUtils.withName;
import static org.reflections.ReflectionUtils.withParameters;
import static org.reflections.ReflectionUtils.withParametersCount;

import java.io.File;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.restexpress.ConfigurationException;
import org.restexpress.Request;
import org.restexpress.Response;
import org.restexpress.RestExpress;
import org.restexpress.domain.metadata.RouteMetadata;
import org.restexpress.domain.metadata.UriMetadata;
import org.restexpress.route.invoker.Invoker;
import org.restexpress.route.invoker.Invokers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

/**
 * Builds a route for a single URI. If a URI is given with no methods or
 * actions, the builder creates routes for the GET, POST, PUT, and DELETE HTTP
 * methods for the given URI.
 * 
 * Change
 * <ul>
 * <li>When using default mapping only, this builder did not raise an exception
 * if a declared action has no method, but log a warning.</li>
 * <li>Turn off serialization option when a method return a {@link File}</li>
 * </ul>
 * 
 * @author <a href="mailto:jguibert@intelligents-ia.com" >Jerome Guibert</a>
 * @author toddf
 * @since 2010
 */
public abstract class RouteBuilder {

    public static Logger logger = LoggerFactory.getLogger(RouteBuilder.class);

    /**
     * Default action name.
     */
    static final String DELETE_ACTION_NAME = "delete";
    static final String GET_ACTION_NAME = "read";
    static final String POST_ACTION_NAME = "create";
    static final String PUT_ACTION_NAME = "update";
    static final String HEAD_ACTION_NAME = "headers";
    static final String OPTION_ACTION_NAME = "options";
    /**
     * Default supported HTTP methods.
     */
    static final List<HttpMethod> DEFAULT_HTTP_METHODS = Arrays.asList(new HttpMethod[] { GET, POST, PUT, DELETE });
    /**
     * Default action mapping
     */
    static final Map<HttpMethod, String> ACTION_MAPPING = Maps.newHashMap();

    static {
        ACTION_MAPPING.put(DELETE, DELETE_ACTION_NAME);
        ACTION_MAPPING.put(GET, GET_ACTION_NAME);
        ACTION_MAPPING.put(POST, POST_ACTION_NAME);
        ACTION_MAPPING.put(PUT, PUT_ACTION_NAME);
        ACTION_MAPPING.put(HEAD, HEAD_ACTION_NAME);
        ACTION_MAPPING.put(OPTIONS, OPTION_ACTION_NAME);
    }

    private final String uri;
    /**
     * Set of supported HTTP method.
     */
    private List<HttpMethod> methods = Lists.newArrayList();
    /**
     * List of action name by HTTP method.
     */
    private final Map<HttpMethod, String> actionNames = Maps.newHashMap();
    /**
     * List of Invoker method by HTTP method.
     */
    private final Map<HttpMethod, Invoker> invokers = Maps.newHashMap();
    /**
     * Controller object.
     */
    private final Object controller;
    /**
     * Should user serialization flag.
     */
    private boolean shouldSerializeResponse = true;
    /**
     * Route name.
     */
    private String name;
    /**
     * Set of flag.
     */
    private final Set<String> flags = new HashSet<>();
    /**
     * Set of parameter.
     */
    private final Map<String, Object> parameters = new HashMap<String, Object>();
    /**
     * Aliases.
     */
    protected final List<String> aliases = new ArrayList<String>();

    /**
     * Create a RouteBuilder instance for the given URI pattern. URIs that match
     * the pattern will map to methods on the POJO controller.
     * 
     * @param uri
     *            a URI pattern
     * @param controller
     *            the POJO service controller.
     */
    public RouteBuilder(final String uri, final Object controller) {
        super();
        this.uri = uri;
        this.controller = controller;
    }

    /**
     * Child builder must implements this method.
     * 
     * @param pattern
     * @param invoker
     * @param method
     * @param shouldSerializeResponse
     * @param name
     * @param supportedFormats
     * @param flags
     * @param parameters
     * @param baseUrl
     * @return
     */
    protected abstract Route newRoute(String pattern, Invoker invoker, HttpMethod method,
            boolean shouldSerializeResponse, String name, Set<String> flags, Map<String, Object> parameters);

    /**
     * Child builder must implements this method.
     * 
     * @param uri
     * @return
     */
    protected abstract String toRegexPattern(String uri);

    /**
     * Map a service method name (action) to a particular HTTP method (e.g. GET,
     * POST, PUT, DELETE, HEAD, OPTIONS)
     * 
     * @param action
     *            the name of a method within the service POJO.
     * @param method
     *            the HTTP method that should invoke the service method.
     * @return the RouteBuilder instance.
     */
    public RouteBuilder action(final String action, final HttpMethod method) {
        if (!actionNames.containsKey(method)) {
            actionNames.put(method, action);
        }
        if (!methods.contains(method))
            methods.add(method);
        return this;
    }

    /**
     * Map a service method (action) to a particular HTTP method (e.g. GET,
     * POST, PUT, DELETE, HEAD, OPTIONS)
     * 
     * @param invoker
     *            {@link Invoker} instance to call the method within the service
     *            POJO.
     * @param method
     *            the HTTP method that should invoke the service method.
     * @return the RouteBuilder instance.
     */
    public RouteBuilder action(final Invoker invoker, final HttpMethod method) {
        if (!invokers.containsKey(method)) {
            invokers.put(method, invoker);
        }
        if (!methods.contains(method))
            methods.add(method);
        return this;
    }

    /**
     * Defines HTTP methods that the route will support (e.g. GET, PUT, POST,
     * DELETE, OPTIONS, HEAD). This utilizes the default HTTP method to service
     * action mapping (e.g. GET maps to read(), PUT to update(), etc.).
     * 
     * @param methods
     *            the HTTP methods supported by the route.
     * @return the RouteBuilder instance.
     */
    public RouteBuilder method(final HttpMethod... methods) {
        for (HttpMethod method : methods) {
            if (!this.methods.contains(method)) {
                this.methods.add(method);
            }
        }
        return this;
    }

    /**
     * Turns off serialization of the response--returns the response body as
     * pain text.
     * 
     * @return the RouteBuilder instance.
     */
    public RouteBuilder noSerialization() {
        this.shouldSerializeResponse = false;
        return this;
    }

    /**
     * Turns on response serialization (the default) so the response body will
     * be serialized (e.g. into JSON or XML).
     * 
     * @return the RouteBuilder instance.
     */
    public RouteBuilder performSerialization() {
        this.shouldSerializeResponse = true;
        return this;
    }

    /**
     * Give the route a known name to facilitate retrieving the route by name.
     * This facilitates using the route URI pattern to create Link instances via
     * LinkUtils.asLinks().
     * 
     * The name must be unique for each URI pattern.
     * 
     * @param name
     *            the given name of the route for later retrieval.
     * @return the RouteBuilder instance.
     */
    public RouteBuilder name(final String name) {
        this.name = name;
        return this;
    }

    /**
     * Flags are boolean settings that are created at route definition time.
     * These flags can be used to pass booleans to preprocessors, controllers,
     * or postprocessors. An example might be: flag(NO_AUTHORIZATION), which
     * might inform an authorization preprocessor to skip authorization for this
     * route.
     * 
     * @param flagValue
     *            the name of the flag.
     * @return this RouteBuilder to facilitate method chaining.
     */
    public RouteBuilder flag(final String flagValue) {
        flags.add(flagValue);
        return this;
    }

    /**
     * Parameters are named settings that are created at route definition time.
     * These parameters can be used to pass data to subsequent preprocessors,
     * controllers, or postprocessors. This is a way to pass data from a route
     * definition down to subsequent controllers, etc. An example might be:
     * setParameter("route", "read_foo") setParameter("permission",
     * "view_private_data"), which might inform an authorization preprocessor of
     * what permission is being requested on a given resource.
     * 
     * @param name
     *            the name of the parameter.
     * @param value
     *            an object that is the parameter value.
     * @return this RouteBuilder to facilitate method chaining.
     */
    public RouteBuilder parameter(final String name, final Object value) {
        parameters.put(name, value);
        return this;
    }

    public RouteBuilder useStreamingMultipartUpload() {
        // TODO: complete supportMultipart()
        return this;
    }

    public RouteBuilder useStreamingDownload() {
        // TODO: complete useStreamingdownload()
        return this;
    }

    /**
     * Build the Route instances. The last step in the Builder process.
     * 
     * @param restExpress
     *            {@link RestExpress} instance
     * @return a {@link RouteDefinition} instance
     */
    public RouteDefinition build(RestExpress restExpress) {
        if (methods.isEmpty()) {
            methods.addAll(DEFAULT_HTTP_METHODS);
        }

        final List<Route> routes = new ArrayList<Route>();
        final String pattern = toRegexPattern(uri);

        for (final HttpMethod httpMethod : methods) {

            boolean serializeResponse = this.shouldSerializeResponse;
            String actionName = actionNames.get(httpMethod);

            Invoker invoker = null;
            // no method name, maybe an invoker ?
            if (actionName == null) {
                invoker = invokers.get(httpMethod);
            }
            // no invoker
            if (invoker == null) {
                Method action = null;
                // lookup in default action
                if (actionName == null) {
                    // on default mapping, if method is not found we did not
                    // raise a configuration exception. We just log a warning.
                    actionName = ACTION_MAPPING.get(httpMethod);
                    try {
                        action = determineActionMethod(controller, actionName);
                    } catch (ConfigurationException e) {
                        logger.warn("{} did not support HTTP {} method.", controller.getClass().getSimpleName(),
                                httpMethod);
                    }
                } else {
                    action = determineActionMethod(controller, actionName);
                }

                if (action != null) {
                    // set accessible
                    action.setAccessible(true);
                    // compute serialization
                    serializeResponse = shouldSerializeResponse(action);
                    // find a good invoker
                    invoker = Invokers.newInvoker(restExpress.paramConverterProvider(), controller, action);
                }

            } else {
                // we have an invoker
                serializeResponse = shouldSerializeResponse(invoker.action());
            }

            // add route
            if (invoker != null) {
                // create route
                Route route = newRoute(pattern, invoker, httpMethod, serializeResponse, name, flags, parameters);
                // add result
                routes.add(route);
            }
        }

        // build meta data and return a RouteDefinition.
        return new RouteDefinition(routes, asMetadata(routes));
    }

    /**
     * Should Serialize Response for specified action ? This method return false
     * if action return type is
     * <ul>
     * <li>a {@link File}</li>
     * <li>a {@link ChannelBuffer}</li>
     * <li>shouldSerializeResponse flag is false (from call on
     * {@link #noSerialization()})</li>
     * </ul>
     * 
     * @param action
     *            {@link Method} to check
     * @return True if we should use serialization
     */
    private boolean shouldSerializeResponse(Method action) {
        if (shouldSerializeResponse) {
            Class<?> returnType = action.getReturnType();
            return !(File.class.isAssignableFrom(returnType) || ChannelBuffer.class.isAssignableFrom(returnType));
        }
        return Boolean.FALSE;
    }

    /**
     * TODO a lot of refactoring here
     * 
     * @return {@link RouteMetadata}
     */
    private RouteMetadata asMetadata(List<Route> routes) {
        final List<String> methods = new ArrayList<String>();
        final UriMetadata uriMeta = new UriMetadata(uri);

        for (final Route route : routes) {
            uriMeta.addAllParameters(route.getUrlParameters());
            methods.add(route.getMethod().getName());
        }
        final RouteMetadata metadata = new RouteMetadata(name, uriMeta, //
                aliases, methods, shouldSerializeResponse);

        return metadata;
    }

    /**
     * Attempts to find the actionName on the controller.
     * 
     * Assuming a signature of actionName(Request, Response),
     * actionName(Request), actionName(Response) or actionName(), and returns
     * the action as a Method to be used later when the route is invoked.
     * 
     * 
     * 
     * @param controller
     *            a pojo that implements a method named by the action, with
     *            Request and Response as parameters.
     * @param actionName
     *            the name of a method on the given controller pojo.
     * @return a Method instance referring to the action on the controller.
     * @throws ConfigurationException
     *             if an error occurs.
     */
    @SuppressWarnings("unchecked")
    private Method determineActionMethod(final Object controller, final String actionName) {
        try {
            Set<Method> methods = getAllMethods(controller.getClass(), Predicates.and(withName(actionName),
                    withParameters(Request.class, Response.class), withParametersCount(2)));
            if (methods.size() == 1) {
                return methods.iterator().next();
            }
            methods = getAllMethods(controller.getClass(),
                    Predicates.and(withName(actionName), withParametersCount(1), //
                            Predicates.or(withParameters(Request.class), withParameters(Response.class))));
            if (methods.size() == 1) {
                return methods.iterator().next();
            }
            methods = getAllMethods(controller.getClass(),
                    Predicates.and(withName(actionName), withParametersCount(0)));
            if (methods.size() == 1) {
                return methods.iterator().next();
            }
            throw new ConfigurationException(
                    String.format("Method %s(Request request, Response response) NOT FOUND", actionName));
        } catch (final Exception e) {
            throw new ConfigurationException(e);
        }
    }

    /**
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "RouteBuilder [name=" + name + ", uri=" + uri + ", methods=" + methods + "]";
    }

}