org.wisdom.api.router.Route.java Source code

Java tutorial

Introduction

Here is the source code for org.wisdom.api.router.Route.java

Source

/*
 * #%L
 * Wisdom-Framework
 * %%
 * Copyright (C) 2013 - 2014 Wisdom Framework
 * %%
 * Licensed 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.
 * #L%
 */
package org.wisdom.api.router;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.net.MediaType;
import org.wisdom.api.Controller;
import org.wisdom.api.http.*;
import org.wisdom.api.router.parameters.ActionParameter;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Represents a route.
 * Routes can be bound if an action method can handle the request, or unbound if not.
 * <p>
 * <strong>IMPORTANT:</strong> Router implementation must extends this class to provide a valid implementation of the
 * {@link org.wisdom.api.router.Route#invoke()} method.
 */
public class Route {

    /**
     * The HTTP method.
     */
    protected final HttpMethod httpMethod;

    /**
     * The path.
     */
    protected final String uri;

    /**
     * The invoked controller, only if the route is `bound`.
     */
    protected final Controller controller;

    /**
     * The invoked method, only if the route is `bound`.
     */
    protected final Method controllerMethod;

    /**
     * The list of parameters.
     */
    protected final List<String> parameterNames;

    /**
     * The path as regex to extract path parameters.
     */
    protected final Pattern regex;

    /**
     * The set of accepted media types.
     */
    protected Set<MediaType> acceptedMediaTypes = Collections.emptySet();

    /**
     * The set of produced media types.
     */
    protected Set<MediaType> producedMediaTypes = Collections.emptySet();

    /**
     * The list of parameters.
     */
    protected final List<ActionParameter> arguments;

    /**
     * The status to return if the route is unbound.
     */
    protected int unboundStatus;

    /**
     * Constructor used in case of delegation.
     */
    protected Route() {
        httpMethod = null;
        uri = null;
        controller = null;
        controllerMethod = null;
        parameterNames = null;
        regex = null;
        arguments = null;
    }

    /**
     * Main constructor.
     *
     * @param httpMethod       the method
     * @param uri              the uri
     * @param controller       the controller object
     * @param controllerMethod the controller method
     */
    public Route(HttpMethod httpMethod, String uri, Controller controller, Method controllerMethod) {
        this.httpMethod = httpMethod;
        this.uri = uri;
        this.controller = controller;
        this.controllerMethod = controllerMethod;
        // Unbound route case.
        if (controllerMethod != null) {
            if (!controllerMethod.isAccessible()) {
                controllerMethod.setAccessible(true);
            }
            this.arguments = RouteUtils.buildActionParameterList(this.controllerMethod);
            parameterNames = ImmutableList.copyOf(RouteUtils.extractParameters(uri));
            regex = Pattern.compile(RouteUtils.convertRawUriToRegex(uri));
        } else {
            parameterNames = Collections.emptyList();
            regex = null;
            arguments = Collections.emptyList();
        }

        if (controller == null) {
            unboundStatus = Status.NOT_FOUND;
        }
    }

    /**
     * Constructors used for `unbound` route.
     *
     * @param httpMethod    the method
     * @param uri           the path
     * @param unboundStatus the HTTP status to return
     */
    public Route(HttpMethod httpMethod, String uri, int unboundStatus) {
        this(httpMethod, uri, null, null);
        this.unboundStatus = unboundStatus;
    }

    /**
     * Sets the set of media types accepted by the route.
     *
     * @param types the set of type
     * @return the current route
     */
    public Route accepts(String... types) {
        Preconditions.checkNotNull(types);
        final ImmutableSet.Builder<MediaType> builder = new ImmutableSet.Builder<>();
        builder.addAll(this.acceptedMediaTypes);
        for (String s : types) {
            builder.add(MediaType.parse(s));
        }
        this.acceptedMediaTypes = builder.build();
        return this;
    }

    /**
     * Sets the set of media types accepted by the route.
     *
     * @param types the set of type
     * @return the current route
     * @see #accepts(String...)
     */
    public Route accepting(String... types) {
        accepts(types);
        return this;
    }

    /**
     * Sets the set of media types produced by the route.
     *
     * @param types the set of type
     * @return the current route
     */
    public Route produces(String... types) {
        Preconditions.checkNotNull(types);
        final ImmutableSet.Builder<MediaType> builder = new ImmutableSet.Builder<>();
        builder.addAll(this.producedMediaTypes);
        for (String s : types) {
            final MediaType mt = MediaType.parse(s);
            if (mt.hasWildcard()) {
                throw new RoutingException("A route cannot `produce` a mime type with a wildcard: " + mt);
            }
            builder.add(mt);
        }
        this.producedMediaTypes = builder.build();
        return this;
    }

    /**
     * Sets the set of media types produced by the route.
     *
     * @param types the set of type
     * @return the current route
     * @see #produces(String...)
     */
    public Route producing(String... types) {
        produces(types);
        return this;
    }

    /**
     * Gets the route uri.
     *
     * @return the uri
     */
    public String getUrl() {
        return uri;
    }

    /**
     * Gets the HTTP method.
     *
     * @return the method
     */
    public HttpMethod getHttpMethod() {
        return httpMethod;
    }

    /**
     * Gets the controller class handling the route.
     *
     * @return the controller class, {@literal null} for unbound routes
     */
    public Class<? extends Controller> getControllerClass() {
        return controller.getClass();
    }

    /**
     * Gets the controller method handling the route.
     *
     * @return the controller method, {@literal null} for unbound routes
     */
    public Method getControllerMethod() {
        return controllerMethod;
    }

    /**
     * Matches /index to /index or /me/1 to /person/{id}.
     *
     * @param method the method
     * @param uri    the uri
     * @return True if the actual route matches a raw rout. False if not.
     */
    public boolean matches(HttpMethod method, String uri) {
        if (this.httpMethod == method) {
            Matcher matcher = regex.matcher(uri);
            return matcher.matches();
        } else {
            return false;
        }
    }

    /**
     * Matches /index to /index or /me/1 to /person/{id}.
     *
     * @param method the method
     * @param uri    the uri
     * @return True if the actual route matches a raw rout. False if not.
     */
    public boolean matches(String method, String uri) {
        return matches(HttpMethod.from(method), uri);
    }

    /**
     * This method does not do any decoding / encoding.
     * <p>
     * If you want to decode you have to do it yourself.
     * <p>
     * Most likely with:
     * http://docs.oracle.com/javase/6/docs/api/java/net/URI.html
     *
     * @param uri The whole encoded uri.
     * @return A map with all parameters of that uri. Encoded in => encoded out.
     */
    public Map<String, String> getPathParametersEncoded(String uri) {
        Map<String, String> map = Maps.newHashMap();
        if (regex == null) {
            // Unbound case
            return map;
        }
        Matcher m = regex.matcher(uri);
        if (m.matches()) {
            for (int i = 1; i < m.groupCount() + 1; i++) {
                map.put(parameterNames.get(i - 1), m.group(i));
            }
        }
        return map;
    }

    /**
     * Gets the controller object.
     *
     * @return the controller handling the request, {@literal null} for unbound routes.
     */
    public Controller getControllerObject() {
        return controller;
    }

    /**
     * Invokes the action method. This method must be overridden by router implementation.
     * On unbound route, a {@literal 404 - NOT FOUND} result is returned. Otherwise,
     * it invokes the route without any parameter support / injection support.
     * <p>
     *
     * @return the result returned by the action method
     * @throws java.lang.Exception if anything goes wrong
     */
    public Result invoke() throws Exception {
        if (isUnbound()) {
            return new Result().status(unboundStatus).noContentIfNone();
        } else {
            return (Result) controllerMethod.invoke(controller);
        }
    }

    /**
     * The list of arguments.
     *
     * @return the list, empty if none.
     */
    public List<ActionParameter> getArguments() {
        return arguments;
    }

    /**
     * A simple implementation of the toString method for routes.
     *
     * @return the string representation
     */
    @Override
    public String toString() {
        if (isUnbound()) {
            return "{" + getHttpMethod() + " " + getUrl() + " => " + "UNBOUND (" + unboundStatus + ")" + "}";
        }

        StringBuilder builder = new StringBuilder();
        builder.append("{");
        builder.append(getHttpMethod()).append(" ").append(uri).append(" => ")
                .append(controller.getClass().toString()).append("#").append(controllerMethod.getName());

        if (!acceptedMediaTypes.isEmpty()) {
            builder.append(" - accepting: ").append(acceptedMediaTypes);
        }

        if (!producedMediaTypes.isEmpty()) {
            builder.append(" - producing: ").append(producedMediaTypes);
        }

        return builder.toString();
    }

    /**
     * For unbound routes, only the uri and method are checked. For bound routes, the controller and method are also
     * checks.
     *
     * @param o the compared object
     * @return {@literal true} if the the given route is equal to the current route, {@literal false} otherwise.
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Route)) { // NOSONAR we use instanceOf to support children class too.
            return false;
        }

        Route route = (Route) o;

        if (this.isUnbound()) {
            return route.isUnbound() && httpMethod == route.httpMethod && uri.equals(route.uri);
        }
        // Bound route.
        return controller.equals(route.controller) && controllerMethod.equals(route.controllerMethod)
                && httpMethod == route.httpMethod && uri.equals(route.uri);

    }

    /**
     * A simple hash code method.
     *
     * @return the hash code.
     */
    @Override
    public int hashCode() {
        int result;
        if (isUnbound()) {
            result = httpMethod.hashCode();
            result = 31 * result + uri.hashCode();
            result = 31 * result + unboundStatus;
        } else {
            result = httpMethod.hashCode();
            result = 31 * result + uri.hashCode();
            result = 31 * result + controller.hashCode();
            result = 31 * result + controllerMethod.hashCode();
        }
        return result;
    }

    /**
     * Is the route unbound?
     *
     * @return {@literal true} if the route is unbound, {@literal false} otherwise.
     */
    public boolean isUnbound() {
        return controllerMethod == null;
    }

    /**
     * Gets the HTTP Status to return for this unbound route. This method is meaningful only if the route is unbound
     * (and so cannot be served).
     *
     * @return {@link Status#NOT_FOUND} when there are no action method to handle the route,
     * {@link Status#UNSUPPORTED_MEDIA_TYPE} when the request content cannot be accepted.
     */
    public int getUnboundStatus() {
        return unboundStatus;
    }

    /**
     * Checks whether or not the current route can accept the given request. It checks the request content type
     * against the list of accepted mime types. It does not return a boolean but an integer indicating the level of
     * acceptation: 0 - not accepted, 1 - accepted using a wildcard, 2 - full accept. This distinction comes from the
     * possibility to have wildcard in the accepted mime types. For instance, if the request contains `text/plain`,
     * and the route accepts `text/*`, it returns 1. If the route would have accepted `text/plain`, 2 would have been
     * returned.
     *
     * @param request the incoming request
     * @return the acceptation level (0, 1 or 2).
     */
    public int isCompliantWithRequestContentType(Request request) {
        if (acceptedMediaTypes == null || acceptedMediaTypes.isEmpty() || request == null) {
            return 2;
        } else {
            String content = request.contentMimeType();
            if (content == null) {
                return 2;
            } else {
                // For all consume, check whether we accept it
                MediaType contentMimeType = MediaType.parse(request.contentMimeType());
                for (MediaType type : acceptedMediaTypes) {
                    if (contentMimeType.is(type)) {
                        if (type.hasWildcard()) {
                            return 1;
                        } else {
                            return 2;
                        }
                    }
                }
                return 0;
            }
        }
    }

    /**
     * Checks whether the given request is compliant with the media type accepted by the current route.
     *
     * @param request the request
     * @return {@code true} if the request is compliant, {@code false} otherwise
     */
    public boolean isCompliantWithRequestAccept(Request request) {
        if (producedMediaTypes == null || producedMediaTypes.isEmpty() || request == null
                || request.getHeader(HeaderNames.ACCEPT) == null) {
            return true;
        } else {
            for (MediaType mt : producedMediaTypes) {
                if (request.accepts(mt.toString())) {
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * @return the set of produced media types.
     */
    public Set<MediaType> getProducedMediaTypes() {
        return producedMediaTypes;
    }

    /**
     * @return the set of accepted media types.
     */
    public Set<MediaType> getAcceptedMediaTypes() {
        return acceptedMediaTypes;
    }
}