/** * 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 java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import ninja.utils.MethodReference; import ninja.utils.NinjaProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Provider; public class RouterImpl implements Router { static private final Logger logger = LoggerFactory.getLogger(RouterImpl.class); private final NinjaProperties ninjaProperties; private final List<RouteBuilderImpl> allRouteBuilders = new ArrayList<>(); private final Injector injector; private List<Route> routes; // for fast reverse route lookups private Map<MethodReference,Route> reverseRoutes; // This regex works for both {myParam} AND {myParam: .*} (with regex) private final String VARIABLE_PART_PATTERN_WITH_PLACEHOLDER = "\\{(%s)(:\\s([^}]*))?\\}"; private final Provider<RouteBuilderImpl> routeBuilderImplProvider; @Inject public RouterImpl(Injector injector, NinjaProperties ninjaProperties, Provider<RouteBuilderImpl> routeBuilderImplProvider) { this.injector = injector; this.ninjaProperties = ninjaProperties; this.routeBuilderImplProvider = routeBuilderImplProvider; } @Override public Route getRouteFor(String httpMethod, String uri) { if (routes == null) { throw new IllegalStateException( "Attempt to get route when routes not compiled"); } for (Route route : routes) { if (route.matches(httpMethod, uri)) { return route; } } return null; } @Override public String getReverseRoute( Class<?> controllerClass, String controllerMethodName) { Optional<Map<String, Object>> parameterMap = Optional.empty(); return getReverseRoute(controllerClass, controllerMethodName, parameterMap); } @Override public String getReverseRoute(Class<?> controllerClass, String controllerMethodName, Object... parameterMap) { if (parameterMap.length % 2 != 0) { logger.error("Always provide key (as String) value (as Object) pairs in parameterMap. That means providing e.g. 2, 4, 6... objects."); return null; } Map<String, Object> map = new HashMap<>(parameterMap.length / 2); for (int i = 0; i < parameterMap.length; i += 2) { map.put((String) parameterMap[i], parameterMap[i + 1]); } return getReverseRoute(controllerClass, controllerMethodName, Optional.of(map)); } @Override public String getReverseRoute(Class<?> controllerClass, String controllerMethodName, Map<String, Object> parameterMap) { Optional<Map<String, Object>> parameterMapOptional = Optional.ofNullable(parameterMap); return getReverseRoute( controllerClass, controllerMethodName, parameterMapOptional); } @Override public String getReverseRoute( Class<?> controllerClass, String controllerMethodName, Optional<Map<String, Object>> parameterMap) { try { ReverseRouter.Builder reverseRouteBuilder = new ReverseRouter(ninjaProperties, this) .with(controllerClass, controllerMethodName); if (parameterMap.isPresent()) { // pathOrQueryParams are not escaped with the deprecated method of creating // reverse routes. use ReverseRouter! parameterMap.get().forEach((name, value) -> { // path or query param? if (reverseRouteBuilder.getRoute().getParameters().containsKey(name)) { reverseRouteBuilder.rawPathParam(name, value); } else { reverseRouteBuilder.rawQueryParam(name, value); } }); } return reverseRouteBuilder.build(); } catch (IllegalArgumentException e) { logger.error("Unable to cleanly build reverse route", e); return null; } } @Override public String getReverseRoute(MethodReference controllerMethodRef) { return getReverseRoute( controllerMethodRef.getDeclaringClass(), controllerMethodRef.getMethodName()); } @Override public String getReverseRoute(MethodReference controllerMethodRef, Map<String, Object> parameterMap) { return getReverseRoute( controllerMethodRef.getDeclaringClass(), controllerMethodRef.getMethodName(), parameterMap); } @Override public String getReverseRoute(MethodReference controllerMethodRef, Object ... parameterMap) { return getReverseRoute( controllerMethodRef.getDeclaringClass(), controllerMethodRef.getMethodName(), parameterMap); } @Override public String getReverseRoute(MethodReference controllerMethodRef, Optional<Map<String, Object>> parameterMap) { return getReverseRoute( controllerMethodRef.getDeclaringClass(), controllerMethodRef.getMethodName(), parameterMap); } @Override public void compileRoutes() { if (routes != null) { throw new IllegalStateException("Routes already compiled"); } List<Route> routesLocal = new ArrayList<>(); for (RouteBuilderImpl routeBuilder : allRouteBuilders) { routesLocal.add(routeBuilder.buildRoute(injector)); } this.routes = ImmutableList.copyOf(routesLocal); // compile reverse routes for O(1) lookups this.reverseRoutes = new HashMap<>(this.routes.size()); for (Route route : this.routes) { // its possible this route is a Result instead of a controller method if (route.getControllerClass() != null) { MethodReference controllerMethodRef = new MethodReference( route.getControllerClass(), route.getControllerMethod().getName()); if (this.reverseRoutes.containsKey(controllerMethodRef)) { // the first one wins with reverse routing so we ignore this route } else { this.reverseRoutes.put(controllerMethodRef, route); } } } logRoutes(); } @Override public List<Route> getRoutes() { verifyRoutesCompiled(); return routes; } @Override public RouteBuilder GET() { RouteBuilderImpl routeBuilder = routeBuilderImplProvider.get().GET(); allRouteBuilders.add(routeBuilder); return routeBuilder; } @Override public RouteBuilder POST() { RouteBuilderImpl routeBuilder = routeBuilderImplProvider.get().POST(); allRouteBuilders.add(routeBuilder); return routeBuilder; } @Override public RouteBuilder PUT() { RouteBuilderImpl routeBuilder = routeBuilderImplProvider.get().PUT(); allRouteBuilders.add(routeBuilder); return routeBuilder; } @Override public RouteBuilder DELETE() { RouteBuilderImpl routeBuilder = routeBuilderImplProvider.get().DELETE(); allRouteBuilders.add(routeBuilder); return routeBuilder; } @Override public RouteBuilder OPTIONS() { RouteBuilderImpl routeBuilder = routeBuilderImplProvider.get().OPTIONS(); allRouteBuilders.add(routeBuilder); return routeBuilder; } @Override public RouteBuilder HEAD() { RouteBuilderImpl routeBuilder = routeBuilderImplProvider.get().HEAD(); allRouteBuilders.add(routeBuilder); return routeBuilder; } @Override public RouteBuilder METHOD(String method) { RouteBuilderImpl routeBuilder = routeBuilderImplProvider.get().METHOD(method); allRouteBuilders.add(routeBuilder); return routeBuilder; } private void verifyRoutesCompiled() { if (routes == null) { throw new IllegalStateException( "Routes not compiled!"); } } @Override public Optional<Route> getRouteForControllerClassAndMethod( Class<?> controllerClass, String controllerMethodName) { verifyRoutesCompiled(); MethodReference reverseRouteKey = new MethodReference(controllerClass, controllerMethodName); Route route = this.reverseRoutes.get(reverseRouteKey); return Optional.ofNullable(route); } private void logRoutes() { // determine the width of the columns int maxMethodLen = 0; int maxPathLen = 0; int maxControllerLen = 0; for (Route route : getRoutes()) { maxMethodLen = Math.max(maxMethodLen, route.getHttpMethod().length()); maxPathLen = Math.max(maxPathLen, route.getUri().length()); if (route.getControllerClass() != null) { int controllerLen = route.getControllerClass().getName().length() + route.getControllerMethod().getName().length(); maxControllerLen = Math.max(maxControllerLen, controllerLen); } } // log the routing table int borderLen = 10 + maxMethodLen + maxPathLen + maxControllerLen; String border = Strings.padEnd("", borderLen, '-'); logger.info(border); logger.info("Registered routes"); logger.info(border); for (Route route : getRoutes()) { if (route.getControllerClass() != null) { logger.info("{} {} => {}.{}()", Strings.padEnd(route.getHttpMethod(), maxMethodLen, ' '), Strings.padEnd(route.getUri(), maxPathLen, ' '), route.getControllerClass().getName(), route.getControllerMethod().getName()); } else { logger.info("{} {}", route.getHttpMethod(), route.getUri()); } } } }