org.wisdom.api.router.Route Maven / Gradle / Ivy
/*
* #%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.
*
* IMPORTANT: 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 parameterNames;
/**
* The path as regex to extract path parameters.
*/
protected final Pattern regex;
/**
* The set of accepted media types.
*/
protected Set acceptedMediaTypes = Collections.emptySet();
/**
* The set of produced media types.
*/
protected Set producedMediaTypes = Collections.emptySet();
/**
* The list of parameters.
*/
protected final List 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 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 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.
*
* If you want to decode you have to do it yourself.
*
* 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 getPathParametersEncoded(String uri) {
Map 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.
*
*
* @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 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 getProducedMediaTypes() {
return producedMediaTypes;
}
/**
* @return the set of accepted media types.
*/
public Set getAcceptedMediaTypes() {
return acceptedMediaTypes;
}
}