package com.zandero.rest.data; import com.zandero.rest.AnnotationProcessor; import com.zandero.rest.annotation.*; import com.zandero.rest.exception.ExceptionHandler; import com.zandero.rest.reader.ValueReader; import com.zandero.rest.writer.HttpResponseWriter; import com.zandero.utils.ArrayUtils; import com.zandero.utils.Assert; import com.zandero.utils.StringUtils; import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.RoutingContext; import javax.annotation.security.DenyAll; import javax.annotation.security.PermitAll; import javax.annotation.security.RolesAllowed; import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.*; /** * Holds definition of a route as defined with annotations */ public class RouteDefinition { private final String DELIMITER = "/"; /** * Original path as given in annotation */ private String path = DELIMITER; /** * Converted path (the route), in case of regular expression paths * otherwise null */ private String routePath = null; private MediaType[] consumes = null; private MediaType[] produces = null; private io.vertx.core.http.HttpMethod method; private Class<? extends HttpResponseWriter> writer; private Class<? extends ValueReader> reader; private Class<? extends ExceptionHandler>[] exceptionHandlers; /** * Parameters extracted from request path, query, headers, cookies ... */ private Map<String, MethodParameter> params = new HashMap<>(); /** * Route order lower is earlier or 0 for default */ private int order; /** * vert.x blocking request */ private boolean blocking = false; // security private Boolean permitAll = null; // true - permit all, false - deny all, null - check roles private String[] roles = null; public RouteDefinition(Class clazz) { Class annotatedClass = AnnotationProcessor.getClassWithAnnotation(clazz, Path.class); if (annotatedClass == null) { annotatedClass = clazz; } init(annotatedClass.getAnnotations()); } public RouteDefinition(RouteDefinition base, Annotation[] annotations) { // copy base route path(base.getPath()); consumes = base.getConsumes(); produces = base.getProduces(); method = base.getMethod(); reader = base.getReader(); writer = base.getWriter(); // set root privileges permitAll = base.getPermitAll(); roles = base.roles; if (roles != null) { permitAll = null; } exceptionHandlers = base.getExceptionHandlers(); // complement / override with additional annotations init(annotations); } public RouteDefinition(RoutingContext context) { Assert.notNull(context, "Missing context!"); path(context.request().path()); method = context.request().method(); // take Accept and set as produces ... MediaType accept = MediaTypeHelper.valueOf(context.getAcceptableContentType()); if (accept == null) { accept = MediaTypeHelper.valueOf(context.request().getHeader("Accept")); } if (accept != null) { produces = new MediaType[]{accept}; } } /** * Sets path specifics * * @param annotations list of method annotations */ private void init(Annotation[] annotations) { for (Annotation annotation : annotations) { //log.info(annotation.toString()); if (annotation instanceof RouteOrder) { order(((RouteOrder) annotation).value()); } if (annotation instanceof Path) { path(((Path) annotation).value()); } if (annotation instanceof Produces) { produces(((Produces) annotation).value()); } if (annotation instanceof Consumes) { consumes(((Consumes) annotation).value()); } if (annotation instanceof javax.ws.rs.HttpMethod) { method(((javax.ws.rs.HttpMethod) annotation).value()); } if (annotation instanceof GET || annotation instanceof POST || annotation instanceof PUT || annotation instanceof DELETE || annotation instanceof HEAD || annotation instanceof OPTIONS) { method(annotation.annotationType().getSimpleName()); } // response writer ... if (annotation instanceof ResponseWriter) { writer = ((ResponseWriter) annotation).value(); } if (annotation instanceof RequestReader) { reader = ((RequestReader) annotation).value(); } if (annotation instanceof Blocking) { blocking = ((Blocking) annotation).value(); } if (annotation instanceof RolesAllowed) { permitAll = null; // override any previous definition roles = ((RolesAllowed) annotation).value(); } if (annotation instanceof DenyAll) { roles = null; // override any previous definition permitAll = false; } if (annotation instanceof PermitAll) { roles = null; // override any previous definition permitAll = true; } if (annotation instanceof CatchWith) { exceptionHandlers = ArrayUtils.join(((CatchWith) annotation).value(), exceptionHandlers); } } } private RouteDefinition order(int value) { order = value; return this; } public RouteDefinition path(String subPath) { Assert.notNullOrEmptyTrimmed(subPath, "Missing or empty route '@Path'!"); // clean up path so all paths end with "/" if (subPath.length() == 1 && DELIMITER.equals(subPath)) { return this; } if (!subPath.startsWith(DELIMITER)) { subPath = DELIMITER + subPath; // add leading "/" } if (subPath.endsWith(DELIMITER)) { subPath = subPath.substring(0, subPath.length() - 1); // loose trailing "/" } if (DELIMITER.equals(path)) { // default path = subPath; } else { path = path + subPath; } // extract parameters from path if any ( List<MethodParameter> params = PathConverter.extract(path); params(params); // read path to Vert.X format routePath = PathConverter.convert(path); return this; } public RouteDefinition consumes(String[] value) { Assert.notNullOrEmpty(value, "Missing '@Consumes' definition!"); consumes = getMediaTypes(value); return this; } public RouteDefinition produces(String[] value) { Assert.notNullOrEmpty(value, "Missing '@Produces' definition!"); produces = getMediaTypes(value); return this; } private MediaType[] getMediaTypes(String[] value) { List<MediaType> types = new ArrayList<>(); for (String item : value) { MediaType type = MediaTypeHelper.valueOf(item); if (type != null) { types.add(type); } } if (types.size() == 0) { return null; } return types.toArray(new MediaType[]{}); } private RouteDefinition method(String value) { for (HttpMethod item : HttpMethod.values()) { if (StringUtils.equals(value, item.name(), true)) { Assert.isNull(method, "Method already set to: " + method + "!"); method = item; break; } } return this; } private RouteDefinition params(List<MethodParameter> pathParams) { if (pathParams == null || pathParams.size() == 0) { return this; } params.clear(); // check if param is already present for (MethodParameter parameter : pathParams) { if (params.get(parameter.getName()) != null) { throw new IllegalArgumentException("Duplicate parameter name given: " + parameter.getName() + "! "); } params.put(parameter.getName(), parameter); } return this; } /** * Extracts method arguments and links them with annotated route parameters * * @param method to extract argument types and annotations from */ public void setArguments(Method method) { Parameter[] parameters = method.getParameters(); Class<?>[] parameterTypes = method.getParameterTypes(); Annotation[][] annotations = method.getParameterAnnotations(); int index = 0; Map<String, MethodParameter> arguments = new HashMap<>(); for (Annotation[] ann : annotations) { String name = null; ParameterType type = null; String defaultValue = null; Class<? extends ValueReader> valueReader = null; for (Annotation annotation : ann) { if (annotation instanceof PathParam) { // find path param ... and set index ... name = ((PathParam) annotation).value(); type = ParameterType.path; } if (annotation instanceof QueryParam) { // add param name = ((QueryParam) annotation).value(); type = ParameterType.query; } if (annotation instanceof FormParam) { type = ParameterType.form; name = ((FormParam) annotation).value(); } if (annotation instanceof CookieParam) { type = ParameterType.cookie; name = ((CookieParam) annotation).value(); } if (annotation instanceof HeaderParam) { type = ParameterType.header; name = ((HeaderParam) annotation).value(); } if (annotation instanceof MatrixParam) { type = ParameterType.matrix; name = ((MatrixParam) annotation).value(); } if (annotation instanceof DefaultValue) { defaultValue = ((DefaultValue) annotation).value(); } if (annotation instanceof RequestReader) { valueReader = ((RequestReader) annotation).value(); } if (annotation instanceof Context) { type = ParameterType.context; name = parameters[index].getName(); } } // if no name provided than parameter is considered the request body if (name == null) { // try to find out what parameter type it is ... POST, PUT have a body ... // regEx path might not have a name ... MethodParameter param = findParameter(index); if (param != null) { Assert.isNull(param.getDataType(), "Duplicate argument type given: " + parameters[index].getName()); param.argument(parameterTypes[index], index); // set missing argument type and index } else { if (valueReader == null) { valueReader = reader; // take reader from method / class definition } else { reader = valueReader; // set body reader from field } Assert.isTrue(requestHasBody(), "Missing argument annotation (@PathParam, @QueryParam, @FormParam, @HeaderParam, @CookieParam, @Context) for: " + parameterTypes[index].getName() + " " + parameters[index].getName()); name = parameters[index].getName(); type = ParameterType.body; } } // collect only needed params for this method if (name != null) { MethodParameter parameter = provideArgument(name, type, defaultValue, parameterTypes[index], valueReader, index); arguments.put(name, parameter); } index++; } setUsedArguments(arguments); } private void setUsedArguments(Map<String, MethodParameter> arguments) { for (String key : params.keySet()) { if (arguments.containsKey(key)) { params.put(key, arguments.get(key)); // override existing } } for (String key : arguments.keySet()) { if (!params.containsKey(key)) { params.put(key, arguments.get(key)); // add missing } } } public MethodParameter findParameter(int index) { if (params == null) { return null; } for (MethodParameter parameter : params.values()) { if (parameter.getIndex() == index) { return parameter; } } // try reg ex index if (pathIsRegEx()) { for (MethodParameter parameter : params.values()) { if (parameter.getRegExIndex() == index) { return parameter; } } } return null; } private MethodParameter provideArgument(String name, ParameterType type, String defaultValue, Class<?> parameterType, Class<? extends ValueReader> valueReader, int index) { Assert.notNull(type, "Argument: " + name + " (" + parameterType + ") can't be provided with Vert.x request, check and annotate method arguments!"); switch (type) { case path: MethodParameter found = params.get(name); // parameter should exist Assert.notNull(found, "Missing @PathParam: " + name + "(" + parameterType + ") as method argument!"); found.argument(parameterType, index); found.setDefaultValue(defaultValue); return found; default: MethodParameter existing = params.get(name); Assert.isNull(existing, "Duplicate argument: " + name + ", already provided!"); // param should not exist! MethodParameter newParam = new MethodParameter(type, name, parameterType, index); newParam.setDefaultValue(defaultValue); newParam.setValueReader(valueReader); return newParam; } } public String getPath() { return path; } public String getRoutePath() { if (pathIsRegEx()) { return regExPathEscape(routePath); } return routePath; } private String regExPathEscape(String path) { if (path == null) { return null; } return path.replaceAll("/", "\\\\/"); } public MediaType[] getConsumes() { return consumes; } public MediaType[] getProduces() { return produces; } public HttpMethod getMethod() { return method; } public int getOrder() { return order; } public Class<? extends HttpResponseWriter> getWriter() { return writer; } public Class<? extends ValueReader> getReader() { return reader; } public Class<? extends ExceptionHandler>[] getExceptionHandlers() { // join failure writers with global ... and return return exceptionHandlers; } public List<MethodParameter> getParameters() { if (params == null) { return Collections.emptyList(); } ArrayList<MethodParameter> list = new ArrayList<>(params.values()); list.sort(Comparator.comparing(MethodParameter::getIndex)); // sort parameters by index ... return list; } public boolean requestHasBody() { return !(HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method)); } public boolean hasBodyParameter() { return getBodyParameter() != null; } public MethodParameter getBodyParameter() { if (params == null) { return null; } return params.values().stream().filter(param -> ParameterType.body.equals(param.getType())).findFirst().orElse(null); } public boolean hasCookies() { if (params == null) { return false; } return params.values().stream().anyMatch(param -> ParameterType.cookie.equals(param.getType())); } public boolean hasMatrixParams() { if (params == null) { return false; } return params.values().stream().anyMatch(param -> ParameterType.matrix.equals(param.getType())); } public boolean pathIsRegEx() { if (params == null) { return false; } for (MethodParameter param : params.values()) { if (param.isRegEx()) { return true; } } return false; } public boolean isBlocking() { return blocking; } /** * @return true - permit all, false - deny all, null - check roles */ public Boolean getPermitAll() { return permitAll; } /** * @return null - no roles defined, or array of allowed roles */ public String[] getRoles() { return roles; } /** * @return true to check if User is in given role, false otherwise */ public boolean checkSecurity() { return permitAll != null || (roles != null && roles.length > 0); } @Override public String toString() { String prefix = " "; // to improve formatting ... prefix = prefix.substring(0, prefix.length() - method.toString().length()); String security = ""; if (checkSecurity()) { if (permitAll != null) { security = permitAll ? " @PermitAll" : " @DenyAll"; } else { security = " [" + StringUtils.join(roles, ", ") + "]"; } } return prefix + method + " " + routePath + security; } }