/**
* Copyright 2013 Bayes Technologies
*
* 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.
*/
package me.bayes.vertx.vest;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Route;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import me.bayes.vertx.vest.binding.DefaultRouteBindingHolderFactory;
import me.bayes.vertx.vest.binding.RouteBindingHolder;
import me.bayes.vertx.vest.util.DefaultParameterResolver;
import me.bayes.vertx.vest.util.ParameterResolver;
import me.bayes.vertx.vest.util.UriPathUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
/**
* <pre>
* The {@link DefaultRouterBuilder} is a basic {@link RouterBuilder} that
* uses the {@link javax.ws.rs.core.Application} to get classes that are candidates for adding to
* the {@link Router}.
*
* This simple logic searches the given classes for the {@link javax.ws.rs.Path} annotation
* and uses that with the combination of the {@link javax.ws.rs.Path} anotation on method level
* to create the route that the method will handle. Once the path is established
* a {@link Handler} is added to the route matcher for the specific path. The method
* associated with the {@link Handler} is derived from either one of the {@link javax.ws.rs.GET},
* {@link javax.ws.rs.POST}, {@link javax.ws.rs.PUT}, {@link javax.ws.rs.DELETE}, {@link javax.ws.rs.OPTIONS} xor {@link javax.ws.rs.HEAD} annotations
* on the method.
*
* Spec: 3.3.5 HEAD and OPTIONS is only supported for explicit request
*
* TODO: {@link javax.ws.rs.core.Context} annotation support.
* TODO: {@link javax.ws.rs.Consumes}
* TODO: {@link javax.ws.rs.Produces}
* TODO: Add verticle reference injection through {@link javax.ws.rs.core.Context} annotation.
* TODO: Add default behaviour for HEAD and OPTIONS according to 3.3.5.
* TODO: Handle exceptions better. 3.3.4 Exceptions
* TODO: 3.8 Determining the MediaType of Responses
* TODO: Allow multiple endpoint implementation to have the same uri but different consumes and produces parameters.
* </pre>
*
* @author Kevin Bayes
* @since 1.0
* @version 1.0
*
*/
public class DefaultRouterBuilder extends AbstractRouterBuilder {
private static final Logger LOG = LoggerFactory.getLogger(DefaultRouterBuilder.class);
private final ParameterResolver parameterResolver;
/**
* Requires a {@link VestApplication}.
* @param application
*/
public DefaultRouterBuilder(final VestApplication application) {
super(application, new DefaultRouteBindingHolderFactory(application));
this.parameterResolver = new DefaultParameterResolver(application);
}
/*
* (non-Javadoc)
* @see me.bayes.vertx.extension.RouteMatcherBuilder#build(me.bayes.vertx.extension.BuilderContext)
*/
protected Router buildInternal() throws Exception {
addRoutes(router);
return router;
}
/**
* The simplest case adds routes and delegates the execution to the method annotated with the {@link javax.ws.rs.Path} annotation.
*
* @param router
* @throws Exception
*/
private void addRoutes(final Router router) throws Exception {
router.route().handler(BodyHandler.create());
this.bindingHolder.foreach(
(method, key, bindings) -> {
//3.7 Matching Requests to Resource Methods is delegated to vertx route matcher
final String finalPath = UriPathUtil.convertPath(key);
Route route = router.route(HttpMethod.valueOf(method), finalPath);
route
.handler(event -> {
final HttpServerRequest request = event.request();
final HttpServerResponse response = event.response();
try {
RouteBindingHolder.MethodBinding binding = null;
String acceptsHeader = request.headers().get(HttpHeaders.ACCEPT);
String contentTypeHeader = request.headers().get(HttpHeaders.CONTENT_TYPE);
if (!request.headers().isEmpty()) {
for (RouteBindingHolder.MethodBinding binding_ : bindings) {
if (binding_.hasConsumes(contentTypeHeader) && binding_.hasProduces(acceptsHeader)) {
binding = binding_;
break;
}
if (binding_.hasConsumes(contentTypeHeader) && acceptsHeader == null) {
binding = binding_;
break;
}
if (contentTypeHeader == null && binding_.hasProduces(acceptsHeader)) {
binding = binding_;
break;
}
}
}
if (binding == null && bindings.size() > 0) {
binding = bindings.get(0);
} else if (binding == null) {
throw new Exception("No route that supports accepts given HTTP parameters");
}
Method bindingMethod = binding.getMethod();
final Class<?>[] parameterTypes = bindingMethod.getParameterTypes();
if (parameterTypes.length == 0 || !parameterTypes[0].equals(HttpServerRequest.class)) {
LOG.warn("Classes marked with a HttpMethod must have at least one parameter. The first parameter should be HttpServerRequest.");
return;
}
final Annotation[][] parameterAnnotations = bindingMethod.getParameterAnnotations();
final String[] produces = binding.getProduces();
final String[] consumes = binding.getConsumes();
String producesMediaType = null;
if (acceptsHeader != null) {
if (produces != null && produces.length > 0) {
for (String type : produces) {
if (acceptsHeader.contains(type)) {
producesMediaType = type;
break;
}
}
}
}
if (producesMediaType == null) {
producesMediaType =
(produces != null && produces.length == 1) ?
produces[0] : MediaType.TEXT_PLAIN;
}
if (contentTypeHeader != null) {
if (consumes != null && consumes.length > 0) {
for (String type : consumes) {
if (acceptsHeader.contains(type)) {
contentTypeHeader = type;
break;
}
}
}
}
request.response().headers().set(HttpHeaders.CONTENT_TYPE, producesMediaType);
final Object[] parameters = new Object[parameterTypes.length];
parameters[0] = request;
boolean isBodyResolved = false;
int objectParameterIndex = -1;
if (parameters.length > 1) {
for (int i = 1; i < parameters.length; i++) {
if (parameterAnnotations[i].length > 0 ||
RoutingContext.class.equals(parameterTypes[i]) ||
HttpServerResponse.class.equals(parameterTypes[i])) {
parameters[i] = parameterResolver.resolve(bindingMethod, parameterTypes[i], parameterAnnotations[i], event);
} else if (JsonObject.class.equals(parameterTypes[i])) {
objectParameterIndex = i;
isBodyResolved = true;
}
}
}
if (isBodyResolved) {
parameters[objectParameterIndex] = event.getBodyAsJson();
bindingMethod.invoke(binding.getDelegate(), parameters);
} else {
bindingMethod.invoke(binding.getDelegate(), parameters);
}
} catch (Exception e) {
e.printStackTrace();
LOG.error("Exception occurred.", e);
if (exceptionHandler != null) {
exceptionHandler.handle(request);
} else {
request.response().headers().set(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN);
request.response().setStatusCode(500);
request.response().setStatusMessage("Internal server error");
request.response().end();
}
}
});
});
}
}