/*
* #%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.router;
import com.google.common.base.Preconditions;
import com.google.common.net.MediaType;
import org.wisdom.api.Controller;
import org.wisdom.api.annotations.Interception;
import org.wisdom.api.http.*;
import org.wisdom.api.interception.Filter;
import org.wisdom.api.interception.Interceptor;
import org.wisdom.api.interception.RequestContext;
import org.wisdom.api.router.Route;
import org.wisdom.api.router.parameters.ActionParameter;
import org.wisdom.router.parameter.Bindings;
import javax.validation.Constraint;
import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validator;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Delegated route used for interception purpose.
*/
public class RouteDelegate extends Route {
private final Route route;
private final RequestRouter router;
private final boolean mustValidate;
private final Map<String, Object> interceptors;
/**
* Creates a new instance of {@link org.wisdom.router.RouteDelegate}.
*
* @param router the router
* @param route the delegate / wrapped route
*/
public RouteDelegate(RequestRouter router, Route route) {
this.route = route;
this.router = router;
if (!route.isUnbound()) {
this.mustValidate = detectValidationRequirement(route.getControllerMethod());
this.interceptors = extractInterceptors();
} else {
this.mustValidate = false;
this.interceptors = Collections.emptyMap();
}
}
private Map<String, Object> extractInterceptors() {
Map<String, Object> map = new LinkedHashMap<>();
Annotation[] classAnnotations = route.getControllerClass().getAnnotations();
for (Annotation annotation : classAnnotations) {
if (annotation.annotationType().isAnnotationPresent(Interception.class)) {
// Interceptor detected.
map.put(annotation.annotationType().getName(), annotation);
}
}
// Check the method
Annotation[] methodAnnotations = route.getControllerMethod().getAnnotations();
for (Annotation annotation : methodAnnotations) {
if (annotation.annotationType().isAnnotationPresent(Interception.class)) {
// Interceptor detected.
map.put(annotation.annotationType().getName(), annotation);
}
}
return map;
}
private static boolean detectValidationRequirement(Method method) {
Annotation[][] annotations = method.getParameterAnnotations();
for (Annotation[] array : annotations) {
for (Annotation annotation : array) {
if (isConstraint(annotation)) {
return true;
}
}
}
return false;
}
/**
* Determines whether the given annotation is a 'constraint' or not.
* It just checks if the annotation has the {@link Constraint} annotation on it or if the annotation is the {@link
* Valid} annotation.
*
* @param annotation the annotation to check
* @return {@code true} if the given annotation is a constraint
*/
private static boolean isConstraint(Annotation annotation) {
return annotation.annotationType().isAnnotationPresent(Constraint.class)
|| annotation.annotationType().equals(Valid.class);
}
@Override
public String getUrl() {
return route.getUrl();
}
@Override
public HttpMethod getHttpMethod() {
return route.getHttpMethod();
}
@Override
public Class<? extends Controller> getControllerClass() {
return route.getControllerClass();
}
@Override
public Method getControllerMethod() {
return route.getControllerMethod();
}
@Override
public boolean matches(HttpMethod method, String uri) {
return route.matches(method, uri);
}
@Override
public boolean matches(String method, String uri) {
return route.matches(method, uri);
}
@Override
public Map<String, String> getPathParametersEncoded(String uri) {
return route.getPathParametersEncoded(uri);
}
@Override
public int isCompliantWithRequestContentType(Request request) {
return route.isCompliantWithRequestContentType(request);
}
@Override
public Route accepting(String... types) {
return route.accepting(types);
}
@Override
public Route accepts(String... types) {
return route.accepts(types);
}
@Override
public Set<MediaType> getAcceptedMediaTypes() {
return route.getAcceptedMediaTypes();
}
@Override
public Set<MediaType> getProducedMediaTypes() {
return route.getProducedMediaTypes();
}
/**
* 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.
*/
@Override
public int getUnboundStatus() {
return route.getUnboundStatus();
}
@Override
public Route produces(String... types) {
return route.produces(types);
}
@Override
public Route producing(String... provide) {
return route.producing(provide);
}
@Override
public boolean isCompliantWithRequestAccept(Request request) {
return route.isCompliantWithRequestAccept(request);
}
@Override
public Controller getControllerObject() {
return route.getControllerObject();
}
@Override
public List<ActionParameter> getArguments() {
return route.getArguments();
}
@Override
public Result invoke() throws Exception {
Context context = Context.CONTEXT.get();
Preconditions.checkNotNull(context);
// Build chain if needed.
// We get an immutable copy of the set.
Set<Filter> filters = router.getFilters();
// Interceptors will be handled after filters.
List<Filter> chain = filters.stream()
.filter(filter -> !(filter instanceof Interceptor) && filter.uri().matcher(route.getUrl()).matches())
.collect(Collectors.toList());
Map<Interceptor<?>, Object> itcpConfiguration = new LinkedHashMap<>();
if (!interceptors.isEmpty()) {
for (Map.Entry<String, Object> entry : interceptors.entrySet()) {
final Interceptor<?> interceptor = getInterceptorForAnnotation(entry.getKey());
if (interceptor == null) {
return Results.badRequest("Missing interceptor handling " + entry.getKey());
}
itcpConfiguration.put(interceptor, entry.getValue());
chain.add(interceptor);
}
}
// Ready to call the action.
Filter endOfChain = new EndOfChainInvoker();
RequestContext ctx = new RequestContext(this, chain, itcpConfiguration, null, endOfChain);
return ctx.proceed();
}
private Interceptor<?> getInterceptorForAnnotation(String className) {
List<Interceptor<?>> localInterceptors = router.getInterceptors();
if (localInterceptors == null) {
return null;
}
for (Interceptor<?> interceptor : localInterceptors) {
if (interceptor.annotation().getName().equals(className)) {
return interceptor;
}
}
return null;
}
@Override
public String toString() {
return route.toString();
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
} else if (o instanceof Route) { //NOSONAR we want to check for Route too.
return route.equals(o);
} else {
return o.equals(this);
}
}
@Override
public int hashCode() {
return route.hashCode();
}
@Override
public boolean isUnbound() {
return route.isUnbound();
}
private class EndOfChainInvoker implements Filter {
/**
* We are the end of the chain, so we call the action method.
* If the route is unbound, there are no action method, a {@literal 404 - NOT FOUND} result is returned.
*
* @param route the intercepted route
* @param context the filter context
* @return the result of the action method, {@literal 404 - NOT FOUND} for unbound routes.
* @throws java.lang.reflect.InvocationTargetException if the action method throws an exception
* @throws java.lang.IllegalAccessException if the action method cannot be called
*/
@Override
public Result call(Route route, RequestContext context) throws InvocationTargetException, IllegalAccessException {
if (isUnbound()) {
return new Result().status(route.getUnboundStatus()).noContentIfNone();
} else {
// The interceptor and filter may have change some values, compute the parameters.
final List<ActionParameter> arguments = getArguments();
Object[] parameters = new Object[arguments.size()];
for (int i = 0; i < arguments.size(); i++) {
ActionParameter argument = arguments.get(i);
parameters[i] = Bindings.create(argument, context.context(),
router.getParameterConverterEngine());
}
// Validate if needed.
if (mustValidate) {
Validator validator = router.getValidator();
if (validator != null) {
Set<ConstraintViolation<Controller>> violations =
validator.forExecutables().validateParameters(getControllerObject(), getControllerMethod(),
parameters);
if (!violations.isEmpty()) {
return Results.badRequest(violations).json();
}
}
}
// Sets the parameters.
context.setParameters(parameters);
// Invoke the action method.
final Result result = (Result) getControllerMethod().invoke(
getControllerObject(), parameters);
// Manage the VARY header if the route has a 'consume' set:
if (! result.getHeaders().containsKey(HeaderNames.VARY)) {
String headers = null;
if (! getAcceptedMediaTypes().isEmpty()) {
headers = HeaderNames.CONTENT_TYPE;
}
if (! getProducedMediaTypes().isEmpty()) {
if (headers == null) {
headers = HeaderNames.ACCEPT;
} else {
headers += ", " + HeaderNames.ACCEPT;
}
}
if (headers != null) {
result.with(HeaderNames.VARY, headers);
}
}
// Manage produced types
final Set<MediaType> mediaTypes = route.getProducedMediaTypes();
if (mediaTypes.isEmpty() || result.getContentType() != null
|| result.getRenderable() != null && result.getRenderable().mimetype() != null) {
return result;
}
// check whether we can set the produced media type
if (mediaTypes.size() == 1) {
// Only one
result.as(mediaTypes.iterator().next().toString());
}
// Else we cannot do anything.
return result;
}
}
/**
* @return {@literal null} as it's meaningless here.
*/
@Override
public Pattern uri() {
// Not meaningful here.
return null;
}
/**
* @return {@literal -1} as it's meaningless here.
*/
@Override
public int priority() {
// Anyway, we're the last.
return -1;
}
}
}