All Downloads are FREE. Search and download functionalities are using the official Maven repository.

play.mvc.ActionInvoker Maven / Gradle / Ivy

There is a newer version: 2.6.2
Show newest version
package play.mvc;

import jakarta.inject.Inject;
import java.io.File;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.Play;
import play.cache.Cache;
import play.cache.CacheFor;
import play.data.binding.Binder;
import play.data.binding.CachedBoundActionMethodArgs;
import play.data.binding.ParamNode;
import play.data.binding.RootParamNode;
import play.data.validation.Validation;
import play.exceptions.ActionNotFoundException;
import play.exceptions.PlayException;
import play.exceptions.UnexpectedException;
import play.inject.Injector;
import play.mvc.Scope.Flash;
import play.mvc.Scope.RenderArgs;
import play.mvc.Scope.Session;
import play.mvc.results.NoResult;
import play.mvc.results.NotFound;
import play.mvc.results.RenderBinary;
import play.mvc.results.RenderHtml;
import play.mvc.results.Result;
import play.utils.Java;
import play.utils.Utils;

/** Invoke an action after an HTTP request. */
public class ActionInvoker {
  private static final Logger logger = LoggerFactory.getLogger(ActionInvoker.class);

  private final SessionStore sessionStore;
  private final FlashStore flashStore = new FlashStore();

  @Inject
  public ActionInvoker(SessionStore sessionStore) {
    this.sessionStore = sessionStore;
  }

  @SuppressWarnings("unchecked")
  public static void resolve(Http.Request request) {

    if (!Play.started) {
      return;
    }

    if (request.resolved) {
      return;
    }

    // Route and resolve format if not already done
    if (request.action == null) {
      Play.pluginCollection.routeRequest(request);
      Router.instance.route(request);
    }
    request.resolveFormat();

    // Find the action method
    try {
      Object[] ca = getActionMethod(request.action);
      Method 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;

      logger.trace("------- {}", actionMethod);

      request.resolved = true;

    } catch (ActionNotFoundException e) {
      logger.error(e.getMessage(), e);
      throw new NotFound(String.format("%s action not found", e.getAction()));
    }
  }

  private static void initActionContext(ActionContext context) {
    Http.Request.setCurrent(context.request);
    Http.Response.setCurrent(context.response);
    RenderArgs.current.set(context.renderArgs);
    CachedBoundActionMethodArgs.init();
  }

  public void invoke(Http.Request request, Http.Response response) {
    Session session =
        actionNeedsSession(request) ? sessionStore.restore(request) : new ReadonlySession();
    Flash flash = flashStore.restore(request);
    RenderArgs renderArgs = new RenderArgs();
    ActionContext context =
        new ActionContext(request, response, session, flash, renderArgs, Validation.current());
    initActionContext(context);

    if (!Modifier.isStatic(request.invokedMethod.getModifiers())) {
      request.controllerInstance = createController(context);
    }

    try {
      Method actionMethod = request.invokedMethod;

      Play.pluginCollection.beforeActionInvocation(
          request, response, session, renderArgs, flash, actionMethod);

      String cacheKey = null;
      Result actionResult = null;

      // 3. Invoke the action
      try {
        // @Before
        handleBefores(request, session);

        // Action

        // 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 (cacheKey != null && cacheKey.isEmpty()) {
            cacheKey = "urlcache:" + request.path + '?' + request.querystring;
          }
          actionResult = Cache.get(cacheKey);
        }

        if (actionResult == null) {
          inferResult(invokeControllerMethod(request, session, actionMethod));
        }
      } catch (Result result) {
        actionResult = result;
        // Cache it if needed
        if (cacheKey != null) {
          Cache.set(cacheKey, actionResult, actionMethod.getAnnotation(CacheFor.class).value());
        }
      } catch (Exception e) {
        invokeControllerCatchMethods(request, session, e);
        throw e;
      }

      // @After
      handleAfters(request, session);

      // OK, re-throw the original action result
      if (actionResult != null) {
        throw actionResult;
      }

      throw new NoResult();

    } catch (Result result) {
      applyResult(request, response, session, flash, renderArgs, result);
    } catch (RuntimeException e) {
      handleFinallies(request, session, e);
      throw e;
    } catch (Throwable e) {
      handleFinallies(request, session, e);
      throw new UnexpectedException(e);
    }
  }

  boolean actionNeedsSession(Http.Request request) {
    return !request.invokedMethod.isAnnotationPresent(NoSession.class);
  }

  private PlayController createController(ActionContext context) {
    PlayController controller = Injector.getBeanOfType(context.request.controllerClass);
    if (controller instanceof PlayContextController) {
      ((PlayContextController) controller).setContext(context);
    }
    return controller;
  }

  private void applyResult(
      Http.Request request,
      Http.Response response,
      Session session,
      Flash flash,
      RenderArgs renderArgs,
      Result result) {
    Play.pluginCollection.onActionInvocationResult(
        request, response, session, flash, renderArgs, result);

    try {
      result.apply(request, response, session, renderArgs, flash);
    } catch (Result anotherResult) {
      if (result == anotherResult) {
        // avoid endless recursion
        throw new IllegalArgumentException("result is rethrown: " + anotherResult);
      } else {
        // There is a weird ExcelPlugin that throws RenderExcel from inside ViewResult.apply().
        // In this case, we need to call RenderExcel.apply()
        applyResult(request, response, session, flash, renderArgs, anotherResult);
      }
    }

    Play.pluginCollection.afterActionInvocation(request, response, session, flash);

    // It's important to send "flash" and "session" cookies to browser AFTER html is applied.
    // Because sometimes html does change flash.
    // For example, some html might execute %{flash.discard('info')}%`
    if (actionNeedsSession(request)) {
      sessionStore.save(session, request, response);
    }
    flashStore.save(flash, request, response);

    handleFinallies(request, session, null);
  }

  private static void invokeControllerCatchMethods(
      Http.Request request, Session session, Throwable throwable) throws Exception {
    // @Catch
    Object[] args = new Object[] {throwable};
    List catches = Java.findAllAnnotatedMethods(request.controllerClass, Catch.class);
    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(request, session, mCatch, args));
          break;
        }
      }
    }
  }

  private static boolean isActionMethod(Method method) {
    return !method.isAnnotationPresent(Before.class)
        && !method.isAnnotationPresent(After.class)
        && !method.isAnnotationPresent(Finally.class)
        && !method.isAnnotationPresent(Catch.class)
        && !method.isAnnotationPresent(Util.class);
  }

  /**
   * Find the first public method of a controller class
   *
   * @param name The method name
   * @param clazz The class
   * @return The method or null
   */
  public static Method findActionMethod(String name, Class clazz) {
    while (!"java.lang.Object".equals(clazz.getName())) {
      for (Method m : clazz.getDeclaredMethods()) {
        if (m.getName().equalsIgnoreCase(name) && Modifier.isPublic(m.getModifiers())) {
          // Check that it is not an interceptor
          if (isActionMethod(m)) {
            return m;
          }
        }
      }
      clazz = clazz.getSuperclass();
    }
    return null;
  }

  private static void handleBefores(Http.Request request, Session session) throws Exception {
    List befores = Java.findAllAnnotatedMethods(request.controllerClass, Before.class);
    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(request, session, before));
      }
    }
  }

  private static void handleAfters(Http.Request request, Session session) throws Exception {
    List afters = Java.findAllAnnotatedMethods(request.controllerClass, After.class);
    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(request, session, 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 caughtException If @Finally-methods are called after an error, this variable holds the
   *     caught error
   */
  static void handleFinallies(Http.Request request, Session session, Throwable caughtException)
      throws PlayException {

    if (request.controllerClass == null) {
      // skip it
      return;
    }

    try {
      List allFinally =
          Java.findAllAnnotatedMethods(request.controllerClass, Finally.class);
      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(request, session, aFinally, new Object[] {caughtException});
          } else {
            // invoke @Finally-method the regular way without
            // caughtException
            invokeControllerMethod(request, session, aFinally, null);
          }
        }
      }
    } catch (PlayException e) {
      throw e;
    } catch (Exception e) {
      throw new UnexpectedException("Exception while doing @Finally", e);
    }
  }

  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) {
        throw new RenderBinary((InputStream) o, null, true);
      }
      if (o instanceof File) {
        throw new RenderBinary((File) o);
      }
      if (o instanceof Map) {
        throw new UnsupportedOperationException("Controller action cannot return Map");
      }
      throw new RenderHtml(o.toString());
    }
  }

  static Object invokeControllerMethod(Http.Request request, Session session, Method method)
      throws Exception {
    return invokeControllerMethod(request, session, method, null);
  }

  static Object invokeControllerMethod(
      Http.Request request, Session session, Method method, Object[] forceArgs) throws Exception {
    boolean isStatic = Modifier.isStatic(method.getModifiers());

    Object[] args = forceArgs != null ? forceArgs : getActionMethodArgs(request, session, method);

    Object methodClassInstance =
        isStatic
            ? null
            : (method.getDeclaringClass().isAssignableFrom(request.controllerClass))
                ? request.controllerInstance
                : Injector.getBeanOfType(method.getDeclaringClass());

    return invoke(method, methodClassInstance, args);
  }

  static Object invoke(Method method, Object instance, Object... realArgs) throws Exception {
    try {
      return method.invoke(instance, realArgs);
    } catch (InvocationTargetException ex) {
      Throwable originalThrowable = ex.getTargetException();

      if (originalThrowable instanceof RuntimeException) throw (RuntimeException) originalThrowable;
      if (originalThrowable instanceof Exception) throw (Exception) originalThrowable;
      if (originalThrowable instanceof Error) throw (Error) originalThrowable;

      throw new RuntimeException(originalThrowable);
    }
  }

  public static Object[] getActionMethod(String fullAction) {
    Method actionMethod;
    Class controllerClass;
    try {
      if (!fullAction.startsWith("controllers.")) {
        fullAction = "controllers." + fullAction;
      }
      String controller = fullAction.substring(0, fullAction.lastIndexOf('.'));
      String action = fullAction.substring(fullAction.lastIndexOf('.') + 1);
      controllerClass = Play.classes.getClassIgnoreCase(controller);
      if (controllerClass == null) {
        throw new ActionNotFoundException(
            fullAction, new Exception("Controller " + controller + " not found"));
      }
      actionMethod = 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(Http.Request request, Session session, Method method) {
    String[] paramsNames = Java.parameterNames(method);

    // Help newcomers understand what went wrong when their arguments are not mapped.
    if (paramsNames.length > 0 && paramsNames[0].length() > 3) {
      char firstArgNumDigit = paramsNames[0].charAt(3);
      if (paramsNames[0].startsWith("arg") && firstArgNumDigit <='9' && firstArgNumDigit >= '0') {
        logger.warn("It seems you did not compile with the '-parameters' flag.");
      }
    }

    // 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 params = new HashMap<>();

      // 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], request.params.getAll(paramsNames[i]));
      } else {
        params.putAll(request.params.all());
      }
      if (logger.isTraceEnabled()) {
        logger.trace(
            "getActionMethodArgs name [{}] annotation [{}]",
            paramsNames[i],
            Utils.join(method.getParameterAnnotations()[i], " "));
      }

      RootParamNode root = ParamNode.convert(params);
      rArgs[i] =
          Binder.bind(
              request,
              session,
              root,
              paramsNames[i],
              method.getParameterTypes()[i],
              method.getGenericParameterTypes()[i],
              method.getParameterAnnotations()[i]);
    }

    CachedBoundActionMethodArgs.current().storeActionMethodArgs(method, rArgs);
    return rArgs;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy