/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.jooby.internal.mvc; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import org.jooby.Env; import org.jooby.MediaType; import org.jooby.Route; import org.jooby.Route.Definition; import org.jooby.internal.RouteMetadata; import org.jooby.mvc.CONNECT; import org.jooby.mvc.Consumes; import org.jooby.mvc.DELETE; import org.jooby.mvc.GET; import org.jooby.mvc.HEAD; import org.jooby.mvc.OPTIONS; import org.jooby.mvc.PATCH; import org.jooby.mvc.POST; import org.jooby.mvc.PUT; import org.jooby.mvc.Path; import org.jooby.mvc.Produces; import org.jooby.mvc.TRACE; import com.google.common.base.CaseFormat; import com.google.common.collect.ImmutableSet; import javaslang.control.Try; public class MvcRoutes { private static final String[] EMPTY = new String[0]; @SuppressWarnings("unchecked") private static final Set<Class<? extends Annotation>> VERBS = ImmutableSet.of(GET.class, POST.class, PUT.class, DELETE.class, PATCH.class, HEAD.class, OPTIONS.class, TRACE.class, CONNECT.class); private static final Set<Class<? extends Annotation>> IGNORE = ImmutableSet .<Class<? extends Annotation>> builder() .addAll(VERBS) .add(Path.class) .add(Produces.class) .add(Consumes.class) .build(); public static List<Route.Definition> routes(final Env env, final RouteMetadata classInfo, final String rpath, final Class<?> routeClass) { // check and fail fast methods(routeClass, methods -> { routes(methods, (m, a) -> { if (!Modifier.isPublic(m.getModifiers())) { throw new IllegalArgumentException("Not a public method: " + m); } }); }); RequestParamProvider provider = new RequestParamProviderImpl( new RequestParamNameProviderImpl(classInfo)); String[] rootPaths = path(routeClass); String[] rootExcludes = excludes(routeClass, EMPTY); // we are good, now collect them Map<Method, List<Class<?>>> methods = new HashMap<>(); routes(routeClass.getMethods(), methods::put); List<Definition> definitions = new ArrayList<>(); Map<String, Object> attrs = attrs(routeClass.getAnnotations()); methods .keySet() .stream() .sorted((m1, m2) -> { int l1 = classInfo.startAt(m1); int l2 = classInfo.startAt(m2); return l1 - l2; }) .forEach(method -> { /** * Param provider: dev vs none dev */ RequestParamProvider paramProvider = provider; if (!env.name().equals("dev")) { List<RequestParam> params = provider.parameters(method); paramProvider = (h) -> params; } List<Class<?>> verbs = methods.get(method); List<MediaType> produces = produces(method); List<MediaType> consumes = consumes(method); Map<String, Object> localAttrs = new HashMap<>(attrs); localAttrs.putAll(attrs(method.getAnnotations())); for (String path : expandPaths(rootPaths, method)) { for (Class<?> verb : verbs) { String name = routeClass.getSimpleName() + "." + method.getName(); String[] excludes = excludes(method, rootExcludes); Definition definition = new Route.Definition( verb.getSimpleName(), rpath + "/" + path, new MvcHandler(method, paramProvider)) .produces(produces) .consumes(consumes) .excludes(excludes) .declaringClass(method.getDeclaringClass().getName()) .line(classInfo.startAt(method) - 1) .name(name); localAttrs.forEach((n, v) -> definition.attr(n, v)); definitions.add(definition); } } }); return definitions; } private static void methods(final Class<?> clazz, final Consumer<Method[]> callback) { if (clazz != Object.class) { callback.accept(clazz.getDeclaredMethods()); methods(clazz.getSuperclass(), callback); } } @SuppressWarnings({"rawtypes", "unchecked" }) private static void routes(final Method[] methods, final BiConsumer<Method, List<Class<?>>> consumer) { for (Method method : methods) { List<Class<?>> annotations = new ArrayList<>(); for (Class annotationType : VERBS) { Annotation annotation = method.getAnnotation(annotationType); if (annotation != null) { annotations.add(annotationType); } } if (annotations.size() > 0) { consumer.accept(method, annotations); } else if (method.isAnnotationPresent(Path.class)) { consumer.accept(method, Arrays.asList(GET.class)); } } } private static Map<String, Object> attrs(final Annotation[] annotations) { Map<String, Object> result = new LinkedHashMap<>(); for (Annotation annotation : annotations) { result.putAll(attrs(annotation)); } return result; } private static Map<String, Object> attrs(final Annotation annotation) { Map<String, Object> result = new LinkedHashMap<>(); Class<? extends Annotation> annotationType = annotation.annotationType(); if (!IGNORE.contains(annotationType)) { Method[] attrs = annotation.annotationType().getDeclaredMethods(); for (Method attr : attrs) { Try.of(() -> attr.invoke(annotation)) .onSuccess(value -> result.put(attrName(annotation, attr), value)); } } return result; } private static String attrName(final Annotation annotation, final Method attr) { String name = attr.getName(); if (name.equals("value")) { return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, annotation.annotationType().getSimpleName()); } return name; } private static List<MediaType> produces(final Method method) { Function<AnnotatedElement, Optional<List<MediaType>>> fn = (element) -> { Produces produces = element.getAnnotation(Produces.class); if (produces != null) { return Optional.of(MediaType.valueOf(produces.value())); } return Optional.empty(); }; // method level return fn.apply(method) // class level .orElseGet(() -> fn.apply(method.getDeclaringClass()) // none .orElse(MediaType.ALL)); } private static List<MediaType> consumes(final Method method) { Function<AnnotatedElement, Optional<List<MediaType>>> fn = (element) -> { Consumes consumes = element.getAnnotation(Consumes.class); if (consumes != null) { return Optional.of(MediaType.valueOf(consumes.value())); } return Optional.empty(); }; // method level return fn.apply(method) // class level .orElseGet(() -> fn.apply(method.getDeclaringClass()) // none .orElse(MediaType.ALL)); } private static String[] path(final AnnotatedElement owner) { Path annotation = owner.getAnnotation(Path.class); if (annotation == null) { return EMPTY; } return annotation.value(); } private static String[] excludes(final AnnotatedElement owner, final String[] parent) { Path annotation = owner.getAnnotation(Path.class); if (annotation == null) { return parent; } String[] excludes = annotation.excludes(); if (excludes.length == 0) { return parent; } if (parent.length == 0) { return excludes; } // join everything int size = parent.length + excludes.length; String[] result = new String[size]; System.arraycopy(parent, 0, result, 0, parent.length); System.arraycopy(excludes, 0, result, parent.length, excludes.length); return result; } private static String[] expandPaths(final String[] root, final Method m) { String[] path = path(m); if (root.length == 0) { if (path.length == 0) { throw new IllegalArgumentException("No path(s) found for: " + m); } return path; } if (path.length == 0) { return root; } String[] result = new String[root.length * path.length]; int k = 0; for (String base : root) { for (String element : path) { result[k] = base + "/" + element; k += 1; } } return result; } }