/* * Copyright (c) 2013-2014 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 io.werval.runtime.routes; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.Set; import io.werval.api.Application; import io.werval.api.exceptions.IllegalRouteException; import io.werval.api.exceptions.WervalException; import io.werval.api.http.Method; import io.werval.api.routes.ControllerCallRecorder; import io.werval.api.routes.ControllerParams; import io.werval.api.routes.ControllerParams.Param; import io.werval.api.routes.ControllerParams.ParamValue; import io.werval.api.routes.Route; import io.werval.api.routes.internal.RouteBuilderContext; import io.werval.runtime.util.Holder; import javassist.util.proxy.MethodHandler; import javassist.util.proxy.ProxyFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static java.util.Collections.emptySet; import static io.werval.runtime.ConfigKeys.WERVAL_ROUTES_IMPORTEDPACKAGES_CONTROLLERS; import static io.werval.runtime.ConfigKeys.WERVAL_ROUTES_IMPORTEDPACKAGES_PARAMETERS; import static io.werval.util.Strings.EMPTY; import static io.werval.util.Strings.isEmpty; import static io.werval.util.Strings.withHead; import static io.werval.util.Strings.withoutTrail; /** * RouteBuilder Instance. */ public class RouteBuilderInstance implements io.werval.api.routes.RouteBuilder { private final Application app; private final String pathPrefix; /** * Create a new RouteBuilder instance that cannot parse. * * See {@link #RouteBuilderInstance(io.werval.api.Application)}. */ public RouteBuilderInstance() { this( null, null ); } /** * Create a new RouteBuilder instance that cannot parse and apply a path prefix to all built routes. * * See {@link #RouteBuilderInstance(io.werval.api.Application)}. * * @param pathPrefix Path prefix */ public RouteBuilderInstance( String pathPrefix ) { this( null, pathPrefix ); } /** * Create a new RouteBuilder instance. * * @param app ApplicationSPI */ public RouteBuilderInstance( Application app ) { this( app, EMPTY ); } /** * Create a new RouteBuilder instance that apply a path prefix to all built routes. * * @param app ApplicationSPI * @param pathPrefix Path prefix */ public RouteBuilderInstance( Application app, String pathPrefix ) { this.app = app; if( isEmpty( pathPrefix ) ) { this.pathPrefix = EMPTY; } else { // Prepend / and remove trailing / if necessary this.pathPrefix = withoutTrail( withHead( pathPrefix, "/" ), "/" ); } } @Override public RouteDeclaration route() { return new RouteDeclarationInstance( null, pathPrefix, null, null, null, ControllerParams.EMPTY, emptySet() ); } @Override public RouteDeclaration route( String httpMethod ) { return new RouteDeclarationInstance( Method.valueOf( httpMethod ), pathPrefix, null, null, null, ControllerParams.EMPTY, emptySet() ); } @Override public RouteDeclaration route( Method httpMethod ) { return new RouteDeclarationInstance( httpMethod, pathPrefix, null, null, null, ControllerParams.EMPTY, emptySet() ); } @Override public RouteParser parse() { if( app == null ) { throw new IllegalStateException( "Cannot parse Routes without an Application" ); } return new RouteParserInstance( app, pathPrefix ); } private static final class RouteDeclarationInstance implements RouteDeclaration { private final Method httpMethod; private final String pathPrefix; private final String path; private final Class<?> controllerType; private final String controllerMethodName; private final ControllerParams controllerParams; private final Set<String> modifiers; public RouteDeclarationInstance( Method httpMethod, String pathPrefix, String path, Class<?> controllerType, String controllerMethodName, ControllerParams controllerParams, Set<String> modifiers ) { this.httpMethod = httpMethod; this.pathPrefix = pathPrefix; this.path = path; this.controllerType = controllerType; this.controllerMethodName = controllerMethodName; this.controllerParams = controllerParams; this.modifiers = modifiers; } @Override public RouteDeclaration method( String httpMethod ) { return new RouteDeclarationInstance( Method.valueOf( httpMethod ), pathPrefix, path, controllerType, controllerMethodName, controllerParams, modifiers ); } @Override public RouteDeclaration on( String path ) { return new RouteDeclarationInstance( httpMethod, pathPrefix, path, controllerType, controllerMethodName, controllerParams, modifiers ); } @Override public <T> RouteDeclaration to( Class<T> controllerType, ControllerCallRecorder<T> callRecorder ) { final Holder<String> methodNameHolder = new Holder<>(); T controllerProxy = controllerType.isInterface() ? newJavaProxy( controllerType, methodNameHolder ) : newJavassistProxy( controllerType, methodNameHolder ); try { // Belt & Braces RouteBuilderContext.clear(); // Record try { callRecorder.recordCall( controllerProxy ); } catch( Exception ex ) { throw new WervalException( "Error while recording Controller call for Route building: " + ex.getMessage(), ex ); } // Done! return new RouteDeclarationInstance( httpMethod, pathPrefix, path, controllerType, methodNameHolder.get(), new ControllerParams( RouteBuilderContext.recordedControllerParams() ), modifiers ); } finally { // Clean-up RouteBuilderContext.clear(); } } @Override public RouteDeclaration modifiedBy( String... modifiers ) { return new RouteDeclarationInstance( httpMethod, pathPrefix, path, controllerType, controllerMethodName, controllerParams, new LinkedHashSet<>( Arrays.asList( modifiers ) ) ); } @Override public Route build() { return new RouteInstance( httpMethod, pathPrefix + path, controllerType, controllerMethodName, controllerParams, modifiers ); } private <T> T newJavaProxy( final Class<T> controllerType, final Holder<String> methodNameHolder ) { return (T) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class<?>[] { controllerType }, new InvocationHandler() { @Override public Object invoke( Object proxy, java.lang.reflect.Method method, Object[] args ) { methodNameHolder.set( method.getName() ); return null; } } ); } private <T> T newJavassistProxy( final Class<T> controllerType, final Holder<String> methodNameHolder ) { ProxyFactory.ClassLoaderProvider previousJavassistLoader = ProxyFactory.classLoaderProvider; try { ProxyFactory.classLoaderProvider = new ProxyFactory.ClassLoaderProvider() { @Override public ClassLoader get( ProxyFactory proxyFactory ) { return Thread.currentThread().getContextClassLoader(); } }; ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setSuperclass( controllerType ); T controllerProxy = (T) proxyFactory.createClass().newInstance(); ( (javassist.util.proxy.Proxy) controllerProxy ).setHandler( new MethodHandler() { @Override public Object invoke( Object self, java.lang.reflect.Method controllerMethod, java.lang.reflect.Method proceed, Object[] args ) { methodNameHolder.set( controllerMethod.getName() ); return null; } } ); return controllerProxy; } catch( InstantiationException | IllegalAccessException ex ) { throw new WervalException( "Unable to record controller method in RouteBuilder", ex ); } finally { ProxyFactory.classLoaderProvider = previousJavassistLoader; } } } private static final class RouteParserInstance implements RouteParser { private static final Logger LOG = LoggerFactory.getLogger( RouteParser.class ); private final Application app; private final String pathPrefix; private RouteParserInstance( Application app, String pathPrefix ) { this.app = app; this.pathPrefix = pathPrefix; } @Override public List<Route> routes( String routesString ) { List<Route> routes = new ArrayList<>(); Scanner scanner = new Scanner( routesString ); while( scanner.hasNextLine() ) { String routeString = scanner.nextLine().trim().replaceAll( "\\s+", " " ); if( !routeString.startsWith( "#" ) && routeString.length() > 0 ) { routes.add( route( routeString ) ); } } return routes; } @Override public List<Route> routes( String... routeStrings ) { List<Route> routes = new ArrayList<>(); if( routeStrings != null ) { for( String routeString : routeStrings ) { routes.add( route( routeString ) ); } } return routes; } @Override public Route route( String routeString ) { if( isEmpty( routeString ) ) { throw new IllegalRouteException( "null", "Unable to parse null or empty String." ); } final String cleanRouteString = routeString.trim().replaceAll( "\\s+", " " ); try { // Split route String final int methodEnd = cleanRouteString.indexOf( ' ' ); final int pathEnd = cleanRouteString.indexOf( ' ', methodEnd + 1 ); final int controllerTypeEnd; final int controllerMethodEnd; final int controllerParamsEnd; final int modifiersEnd; if( cleanRouteString.indexOf( '(', pathEnd + 1 ) > 0 ) { // Parenthesis, with or without parameters or modifiers controllerTypeEnd = cleanRouteString .substring( 0, cleanRouteString.indexOf( '(', pathEnd ) ) .lastIndexOf( '.' ); controllerMethodEnd = cleanRouteString.indexOf( '(', controllerTypeEnd ); controllerParamsEnd = cleanRouteString.lastIndexOf( ')' ); modifiersEnd = cleanRouteString.length() > controllerParamsEnd + 1 ? cleanRouteString.length() : -1; } else if( cleanRouteString.indexOf( ' ', pathEnd + 1 ) > 0 ) { // No parenthesis with modifiers controllerTypeEnd = cleanRouteString .substring( 0, cleanRouteString.indexOf( ' ', pathEnd + 1 ) ) .lastIndexOf( '.' ); controllerMethodEnd = cleanRouteString.indexOf( ' ', controllerTypeEnd ); controllerParamsEnd = -1; modifiersEnd = cleanRouteString.length(); } else { // No parenthesis without modifiers // eg. POST /foo/bar com.acme.app.FakeController.test controllerTypeEnd = cleanRouteString.lastIndexOf( '.' ); controllerMethodEnd = cleanRouteString.length(); controllerParamsEnd = -1; modifiersEnd = -1; } if( LOG.isTraceEnabled() ) { LOG.trace( "Parsing route string, indices: {}\n" + "\tMethod end: {}\n" + "\tPath end: {}\n" + "\tController Type end: {}\n" + "\tController Method end: {}\n" + "\tController Params end: {}", new Object[] { cleanRouteString, methodEnd, pathEnd, controllerTypeEnd, controllerMethodEnd, controllerParamsEnd } ); } final String httpMethod = cleanRouteString.substring( 0, methodEnd ); final String path = cleanRouteString.substring( methodEnd + 1, pathEnd ); final String controllerTypeName = cleanRouteString.substring( pathEnd + 1, controllerTypeEnd ); final String controllerMethodName = cleanRouteString.substring( controllerTypeEnd + 1, controllerMethodEnd ); final String controllerMethodParams; if( controllerParamsEnd != -1 ) { controllerMethodParams = cleanRouteString .substring( controllerMethodEnd + 1, controllerParamsEnd ) .trim(); } else { controllerMethodParams = ""; } if( LOG.isTraceEnabled() ) { LOG.trace( "Parsing route string, values: {}\n" + "\tMethod: {}\n" + "\tPath: {}\n" + "\tController Type: {}\n" + "\tController Method: {}\n" + "\tController Params: {}", new Object[] { cleanRouteString, httpMethod, path, controllerTypeName, controllerMethodName, controllerMethodParams } ); } // Parse controller type Class<?> controllerType = lookupControllerType( app, controllerTypeName ); // Parse controller method parameters ControllerParams controllerParams = parseControllerParams( app, routeString, controllerMethodParams ); // Eventually parse modifiers Set<String> modifiers = new LinkedHashSet<>(); if( modifiersEnd != -1 ) { String modifiersString = controllerParamsEnd != -1 ? cleanRouteString.substring( controllerParamsEnd + 2 ) : cleanRouteString.substring( controllerMethodEnd + 1 ); modifiers.addAll( Arrays.asList( modifiersString.trim().split( " " ) ) ); } // Create new Route instance return new RouteInstance( Method.valueOf( httpMethod ), pathPrefix + path, controllerType, controllerMethodName, controllerParams, modifiers ); } catch( IllegalRouteException ex ) { throw ex; } catch( Exception ex ) { throw new IllegalRouteException( routeString, ex.getMessage(), ex ); } } private static ControllerParams parseControllerParams( Application application, String routeString, String controllerMethodParams ) throws ClassNotFoundException { Map<String, Param> controllerParams = new LinkedHashMap<>(); if( controllerMethodParams.length() > 0 ) { List<String> controllerMethodParamsSegments = splitControllerMethodParams( controllerMethodParams ); for( String controllerMethodParamSegment : controllerMethodParamsSegments ) { switch( valueKindOfSegment( controllerMethodParamSegment ) ) { case DEFAULTED: // Parameter with default value String[] dEqualSplitted = controllerMethodParamSegment.split( "\\?=", 2 ); String[] dTypeAndName = dEqualSplitted[0].trim().split( " " ); String dName = dTypeAndName[1]; Class<?> dType = lookupParamType( application, dTypeAndName[0] ); String defaultValueString = dEqualSplitted[1].trim(); if( defaultValueString.startsWith( "'" ) && defaultValueString.endsWith( "'" ) ) { defaultValueString = defaultValueString .substring( 1, defaultValueString.length() - 1 ) .replaceAll( "\\\\'", "'" ); } Object defaultValue = application.parameterBinders().bind( dType, dName, defaultValueString ); controllerParams.put( dName, new Param( dName, dType, ParamValue.DEFAULTED, defaultValue ) ); break; case FORCED: // Parameter with forced value String[] fEqualSplitted = controllerMethodParamSegment.split( "=", 2 ); String[] fTypeAndName = fEqualSplitted[0].trim().split( " " ); String fName = fTypeAndName[1]; Class<?> fType = lookupParamType( application, fTypeAndName[0] ); String forcedValueString = fEqualSplitted[1].trim(); if( forcedValueString.startsWith( "'" ) && forcedValueString.endsWith( "'" ) ) { forcedValueString = forcedValueString .substring( 1, forcedValueString.length() - 1 ) .replaceAll( "\\\\'", "'" ); } Object forcedValue = application.parameterBinders().bind( fType, fName, forcedValueString ); controllerParams.put( fName, new Param( fName, fType, ParamValue.FORCED, forcedValue ) ); break; case NONE: // Parameter without forced value String[] splitted = controllerMethodParamSegment.split( " " ); if( splitted.length != 2 ) { throw new IllegalRouteException( routeString, "Unable to parse parameter: " + controllerMethodParamSegment ); } String name = splitted[1]; String paramTypeName = splitted[0]; Class<?> type = lookupParamType( application, paramTypeName ); controllerParams.put( name, new Param( name, type ) ); break; default: } } } return new ControllerParams( controllerParams ); } private static List<String> splitControllerMethodParams( String controllerMethodParams ) { List<String> segments = new ArrayList<>(); boolean insideQuotes = false; int previous = controllerMethodParams.codePointAt( 0 ); StringBuilder sb = new StringBuilder().append( Character.toChars( previous ) ); for( int idx = Character.charCount( previous ); idx < controllerMethodParams.length(); ) { int character = controllerMethodParams.codePointAt( idx ); if( character == '\'' && previous != '\\' ) { insideQuotes = !insideQuotes; } if( insideQuotes || character != ',' ) { sb.append( Character.toChars( character ) ); } else { segments.add( sb.toString().trim() ); sb = new StringBuilder(); } previous = character; idx += Character.charCount( character ); } segments.add( sb.toString().trim() ); return segments; } private static ParamValue valueKindOfSegment( String segment ) { if( segment.length() < 2 ) { return ParamValue.NONE; } boolean insideQuotes = false; int previous = segment.codePointAt( 0 ); for( int idx = Character.charCount( previous ); idx < segment.length(); ) { int character = segment.codePointAt( idx ); if( character == '\'' && previous != '\\' ) { insideQuotes = !insideQuotes; } if( '=' == character && !insideQuotes ) { if( '?' == previous ) { return ParamValue.DEFAULTED; } return ParamValue.FORCED; } previous = character; idx += Character.charCount( character ); } return ParamValue.NONE; } private static Class<?> lookupControllerType( Application application, String controllerTypeName ) throws ClassNotFoundException { try { // First try parameter FQCN return application.classLoader().loadClass( controllerTypeName ); } catch( ClassNotFoundException fqcnNotFound ) { return lookupImportedType( "Controller", application, controllerTypeName, application.config().stringList( WERVAL_ROUTES_IMPORTEDPACKAGES_CONTROLLERS ) ); } } private static Class<?> lookupParamType( Application application, String paramTypeName ) throws ClassNotFoundException { try { // First try parameter FQCN return application.classLoader().loadClass( paramTypeName ); } catch( ClassNotFoundException fqcnNotFound ) { return lookupImportedType( "Param", application, paramTypeName, application.config().stringList( WERVAL_ROUTES_IMPORTEDPACKAGES_PARAMETERS ) ); } } private static Class<?> lookupImportedType( String typeKind, Application application, String typeName, List<String> importedPackages ) throws ClassNotFoundException { LOG.trace( "Lookup {} Type - {} not found", typeKind, typeName ); List<String> typesTried = new ArrayList<>(); typesTried.add( typeName ); // Try in configured imported packages for( String importedPackage : importedPackages ) { String fqcn = importedPackage + "." + typeName; try { Class<?> clazz = application.classLoader().loadClass( fqcn ); LOG.trace( "Lookup {} Type - {} found", typeKind, fqcn ); return clazz; } catch( ClassNotFoundException importedNotFound ) { LOG.trace( "Lookup {} Type - {} not found", typeKind, fqcn ); } } throw new ClassNotFoundException( typeKind + " type not found, tried " + typesTried.toString() ); } } }