package play.mvc; import java.io.ByteArrayInputStream; import java.io.File; import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.*; import org.apache.commons.lang.StringUtils; import play.Logger; import play.Play; import play.cache.CacheFor; import play.classloading.enhancers.ControllersEnhancer.ControllerInstrumentation; import play.classloading.enhancers.ControllersEnhancer.ControllerSupport; import play.data.binding.Binder; import play.data.binding.CachedBoundActionMethodArgs; import play.data.binding.ParamNode; import play.data.binding.RootParamNode; import play.data.parsing.UrlEncodedParser; import play.data.validation.Validation; import play.exceptions.ActionNotFoundException; import play.exceptions.JavaExecutionException; import play.exceptions.PlayException; import play.exceptions.UnexpectedException; import play.i18n.Lang; import play.mvc.Http.Request; import play.mvc.Router.Route; import play.mvc.results.NoResult; import play.mvc.results.Result; import play.utils.Java; import play.utils.Utils; import com.jamonapi.Monitor; import com.jamonapi.MonitorFactory; import java.util.concurrent.Future; import org.apache.commons.javaflow.Continuation; import org.apache.commons.javaflow.bytecode.StackRecorder; import play.Invoker.Suspend; import play.classloading.enhancers.ControllersEnhancer; import play.mvc.results.NotFound; /** * Invoke an action after an HTTP request. */ public class ActionInvoker { @SuppressWarnings("unchecked") public static void resolve(Http.Request request, Http.Response response) { if (!Play.started) { return; } Http.Request.current.set(request); Http.Response.current.set(response); Scope.Params.current.set(request.params); Scope.RenderArgs.current.set(new Scope.RenderArgs()); Scope.RouteArgs.current.set(new Scope.RouteArgs()); Scope.Session.current.set(Scope.Session.restore()); Scope.Flash.current.set(Scope.Flash.restore()); CachedBoundActionMethodArgs.init(); ControllersEnhancer.currentAction.set(new Stack<String>()); if (request.resolved) { return; } // Route and resolve format if not already done if (request.action == null) { Play.pluginCollection.routeRequest(request); Route route = Router.route(request); Play.pluginCollection.onRequestRouting(route); } request.resolveFormat(); // Find the action method try { Method actionMethod = null; Object[] ca = getActionMethod(request.action); actionMethod = (Method) ca[1]; request.controller = ((Class) ca[0]).getName().substring(12).replace("$", ""); request.controllerClass = ((Class) ca[0]); request.actionMethod = actionMethod.getName(); request.action = request.controller + "." + request.actionMethod; request.invokedMethod = actionMethod; if (Logger.isTraceEnabled()) { Logger.trace("------- %s", actionMethod); } request.resolved = true; } catch (ActionNotFoundException e) { Logger.error(e, "%s action not found", e.getAction()); throw new NotFound(String.format("%s action not found", e.getAction())); } } public static void invoke(Http.Request request, Http.Response response) { Monitor monitor = null; try { resolve(request, response); Method actionMethod = request.invokedMethod; // 1. Prepare request params Scope.Params.current().__mergeWith(request.routeArgs); // add parameters from the URI query string String encoding = Http.Request.current().encoding; Scope.Params.current()._mergeWith(UrlEncodedParser.parseQueryString(new ByteArrayInputStream(request.querystring.getBytes(encoding)))); // 2. Easy debugging ... if (Play.mode == Play.Mode.DEV) { Controller.class.getDeclaredField("params").set(null, Scope.Params.current()); Controller.class.getDeclaredField("request").set(null, Http.Request.current()); Controller.class.getDeclaredField("response").set(null, Http.Response.current()); Controller.class.getDeclaredField("session").set(null, Scope.Session.current()); Controller.class.getDeclaredField("flash").set(null, Scope.Flash.current()); Controller.class.getDeclaredField("renderArgs").set(null, Scope.RenderArgs.current()); Controller.class.getDeclaredField("routeArgs").set(null, Scope.RouteArgs.current()); Controller.class.getDeclaredField("validation").set(null, Validation.current()); } ControllerInstrumentation.stopActionCall(); Play.pluginCollection.beforeActionInvocation(actionMethod); // Monitoring monitor = MonitorFactory.start(request.action + "()"); // 3. Invoke the action try { // @Before handleBefores(request); // Action Result actionResult = null; String cacheKey = null; // Check the cache (only for GET or HEAD) if ((request.method.equals("GET") || request.method.equals("HEAD")) && actionMethod.isAnnotationPresent(CacheFor.class)) { cacheKey = actionMethod.getAnnotation(CacheFor.class).id(); if ("".equals(cacheKey)) { cacheKey = "urlcache:" + request.url + request.querystring; } actionResult = (Result) play.cache.Cache.get(cacheKey); } if (actionResult == null) { ControllerInstrumentation.initActionCall(); try { inferResult(invokeControllerMethod(actionMethod)); } catch (InvocationTargetException ex) { // It's a Result ? (expected) if (ex.getTargetException() instanceof Result) { actionResult = (Result) ex.getTargetException(); // Cache it if needed if (cacheKey != null) { play.cache.Cache.set(cacheKey, actionResult, actionMethod.getAnnotation(CacheFor.class).value()); } } else { // @Catch Object[] args = new Object[]{ex.getTargetException()}; List<Method> catches = Java.findAllAnnotatedMethods(Controller.getControllerClass(), Catch.class); Collections.sort(catches, new Comparator<Method>() { public int compare(Method m1, Method m2) { Catch catch1 = m1.getAnnotation(Catch.class); Catch catch2 = m2.getAnnotation(Catch.class); return catch1.priority() - catch2.priority(); } }); ControllerInstrumentation.stopActionCall(); for (Method mCatch : catches) { Class[] exceptions = mCatch.getAnnotation(Catch.class).value(); if (exceptions.length == 0) { exceptions = new Class[]{Exception.class}; } for (Class exception : exceptions) { if (exception.isInstance(args[0])) { mCatch.setAccessible(true); inferResult(invokeControllerMethod(mCatch, args)); break; } } } throw ex; } } } // @After handleAfters(request); monitor.stop(); monitor = null; // OK, re-throw the original action result if (actionResult != null) { throw actionResult; } throw new NoResult(); } catch (IllegalAccessException ex) { throw ex; } catch (IllegalArgumentException ex) { throw ex; } catch (InvocationTargetException ex) { // It's a Result ? (expected) if (ex.getTargetException() instanceof Result) { throw (Result) ex.getTargetException(); } // Re-throw the enclosed exception if (ex.getTargetException() instanceof PlayException) { throw (PlayException) ex.getTargetException(); } StackTraceElement element = PlayException.getInterestingStrackTraceElement(ex.getTargetException()); if (element != null) { throw new JavaExecutionException(Play.classes.getApplicationClass(element.getClassName()), element.getLineNumber(), ex.getTargetException()); } throw new JavaExecutionException(Http.Request.current().action, ex); } } catch (Result result) { Play.pluginCollection.onActionInvocationResult(result); // OK there is a result to apply // Save session & flash scope now Scope.Session.current().save(); Scope.Flash.current().save(); result.apply(request, response); Play.pluginCollection.afterActionInvocation(); // @Finally handleFinallies(request, null); } catch (PlayException e) { handleFinallies(request, e); throw e; } catch (Throwable e) { handleFinallies(request, e); throw new UnexpectedException(e); } finally { if (monitor != null) { monitor.stop(); } } } private static boolean isActionMethod(Method method) { if (method.isAnnotationPresent(Before.class)) { return false; } if (method.isAnnotationPresent(After.class)) { return false; } if (method.isAnnotationPresent(Finally.class)) { return false; } if (method.isAnnotationPresent(Catch.class)) { return false; } if (method.isAnnotationPresent(Util.class)) { return false; } return true; } private static void handleBefores(Http.Request request) throws Exception { List<Method> befores = Java.findAllAnnotatedMethods(Controller.getControllerClass(), Before.class); Collections.sort(befores, new Comparator<Method>() { public int compare(Method m1, Method m2) { Before before1 = m1.getAnnotation(Before.class); Before before2 = m2.getAnnotation(Before.class); return before1.priority() - before2.priority(); } }); ControllerInstrumentation.stopActionCall(); for (Method before : befores) { String[] unless = before.getAnnotation(Before.class).unless(); String[] only = before.getAnnotation(Before.class).only(); boolean skip = false; for (String un : only) { if (!un.contains(".")) { un = before.getDeclaringClass().getName().substring(12).replace("$", "") + "." + un; } if (un.equals(request.action)) { skip = false; break; } else { skip = true; } } for (String un : unless) { if (!un.contains(".")) { un = before.getDeclaringClass().getName().substring(12).replace("$", "") + "." + un; } if (un.equals(request.action)) { skip = true; break; } } if (!skip) { before.setAccessible(true); inferResult(invokeControllerMethod(before)); } } } private static void handleAfters(Http.Request request) throws Exception { List<Method> afters = Java.findAllAnnotatedMethods(Controller.getControllerClass(), After.class); Collections.sort(afters, new Comparator<Method>() { public int compare(Method m1, Method m2) { After after1 = m1.getAnnotation(After.class); After after2 = m2.getAnnotation(After.class); return after1.priority() - after2.priority(); } }); ControllerInstrumentation.stopActionCall(); for (Method after : afters) { String[] unless = after.getAnnotation(After.class).unless(); String[] only = after.getAnnotation(After.class).only(); boolean skip = false; for (String un : only) { if (!un.contains(".")) { un = after.getDeclaringClass().getName().substring(12) + "." + un; } if (un.equals(request.action)) { skip = false; break; } else { skip = true; } } for (String un : unless) { if (!un.contains(".")) { un = after.getDeclaringClass().getName().substring(12) + "." + un; } if (un.equals(request.action)) { skip = true; break; } } if (!skip) { after.setAccessible(true); inferResult(invokeControllerMethod(after)); } } } /** * Checks and calla all methods in controller annotated with @Finally. * The caughtException-value is sent as argument to @Finally-method if method has one argument which is Throwable * @param request * @param caughtException If @Finally-methods are called after an error, this variable holds the caught error * @throws PlayException */ static void handleFinallies(Http.Request request, Throwable caughtException) throws PlayException { if (Controller.getControllerClass() == null) { //skip it return; } try { List<Method> allFinally = Java.findAllAnnotatedMethods(Controller.getControllerClass(), Finally.class); Collections.sort(allFinally, new Comparator<Method>() { public int compare(Method m1, Method m2) { Finally finally1 = m1.getAnnotation(Finally.class); Finally finally2 = m2.getAnnotation(Finally.class); return finally1.priority() - finally2.priority(); } }); ControllerInstrumentation.stopActionCall(); for (Method aFinally : allFinally) { String[] unless = aFinally.getAnnotation(Finally.class).unless(); String[] only = aFinally.getAnnotation(Finally.class).only(); boolean skip = false; for (String un : only) { if (!un.contains(".")) { un = aFinally.getDeclaringClass().getName().substring(12) + "." + un; } if (un.equals(request.action)) { skip = false; break; } else { skip = true; } } for (String un : unless) { if (!un.contains(".")) { un = aFinally.getDeclaringClass().getName().substring(12) + "." + un; } if (un.equals(request.action)) { skip = true; break; } } if (!skip) { aFinally.setAccessible(true); //check if method accepts Throwable as only parameter Class[] parameterTypes = aFinally.getParameterTypes(); if (parameterTypes.length == 1 && parameterTypes[0] == Throwable.class) { //invoking @Finally method with caughtException as parameter invokeControllerMethod(aFinally, new Object[]{caughtException}); } else { //invoce @Finally-method the regular way without caughtException invokeControllerMethod(aFinally, null); } } } } catch (InvocationTargetException ex) { StackTraceElement element = PlayException.getInterestingStrackTraceElement(ex.getTargetException()); if (element != null) { throw new JavaExecutionException(Play.classes.getApplicationClass(element.getClassName()), element.getLineNumber(), ex.getTargetException()); } throw new JavaExecutionException(Http.Request.current().action, ex); } catch (Exception e) { throw new UnexpectedException("Exception while doing @Finally", e); } } @SuppressWarnings("unchecked") public static void inferResult(Object o) { // Return type inference if (o != null) { if (o instanceof NoResult) { return; } if (o instanceof Result) { // Of course throw (Result) o; } if (o instanceof InputStream) { Controller.renderBinary((InputStream) o); } if (o instanceof File) { Controller.renderBinary((File) o); } if (o instanceof Map) { Controller.renderTemplate((Map<String, Object>) o); } if (o instanceof Object[]) { Controller.render(o); } Controller.renderHtml(o); } } public static Object invokeControllerMethod(Method method) throws Exception { return invokeControllerMethod(method, null); } public static Object invokeControllerMethod(Method method, Object[] forceArgs) throws Exception { if (Modifier.isStatic(method.getModifiers()) && !method.getDeclaringClass().getName().matches("^controllers\\..*\\$class$")) { return invoke(method, null, forceArgs == null ? getActionMethodArgs(method, null) : forceArgs); } else if (Modifier.isStatic(method.getModifiers())) { Object[] args = getActionMethodArgs(method, null); args[0] = Http.Request.current().controllerClass.getDeclaredField("MODULE$").get(null); return invoke(method, null, args); } else { Object instance = null; try { instance = method.getDeclaringClass().getDeclaredField("MODULE$").get(null); } catch (Exception e) { Annotation[] annotations = method.getDeclaredAnnotations(); String annotation = Utils.getSimpleNames(annotations); if (!StringUtils.isEmpty(annotation)) { throw new UnexpectedException("Method public static void " + method.getName() + "() annotated with " + annotation + " in class " + method.getDeclaringClass().getName() + " is not static."); } // TODO: Find a better error report throw new ActionNotFoundException(Http.Request.current().action, e); } return invoke(method, instance, forceArgs == null ? getActionMethodArgs(method, instance) : forceArgs); } } static Object invoke(Method method, Object instance, Object[] realArgs) throws Exception { if(isActionMethod(method)) { return invokeWithContinuation(method, instance, realArgs); } else { return method.invoke(instance, realArgs); } } static final String C = "__continuation"; static final String A = "__callback"; static final String F = "__future"; static final String CONTINUATIONS_STORE_LOCAL_VARIABLE_NAMES = "__CONTINUATIONS_STORE_LOCAL_VARIABLE_NAMES"; static final String CONTINUATIONS_STORE_RENDER_ARGS = "__CONTINUATIONS_STORE_RENDER_ARGS"; static Object invokeWithContinuation(Method method, Object instance, Object[] realArgs) throws Exception { // Callback case if (Http.Request.current().args.containsKey(A)) { // Action0 instance = Http.Request.current().args.get(A); Future f = (Future) Http.Request.current().args.get(F); Scope.RenderArgs renderArgs = (Scope.RenderArgs) Request.current().args.remove(ActionInvoker.CONTINUATIONS_STORE_RENDER_ARGS); Scope.RenderArgs.current.set(renderArgs); if (f == null) { method = instance.getClass().getDeclaredMethod("invoke"); method.setAccessible(true); return method.invoke(instance); } else { method = instance.getClass().getDeclaredMethod("invoke", Object.class); method.setAccessible(true); return method.invoke(instance, f.get()); } } // Continuations case Continuation continuation = (Continuation) Http.Request.current().args.get(C); if (continuation == null) { continuation = new Continuation(new StackRecorder((Runnable) null)); } StackRecorder pStackRecorder = new StackRecorder(continuation.stackRecorder); Object result = null; final StackRecorder old = pStackRecorder.registerThread(); try { pStackRecorder.isRestoring = !pStackRecorder.isEmpty(); // Execute code result = method.invoke(instance, realArgs); if (pStackRecorder.isCapturing) { if (pStackRecorder.isEmpty()) { throw new IllegalStateException("stack corruption. Is " + method + " instrumented for javaflow?"); } Object trigger = pStackRecorder.value; Continuation nextContinuation = new Continuation(pStackRecorder); Http.Request.current().args.put(C, nextContinuation); if (trigger instanceof Long) { throw new Suspend((Long) trigger); } if (trigger instanceof Integer) { throw new Suspend(((Integer) trigger).longValue()); } if (trigger instanceof Future) { throw new Suspend((Future) trigger); } throw new UnexpectedException("Unexpected continuation trigger -> " + trigger); } else { Http.Request.current().args.remove(C); } } finally { pStackRecorder.deregisterThread(old); } return result; } public static Object[] getActionMethod(String fullAction) { Method actionMethod = null; Class controllerClass = null; try { if (!fullAction.startsWith("controllers.")) { fullAction = "controllers." + fullAction; } String controller = fullAction.substring(0, fullAction.lastIndexOf(".")); String action = fullAction.substring(fullAction.lastIndexOf(".") + 1); controllerClass = Play.classloader.getClassIgnoreCase(controller); if (controllerClass == null) { throw new ActionNotFoundException(fullAction, new Exception("Controller " + controller + " not found")); } if (!ControllerSupport.class.isAssignableFrom(controllerClass)) { // Try the scala way controllerClass = Play.classloader.getClassIgnoreCase(controller + "$"); if (!ControllerSupport.class.isAssignableFrom(controllerClass)) { throw new ActionNotFoundException(fullAction, new Exception("class " + controller + " does not extend play.mvc.Controller")); } } actionMethod = Java.findActionMethod(action, controllerClass); if (actionMethod == null) { throw new ActionNotFoundException(fullAction, new Exception("No method public static void " + action + "() was found in class " + controller)); } } catch (PlayException e) { throw e; } catch (Exception e) { throw new ActionNotFoundException(fullAction, e); } return new Object[]{controllerClass, actionMethod}; } public static Object[] getActionMethodArgs(Method method, Object o) throws Exception { String[] paramsNames = Java.parameterNames(method); if (paramsNames == null && method.getParameterTypes().length > 0) { throw new UnexpectedException("Parameter names not found for method " + method); } // Check if we have already performed the bind operation Object[] rArgs = CachedBoundActionMethodArgs.current().retrieveActionMethodArgs(method); if ( rArgs != null) { // We have already performed the binding-operation for this method // in this request. return rArgs; } rArgs = new Object[method.getParameterTypes().length]; for (int i = 0; i < method.getParameterTypes().length; i++) { Class<?> type = method.getParameterTypes()[i]; Map<String, String[]> params = new HashMap<String, String[]> (); // In case of simple params, we don't want to parse the body. if (type.equals(String.class) || Number.class.isAssignableFrom(type) || type.isPrimitive()) { params.put(paramsNames[i], Scope.Params.current().getAll(paramsNames[i])); } else { params.putAll(Scope.Params.current().all()); } Logger.trace("getActionMethodArgs name [" + paramsNames[i] + "] annotation [" + Utils.join(method.getParameterAnnotations()[i], " ") + "]"); RootParamNode root = ParamNode.convert(params); rArgs[i] = Binder.bind( root, paramsNames[i], method.getParameterTypes()[i], method.getGenericParameterTypes()[i], method.getParameterAnnotations()[i], new Binder.MethodAndParamInfo(o, method, i + 1)); } CachedBoundActionMethodArgs.current().storeActionMethodArgs(method, rArgs); return rArgs; } }