/** * Copyright (C) 2012-2017 the original author or authors. * * 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 ninja; import com.google.common.collect.Lists; import com.google.inject.Inject; import java.lang.reflect.Method; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.Set; import ninja.params.ControllerMethodInvoker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Injector; import com.google.inject.Provider; import com.google.inject.util.Providers; import java.util.List; import java.util.Optional; import java.util.logging.Level; import ninja.ControllerMethods.ControllerMethod; import ninja.utils.LambdaRoute; import ninja.utils.MethodReference; import ninja.utils.NinjaBaseDirectoryResolver; import ninja.utils.NinjaProperties; import ninja.utils.SwissKnife; import ninja.application.ApplicationFilters; public class RouteBuilderImpl implements RouteBuilder { private static final Logger log = LoggerFactory.getLogger(RouteBuilder.class); protected final static String GLOBAL_FILTERS_DEFAULT_LOCATION = "conf.Filters"; private String httpMethod; private String uri; private Method functionalMethod; private Optional<Method> implementationMethod; // method to use for parameter/annotation extraction private Optional<Object> targetObject; // instance to invoke private final NinjaProperties ninjaProperties; private Optional<List<Class<? extends Filter>>> globalFiltersOptional; private final List<Class<? extends Filter>> localFilters; private final NinjaBaseDirectoryResolver ninjaBaseDirectoryResolver; @Inject public RouteBuilderImpl( NinjaProperties ninjaProperties, NinjaBaseDirectoryResolver ninjaBaseDirectoryResolver) { this.implementationMethod = Optional.empty(); this.targetObject = Optional.empty(); this.ninjaProperties = ninjaProperties; this.ninjaBaseDirectoryResolver = ninjaBaseDirectoryResolver; this.globalFiltersOptional = Optional.empty(); this.localFilters = Lists.newArrayList(); } public RouteBuilderImpl GET() { httpMethod = "GET"; return this; } public RouteBuilderImpl POST() { httpMethod = "POST"; return this; } public RouteBuilderImpl PUT() { httpMethod = "PUT"; return this; } public RouteBuilderImpl DELETE() { httpMethod = "DELETE"; return this; } public RouteBuilderImpl OPTIONS() { httpMethod = "OPTIONS"; return this; } public RouteBuilderImpl HEAD() { httpMethod = "HEAD"; return this; } public RouteBuilderImpl METHOD(String method) { httpMethod = method; return this; } @Override public void with(Class controllerClass, String controllerMethod) { this.functionalMethod = verifyControllerMethod(controllerClass, controllerMethod); } @Override @Deprecated public void with(MethodReference methodRef) { with(methodRef.getDeclaringClass(), methodRef.getMethodName()); } @Override @Deprecated public void with(final Result result) { with(ControllerMethods.of(() -> result)); } @Override public Void with(ControllerMethod controllerMethod) { LambdaRoute lambdaRoute = LambdaRoute.resolve(controllerMethod); this.functionalMethod = lambdaRoute.getFunctionalMethod(); this.implementationMethod = lambdaRoute.getImplementationMethod(); this.targetObject = lambdaRoute.getTargetObject(); return null; } @Override public RouteBuilder globalFilters(List<Class<? extends Filter>> filtersToAdd) { this.globalFiltersOptional = Optional.of(filtersToAdd); return this; } @Override public RouteBuilder globalFilters(Class<? extends Filter> ... filtersToAdd) { List<Class<? extends Filter>> globalFiltersTemp = Lists.newArrayList(filtersToAdd); globalFilters(globalFiltersTemp); return this; } @Override public RouteBuilder filters(List<Class<? extends Filter>> filtersToAdd) { this.localFilters.addAll(filtersToAdd); return this; } @Override public RouteBuilder filters(Class<? extends Filter> ... filtersToAdd) { List<Class<? extends Filter>> filtersTemp = Lists.newArrayList(filtersToAdd); filters(filtersTemp); return this; } @Override public RouteBuilder route(String uri) { this.uri = uri; return this; } /** * Routes are usually defined in conf/Routes.java as * router.GET().route("/teapot").with(FilterController.class, "teapot"); * * Unfortunately "teapot" is not checked by the compiler. We do that here at * runtime. * * We are reloading when there are changes. So this is almost as good as * compile time checking. * * @param controller * The controller class * @param controllerMethod * The method * @return The actual method */ private Method verifyControllerMethod(Class<?> controllerClass, String controllerMethod) { try { Method methodFromQueryingClass = null; // 1. Make sure method is in class // 2. Make sure only one method is there. Otherwise we cannot really // know what to do with the parameters. for (Method method : controllerClass.getMethods()) { if (method.getName().equals(controllerMethod)) { if (methodFromQueryingClass == null) { methodFromQueryingClass = method; } else { throw new NoSuchMethodException(); } } } if (methodFromQueryingClass == null) { throw new NoSuchMethodException(); } // make sure that the return type of that controller method // is of type Result. if (Result.class.isAssignableFrom(methodFromQueryingClass.getReturnType())) { return methodFromQueryingClass; } else { throw new NoSuchMethodException(); } } catch (SecurityException e) { log.error( "Error while checking for valid Controller / controllerMethod combination", e); } catch (NoSuchMethodException e) { log.error("Error in route configuration!!!"); log.error("Can not find Controller " + controllerClass.getName() + " and method " + controllerMethod); log.error("Hint: make sure the controller returns a ninja.Result!"); log.error("Hint: Ninja does not allow more than one method with the same name!"); } return null; } /** * Build the route. * @param injector The injector to build the route with * @return The built route */ public Route buildRoute(Injector injector) { if (functionalMethod == null) { log.error("Error in route configuration for {}", uri); throw new IllegalStateException("Route missing a controller method"); } // Calculate filters LinkedList<Class<? extends Filter>> allFilters = new LinkedList<>(); allFilters.addAll(calculateGlobalFilters(this.globalFiltersOptional, injector)); allFilters.addAll(this.localFilters); allFilters.addAll(calculateFiltersForClass(functionalMethod.getDeclaringClass())); FilterWith filterWith = functionalMethod.getAnnotation(FilterWith.class); if (filterWith != null) { allFilters.addAll(Arrays.asList(filterWith.value())); } FilterChain filterChain = buildFilterChain(injector, allFilters); return new Route(httpMethod, uri, functionalMethod, filterChain); } private List<Class<? extends Filter>> calculateGlobalFilters( Optional<List<Class<? extends Filter>>> globalFiltersList, Injector injector) { List<Class<? extends Filter>> allFilters = Lists.newArrayList(); // Setting globalFilters in route will deactivate the filters defined // by conf.Filters if (globalFiltersList.isPresent()) { allFilters.addAll(globalFiltersList.get()); } else { String globalFiltersWithPrefixMaybe = ninjaBaseDirectoryResolver.resolveApplicationClassName(GLOBAL_FILTERS_DEFAULT_LOCATION); if (SwissKnife.doesClassExist(globalFiltersWithPrefixMaybe, this)) { try { Class<?> globalFiltersClass = Class.forName(globalFiltersWithPrefixMaybe); ApplicationFilters globalFilters = (ApplicationFilters) injector.getInstance(globalFiltersClass); globalFilters.addFilters(allFilters); } catch (Exception exception) { // That simply means the user did not configure conf.Filters. } } } return allFilters; } private FilterChain buildFilterChain(Injector injector, LinkedList<Class<? extends Filter>> filters) { if (filters.isEmpty()) { // either target object (functional method) or guice will create new instance Provider<?> targetProvider = (targetObject.isPresent() ? Providers.of(targetObject.get()) : injector.getProvider(functionalMethod.getDeclaringClass())); // invoke functional method with optionally using impl for argument extraction ControllerMethodInvoker methodInvoker = ControllerMethodInvoker.build( functionalMethod, implementationMethod.orElse(functionalMethod), injector, ninjaProperties); return new FilterChainEnd(targetProvider, methodInvoker); } else { Class<? extends Filter> filter = filters.pop(); Provider<? extends Filter> filterProvider = injector.getProvider(filter); return new FilterChainImpl(filterProvider,buildFilterChain(injector, filters)); } } private Set<Class<? extends Filter>> calculateFiltersForClass( Class controller) { LinkedHashSet<Class<? extends Filter>> filters = new LinkedHashSet<>(); // // Step up the superclass tree, so that superclass filters come first // // Superclass if (controller.getSuperclass() != null) { filters.addAll(calculateFiltersForClass(controller.getSuperclass())); } // Interfaces if (controller.getInterfaces() != null) { for (Class clazz : controller.getInterfaces()) { filters.addAll(calculateFiltersForClass(clazz)); } } // Now add from here FilterWith filterWith = (FilterWith) controller .getAnnotation(FilterWith.class); if (filterWith != null) { filters.addAll(Arrays.asList(filterWith.value())); } // And return return filters; } }