/** * Copyright (C) 2013-2015 all@code-story.net * * 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 net.codestory.http.routes; import net.codestory.http.Configuration; import net.codestory.http.Context; import net.codestory.http.Request; import net.codestory.http.Response; import net.codestory.http.annotations.*; import net.codestory.http.compilers.CompilerFacade; import net.codestory.http.convert.TypeConvert; import net.codestory.http.extensions.Extensions; import net.codestory.http.filters.Filter; import net.codestory.http.injection.IocAdapter; import net.codestory.http.injection.Singletons; import net.codestory.http.io.ClassPaths; import net.codestory.http.io.ClasspathScanner; import net.codestory.http.io.Resources; import net.codestory.http.livereload.LiveReloadListener; import net.codestory.http.misc.Env; import net.codestory.http.payload.Payload; import net.codestory.http.payload.PayloadWriter; import net.codestory.http.security.User; import net.codestory.http.templating.Site; import net.codestory.http.websockets.WebSocketListener; import net.codestory.http.websockets.WebSocketListenerFactory; import net.codestory.http.websockets.WebSocketSession; import java.io.*; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Deque; import java.util.LinkedList; import java.util.Set; import java.util.function.Supplier; import static java.util.stream.Stream.of; import static net.codestory.http.annotations.AnnotationHelper.parseAnnotations; import static net.codestory.http.constants.Methods.*; import static net.codestory.http.payload.Payload.*; import static net.codestory.http.routes.UriParser.paramsCount; public class RouteCollection implements Routes { protected final Env env; protected final Resources resources; protected final CompilerFacade compilers; protected final Site site; protected final MethodAnnotationsFactory methodAnnotationsFactory; protected final RouteSorter routes; protected final Deque<Supplier<Filter>> filters; protected IocAdapter iocAdapter; protected Extensions extensions; protected WebSocketListenerFactory webSocketListenerFactory; protected ContextToPayload contextToPayload; public RouteCollection(Env env) { this.env = env; this.resources = new Resources(env); this.compilers = new CompilerFacade(env, resources); this.site = new Site(env, resources); this.methodAnnotationsFactory = createMethodAnnotationsFactory(); this.routes = new RouteSorter(); this.filters = new LinkedList<>(); this.iocAdapter = new Singletons(); this.extensions = new Extensions() { // No extension }; this.webSocketListenerFactory = (session, context) -> { throw new UnsupportedOperationException(); }; } public void configure(Configuration configuration) { configuration.configure(this); installExtensions(); addStaticRoutes(); contextToPayload = createContextToPayload(routes.getSortedRoutes(), filters); } private void installExtensions() { TypeConvert.configureOrReplaceMapper(mapper -> extensions.configureOrReplaceObjectMapper(mapper, env)); extensions.configureCompilers(compilers, env); } private void addStaticRoutes() { routes.addStaticRoute(new WebJarsRoute(env.prodMode())); routes.addStaticRoute(new StaticRoute(env.prodMode(), resources, compilers)); if (!env.prodMode()) { routes.addStaticRoute(new SourceMapRoute(resources, compilers)); routes.addStaticRoute(new SourceRoute(resources)); } if (env.liveReloadServer()) { get("/livereload.js", ClassPaths.getResource("livereload/livereload.js")); setWebSocketListenerFactory((session, context) -> new LiveReloadListener(session, env)); } } public PayloadWriter createPayloadWriter(Request request, Response response) { return extensions.createPayloadWriter(request, response, env, site, resources, compilers); } public Context createContext(Request request, Response response) { return extensions.createContext(request, response, iocAdapter, env, site); } @Override public RouteCollection setExtensions(Extensions extensions) { this.extensions = extensions; return this; } @Override public RouteCollection setIocAdapter(IocAdapter iocAdapter) { this.iocAdapter = iocAdapter; return this; } @Override public Routes setWebSocketListenerFactory(WebSocketListenerFactory factory) { this.webSocketListenerFactory = factory; return null; } @Override public RouteCollection filter(Class<? extends Filter> filterClass) { filters.addFirst(() -> iocAdapter.get(filterClass)); return this; } @Override public RouteCollection filter(Filter filter) { filters.addFirst(() -> filter); return this; } @Override public RouteCollection add(Class<?> resourceType) { addResource("", resourceType, () -> iocAdapter.get(resourceType)); return this; } @Override public RouteCollection add(String urlPrefix, Class<?> resourceType) { addResource(urlPrefix, resourceType, () -> iocAdapter.get(resourceType)); return this; } @Override public RouteCollection add(Object resource) { addResource("", resource.getClass(), () -> resource); return this; } @Override public RouteCollection add(String urlPrefix, Object resource) { addResource(urlPrefix, resource.getClass(), () -> resource); return this; } protected void addResource(String urlPrefix, Class<?> resourceType, Supplier<Object> resource) { parseAnnotations(urlPrefix, resourceType, (httpMethod, uri, method) -> addResource(httpMethod, method, resource, uri)); } protected void addResource(String httpMethod, Method method, Supplier<Object> resource, String uriPattern) { int methodParamsCount = method.getParameterCount(); int uriParamsCount = paramsCount(uriPattern); if (methodParamsCount < uriParamsCount) { throw new IllegalArgumentException("Expected at least " + uriParamsCount + " parameters in " + uriPattern); } add(httpMethod, uriPattern, new ReflectionRoute(resource, method, methodAnnotationsFactory.forMethod(method))); } @Override public RouteCollection get(String uriPattern, Object payload) { get(uriPattern, () -> payload); return this; } @Override public RouteCollection get(String uriPattern, NoParamRoute route) { add(GET, checkParametersCount(uriPattern, 0), route); return this; } @Override public RouteCollection get(String uriPattern, NoParamRouteWithContext route) { add(GET, checkParametersCount(uriPattern, 0), route); return this; } @Override public RouteCollection get(String uriPattern, OneParamRoute route) { add(GET, checkParametersCount(uriPattern, 1), route); return this; } @Override public RouteCollection get(String uriPattern, TwoParamsRoute route) { add(GET, checkParametersCount(uriPattern, 2), route); return this; } @Override public RouteCollection get(String uriPattern, ThreeParamsRoute route) { add(GET, checkParametersCount(uriPattern, 3), route); return this; } @Override public RouteCollection get(String uriPattern, FourParamsRoute route) { add(GET, checkParametersCount(uriPattern, 4), route); return this; } @Override public RouteCollection options(String uriPattern, Object payload) { options(uriPattern, () -> payload); return this; } @Override public RouteCollection options(String uriPattern, NoParamRoute route) { add(OPTIONS, checkParametersCount(uriPattern, 0), route); return this; } @Override public RouteCollection options(String uriPattern, NoParamRouteWithContext route) { add(OPTIONS, checkParametersCount(uriPattern, 0), route); return this; } @Override public RouteCollection options(String uriPattern, OneParamRoute route) { add(OPTIONS, checkParametersCount(uriPattern, 1), route); return this; } @Override public RouteCollection options(String uriPattern, TwoParamsRoute route) { add(OPTIONS, checkParametersCount(uriPattern, 2), route); return this; } @Override public RouteCollection options(String uriPattern, ThreeParamsRoute route) { add(OPTIONS, checkParametersCount(uriPattern, 3), route); return this; } @Override public RouteCollection options(String uriPattern, FourParamsRoute route) { add(OPTIONS, checkParametersCount(uriPattern, 4), route); return this; } @Override public RouteCollection head(String uriPattern, Object payload) { head(uriPattern, () -> payload); return this; } @Override public RouteCollection head(String uriPattern, NoParamRoute route) { add(HEAD, checkParametersCount(uriPattern, 0), route); return this; } @Override public RouteCollection head(String uriPattern, NoParamRouteWithContext route) { add(HEAD, checkParametersCount(uriPattern, 0), route); return this; } @Override public RouteCollection head(String uriPattern, OneParamRoute route) { add(HEAD, checkParametersCount(uriPattern, 1), route); return this; } @Override public RouteCollection head(String uriPattern, TwoParamsRoute route) { add(HEAD, checkParametersCount(uriPattern, 2), route); return this; } @Override public RouteCollection head(String uriPattern, ThreeParamsRoute route) { add(HEAD, checkParametersCount(uriPattern, 3), route); return this; } @Override public RouteCollection head(String uriPattern, FourParamsRoute route) { add(HEAD, checkParametersCount(uriPattern, 4), route); return this; } @Override public RouteCollection post(String uriPattern, NoParamRoute route) { add(POST, checkParametersCount(uriPattern, 0), route); return this; } @Override public RouteCollection post(String uriPattern, NoParamRouteWithContext route) { add(POST, checkParametersCount(uriPattern, 0), route); return this; } @Override public RouteCollection post(String uriPattern, OneParamRoute route) { add(POST, checkParametersCount(uriPattern, 1), route); return this; } @Override public RouteCollection post(String uriPattern, TwoParamsRoute route) { add(POST, checkParametersCount(uriPattern, 2), route); return this; } @Override public RouteCollection post(String uriPattern, ThreeParamsRoute route) { add(POST, checkParametersCount(uriPattern, 3), route); return this; } @Override public RouteCollection post(String uriPattern, FourParamsRoute route) { add(POST, checkParametersCount(uriPattern, 4), route); return this; } @Override public RouteCollection put(String uriPattern, NoParamRoute route) { add(PUT, checkParametersCount(uriPattern, 0), route); return this; } @Override public RouteCollection put(String uriPattern, NoParamRouteWithContext route) { add(PUT, checkParametersCount(uriPattern, 0), route); return this; } @Override public RouteCollection put(String uriPattern, OneParamRoute route) { add(PUT, checkParametersCount(uriPattern, 1), route); return this; } @Override public RouteCollection put(String uriPattern, TwoParamsRoute route) { add(PUT, checkParametersCount(uriPattern, 2), route); return this; } @Override public RouteCollection put(String uriPattern, ThreeParamsRoute route) { add(PUT, checkParametersCount(uriPattern, 3), route); return this; } @Override public RouteCollection put(String uriPattern, FourParamsRoute route) { add(PUT, checkParametersCount(uriPattern, 4), route); return this; } @Override public RouteCollection delete(String uriPattern, NoParamRoute route) { add(DELETE, checkParametersCount(uriPattern, 0), route); return this; } @Override public RouteCollection delete(String uriPattern, NoParamRouteWithContext route) { add(DELETE, checkParametersCount(uriPattern, 0), route); return this; } @Override public RouteCollection delete(String uriPattern, OneParamRoute route) { add(DELETE, checkParametersCount(uriPattern, 1), route); return this; } @Override public RouteCollection delete(String uriPattern, TwoParamsRoute route) { add(DELETE, checkParametersCount(uriPattern, 2), route); return this; } @Override public RouteCollection delete(String uriPattern, ThreeParamsRoute route) { add(DELETE, checkParametersCount(uriPattern, 3), route); return this; } @Override public RouteCollection delete(String uriPattern, FourParamsRoute route) { add(DELETE, checkParametersCount(uriPattern, 4), route); return this; } @Override public Routes anyGet(NoParamRouteWithContext route) { routes.addCatchAllRoute(new CatchAllRoute(GET, route)); return this; } @Override public Routes anyHead(NoParamRouteWithContext route) { routes.addCatchAllRoute(new CatchAllRoute(HEAD, route)); return this; } @Override public Routes anyPost(NoParamRouteWithContext route) { routes.addCatchAllRoute(new CatchAllRoute(POST, route)); return this; } @Override public Routes anyPut(NoParamRouteWithContext route) { routes.addCatchAllRoute(new CatchAllRoute(PUT, route)); return this; } @Override public Routes anyOptions(NoParamRouteWithContext route) { routes.addCatchAllRoute(new CatchAllRoute(OPTIONS, route)); return this; } @Override public Routes anyDelete(NoParamRouteWithContext route) { routes.addCatchAllRoute(new CatchAllRoute(DELETE, route)); return this; } @Override public RouteCollection any(NoParamRouteWithContext route) { routes.addCatchAllRoute(new CatchAllRoute(route)); return this; } @Override public RouteCollection autoDiscover(String packageToScan) { Set<Class<?>> types = new ClasspathScanner().getTypesAnnotatedWith(packageToScan, Resource.class); types.forEach(this::add); return this; } @Override public Routes bind(String uriRoot, File path) { routes.addStaticRoute(new BoundFolderRoute(uriRoot, path)); return this; } @Override public <T extends Annotation> Routes registerAroundAnnotation(Class<T> annotationType, ApplyAroundAnnotation<T> apply) { methodAnnotationsFactory.registerAroundAnnotation(annotationType, () -> apply); return this; } @Override public <T extends Annotation> Routes registerAroundAnnotation(Class<T> annotationType, Class<? extends ApplyAroundAnnotation<T>> applyType) { methodAnnotationsFactory.registerAroundAnnotation(annotationType, () -> (ApplyAroundAnnotation<T>) iocAdapter.get(applyType)); return this; } @Override public <T extends Annotation> Routes registerAfterAnnotation(Class<T> annotationType, ApplyAfterAnnotation<T> apply) { methodAnnotationsFactory.registerAfterAnnotation(annotationType, () -> apply); return this; } @Override public <T extends Annotation> Routes registerAfterAnnotation(Class<T> annotationType, Class<? extends ApplyAfterAnnotation<T>> applyType) { methodAnnotationsFactory.registerAfterAnnotation(annotationType, () -> (ApplyAfterAnnotation<T>) iocAdapter.get(applyType)); return this; } @Override public RoutesWithPattern url(String uriPattern) { return new RoutesWithPattern(this, uriPattern); } protected RouteCollection add(String method, String uriPattern, AnyRoute route) { routes.addUserRoute(new RouteWithPattern(method, uriPattern, route)); return this; } public WebSocketListener createWebSocketListener(WebSocketSession session, Request request, Response response) { Context context = createContext(request, response); return webSocketListenerFactory.create(session, context); } public Payload apply(Request request, Response response) throws Exception { Context context = createContext(request, response); if (context.uri() == null) { return Payload.notFound(); } return contextToPayload.get(context.uri(), context); } private static ContextToPayload createContextToPayload(Route[] sortedRoutes, Deque<Supplier<Filter>> filters) { ContextToPayload payloadSupplier = (uri, context) -> { Payload response = notFound(); for (Route route : sortedRoutes) { if (route.matchUri(uri)) { if (route.matchMethod(context.method())) { return route.apply(uri, context); } response = methodNotAllowed(); } } return response; }; for (Supplier<Filter> filterSupplier : filters) { Filter filter = filterSupplier.get(); ContextToPayload nextFilter = payloadSupplier; payloadSupplier = (uri, context) -> { if (filter.matches(uri, context)) { return filter.apply(uri, context, () -> nextFilter.get(uri, context)); } else { return nextFilter.get(uri, context); } }; } return payloadSupplier; } protected MethodAnnotationsFactory createMethodAnnotationsFactory() { MethodAnnotationsFactory factory = new MethodAnnotationsFactory(); factory.registerAroundAnnotation(Roles.class, () -> (roles, context, payloadSupplier) -> isAuthorized(roles, context.currentUser()) ? payloadSupplier.apply(context) : Payload.forbidden()); factory.registerAfterAnnotation(AllowOrigin.class, () -> (origin, context, payload) -> payload.withAllowOrigin(origin.value())); factory.registerAfterAnnotation(AllowMethods.class, () -> (methods, context, payload) -> payload.withAllowMethods(methods.value())); factory.registerAfterAnnotation(AllowCredentials.class, () -> (credentials, context, payload) -> payload.withAllowCredentials(credentials.value())); factory.registerAfterAnnotation(AllowHeaders.class, () -> (allowedHeaders, context, payload) -> payload.withAllowHeaders(allowedHeaders.value())); factory.registerAfterAnnotation(ExposeHeaders.class, () -> (exposedHeaders, context, payload) -> payload.withExposeHeaders(exposedHeaders.value())); factory.registerAfterAnnotation(MaxAge.class, () -> (maxAge, context, payload) -> payload.withMaxAge(maxAge.value())); return factory; } protected boolean isAuthorized(Roles roles, User user) { if (roles.allMatch()) { return of(roles.value()).allMatch(user::isInRole); } else { return of(roles.value()).anyMatch(user::isInRole); } } protected String checkParametersCount(String uriPattern, int count) { if (paramsCount(uriPattern) != count) { String error = (count == 1) ? "1 parameter" : count + " parameters"; throw new IllegalArgumentException("Expected " + error + " in " + uriPattern); } return uriPattern; } }