com.vmware.xenon.common.RequestRouter Maven / Gradle / Ivy
/*
* Copyright (c) 2014-2015 VMware, Inc. All Rights Reserved.
*
* 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 com.vmware.xenon.common;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import com.vmware.xenon.common.Service.Action;
/**
* A RequestRouter routes operations to handlers by evaluating an incoming operation against a list of matchers, and invoking the handler of the first match.
*
* The primary benefit of using a RequestRouter to map incoming requests to handlers is that the API is being modeled in a way that can later be parsed, for example
* API documentation can be auto-generated. In addition, it encourages separating logic of each logical operation into its own method/code block.
*/
public class RequestRouter implements Predicate {
public static class Parameter {
public String name;
public String description;
public String type;
public boolean required;
public String value;
public ParamDef paramDef;
public Parameter(String name, String description, String type, boolean required,
String value, ParamDef paramDef) {
this.name = name;
this.description = description;
this.type = type;
this.required = required;
this.value = value;
this.paramDef = paramDef;
}
}
// Defines where the parameter appears in the given request.
public enum ParamDef {
/** Used to document the query parameters understood by this route */
QUERY("query"),
/** Used to specify the body type used for this route */
BODY("body"),
/** Used to document the media types accepted by this route */
CONSUMES("consumes"),
/** Used to document the media types this route can produce */
PRODUCES("produces"),
/** Used to document the possible response codes from this route */
RESPONSE("response");
String value;
ParamDef(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
}
public static class Route {
/**
* Support level of this route, for documentation purposes.
* Note that this does not imply the support level of
* the Service itself - if a Service is visible to the client then it is considered
* to be supported, but a Service or Routes may be hidden from the client through access controls.
*/
public enum SupportLevel {
/** Route is not supported and requests will fail */
NOT_SUPPORTED,
/** Route is for internal use only */
INTERNAL,
/** Route is exposed to the public but is deprecated and alternatives should be used */
DEPRECATED,
/** Route is public */
PUBLIC
}
public Action action;
public Predicate matcher;
public Consumer handler;
public String description;
public Class> requestType;
public Class> responseType;
public List parameters;
public SupportLevel supportLevel;
public Route(Action action, Predicate matcher, Consumer handler,
String description) {
this.action = action;
this.matcher = matcher;
this.handler = handler;
this.description = description;
}
public Route() {
}
/**
* Documentation annotations for Route handler methods (doGet, doPost, ...)
* This annotation is placed on handler methods to document the
* behavior of the http verb.
*
* Note that the 'description' fields can either directly contain a description,
* or can be used as a key to look up a more complete description in an HTML
* resource file with the same path as the java class, but with the
* file terminating in '.html'.
*
* The format of the HTML file is that each key must appear on a line on its own
* surrounded by >h1< and >/h1< tags, and the lines between that key
* and the next key will be inserted as the description.
*
* Example:
*
* {@code
*
* @Documentation(description = "@CAR",
* queryParams = {
* @QueryParam(description = "@TEAPOT",
* example = "false", name = "teapot", required = false, type = "boolean")
* },
* consumes = { "application/json", "app/json" },
* produces = { "application/json", "app/json" },
* responses = {
* @ApiResponse(statusCode = 200, description = "OK"),
* @ApiResponse(statusCode = 404, description = "Not Found"),
* @ApiResponse(statusCode = 418, description = "I'm a teapot!")
* })
* @Override
* public void handlePut(Operation put) {
* ...
*
* Car.html:
* @TEAPOT
*
* Test param - if true then do not modify state, and return http status
* \"I'm a teapot\"
* @CAR
*
* Description of a car
*
* }
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface RouteDocumentation {
/** defines API support level - default is APIs are public unless stated otherwise */
SupportLevel supportLevel() default SupportLevel.PUBLIC;
String description() default "";
/** defines HTTP status statusCode responses */
public ApiResponse[] responses() default {};
/** defines optional query parameters */
public QueryParam[] queryParams() default {};
/** List of supported media types, defaults to application/json */
public String[] consumes() default {};
/** List of supported media types, defaults to application/json */
public String[] produces() default {};
/**
* Documentation of HTTP response codes for Route handler methods.
* This annotation is used as an embedded annotation inside the @Documentation
* annotation.
*/
@Target(value = {ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface ApiResponse {
public int statusCode();
public String description();
public Class> response() default Void.class;
}
/**
* Documentation of query parameter support for Route handler methods.
* This annotation is used as an embedded annotation inside the @Documentation
* annotation.
*/
@Target(value = {ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface QueryParam {
public String name();
public String description() default "";
public String example() default "";
public String type() default "string";
public boolean required() default false;
}
}
}
public static class RequestDefaultMatcher implements Predicate {
@Override
public boolean test(Operation op) {
return true;
}
@Override
public String toString() {
return "#";
}
}
public static class RequestUriMatcher implements Predicate {
private Pattern pattern;
public RequestUriMatcher(String regexp) {
this.pattern = Pattern.compile(regexp);
}
@Override
public boolean test(Operation op) {
return op.getUri() != null && op.getUri().getQuery() != null
&& this.pattern.matcher(op.getUri().getQuery()).matches();
}
@Override
public String toString() {
return String.format("?%s", this.pattern.pattern());
}
}
public static class RequestBodyMatcher implements Predicate {
private final Class typeParameterClass;
private final Object fieldValue;
private final Field field;
public RequestBodyMatcher(Class typeParameterClass, String fieldName, Object fieldValue) {
this.typeParameterClass = typeParameterClass;
this.field = ReflectionUtils.getField(typeParameterClass, fieldName);
this.fieldValue = fieldValue;
}
@Override
public boolean test(Operation op) {
if (this.field == null) {
return false;
}
try {
T body = op.getBody(this.typeParameterClass);
return body != null && Objects.equals(this.field.get(body), this.fieldValue);
} catch (IllegalAccessException ex) {
return false;
}
}
@Override
public String toString() {
return String.format("%s#%s=%s", this.typeParameterClass.getName(),
this.field != null ? this.field.getName() : "<>", this.fieldValue);
}
}
private Map> routes;
public RequestRouter() {
this.routes = new LinkedHashMap<>();
}
public void register(Route route) {
Action action = route.action;
List actionRoutes = this.routes.get(action);
if (actionRoutes == null) {
actionRoutes = new ArrayList<>();
}
actionRoutes.add(route);
this.routes.put(action, actionRoutes);
}
public void register(Action action, Predicate matcher, Consumer handler,
String description) {
List actionRoutes = this.routes.get(action);
if (actionRoutes == null) {
actionRoutes = new ArrayList();
}
actionRoutes.add(new Route(action, matcher, handler, description));
this.routes.put(action, actionRoutes);
}
public boolean test(Operation op) {
List actionRoutes = this.routes.get(op.getAction());
if (actionRoutes != null) {
for (Route route : actionRoutes) {
if (route.matcher.test(op)) {
route.handler.accept(op);
return false;
}
}
}
// no match found - processing of the request should continue
return true;
}
public Map> getRoutes() {
return this.routes;
}
public static RequestRouter findRequestRouter(OperationProcessingChain opProcessingChain) {
if (opProcessingChain == null) {
return null;
}
List> filters = opProcessingChain.getFilters();
if (filters.isEmpty()) {
return null;
}
// we are assuming as convention that if a RequestRouter exists in the chain, it is
// the last element as it invokes the service handler by itself and then drops the request
Predicate lastElement = filters.get(filters.size() - 1);
if (lastElement instanceof RequestRouter) {
return (RequestRouter) lastElement;
}
return null;
}
}