content: package act.handler.builtin.controller.impl;
  
  /*-
   * #%L
   * ACT Framework
   * %%
   * Copyright (C) 2014 - 2017 ActFramework
   * %%
   * 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.
   * #L%
   */
  
  import static act.app.ActionContext.State.HANDLING;
  import static act.app.ActionContext.State.INTERCEPTING;
  import static org.osgl.util.Generics.buildTypeParamImplLookup;
  
  import act.Act;
  import act.Trace;
  import act.annotations.*;
  import act.apidoc.SampleData;
  import act.app.*;
  import act.cli.ReportProgress;
  import act.conf.AppConfig;
  import act.controller.*;
  import act.controller.annotation.*;
  import act.controller.builtin.ThrottleFilter;
  import act.controller.captcha.RequireCaptcha;
  import act.controller.meta.*;
  import act.data.annotation.DateFormatPattern;
  import act.data.annotation.Pattern;
  import act.db.RequireDataBind;
  import act.handler.*;
  import act.handler.builtin.controller.*;
  import act.handler.event.ReflectedHandlerInvokerInit;
  import act.handler.event.ReflectedHandlerInvokerInvoke;
  import act.inject.DependencyInjector;
  import act.inject.SessionVariable;
  import act.inject.param.*;
  import act.job.Job;
  import act.job.JobManager;
  import act.job.TrackableWorker;
  import act.plugin.ControllerPlugin;
  import act.security.*;
  import act.sys.Env;
  import act.util.*;
  import act.util.Output;
  import act.util.ProgressGauge;
  import act.view.*;
  import act.ws.WebSocketConnectionManager;
  import com.alibaba.fastjson.*;
  import com.alibaba.fastjson.annotation.JSONField;
  import com.alibaba.fastjson.serializer.SerializeFilter;
  import com.alibaba.fastjson.serializer.SerializerFeature;
  import com.esotericsoftware.reflectasm.MethodAccess;
  import org.osgl.$;
  import org.osgl.Lang;
  import org.osgl.exception.NotAppliedException;
  import org.osgl.exception.ToBeImplemented;
  import org.osgl.http.H;
  import org.osgl.inject.BeanSpec;
  import org.osgl.mvc.annotation.*;
  import org.osgl.mvc.result.*;
  import org.osgl.util.*;
  import org.w3c.dom.Document;
  
  import java.lang.annotation.Annotation;
  import java.lang.reflect.*;
  import java.util.*;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.concurrent.ConcurrentMap;
  import java.util.concurrent.TimeUnit;
  import javax.enterprise.context.ApplicationScoped;
  import javax.validation.ConstraintViolation;
  
  /**
   * Implement handler using
   * https://github.com/EsotericSoftware/reflectasm
   */
  @ApplicationScoped
  public class ReflectedHandlerInvoker<M extends HandlerMethodMetaInfo> extends LogSupportedDestroyableBase
          implements ActionHandlerInvoker, AfterInterceptorInvoker, ExceptionInterceptorInvoker {
  
      private static final Object[] DUMP_PARAMS = new Object[0];
      private boolean mock;
      private App app;
      private AppConfig config;
      private ControllerClassMetaInfo controller;
      private Class<?> controllerClass;
      private MethodAccess methodAccess;
      private M handler;
      private int handlerIndex;
      private ConcurrentMap<H.Format, Boolean> templateAvailabilityCache = new ConcurrentHashMap<>();
      private $.Visitor<H.Format> templateChangeListener = new $.Visitor<H.Format>() {
          @Override
          public void visit(H.Format format) throws $.Break {
              templateAvailabilityCache.remove(format);
          }
      };
      protected Method method; //
      private ParamValueLoaderService paramLoaderService;
      private JsonDtoClassManager jsonDTOClassManager;
      private int paramCount;
      private int fieldsAndParamsCount;
      private int sessionVariablesCount;
      private String singleJsonFieldName;
      private boolean sessionFree;
      private boolean express;
      private boolean skipEvents;
      private List<BeanSpec> paramSpecs;
      private CORS.Spec corsSpec;
      private CSRF.Spec csrfSpec;
      private String csp;
      private boolean disableCsp;
      private boolean isStatic;
      private boolean requireCaptcha;
      private Object singleton;
      private H.Format forceResponseContentType;
      private H.Status forceResponseStatus;
      // Env doesn't match
      private boolean disabled;
      private String dspToken;
      private CacheSupportMetaInfo cacheSupport;
      // (field name: output name)
      private Map<Field, String> outputFields;
      // (param index: output name)
      private Map<Integer, String> outputParams;
      private boolean hasOutputVar;
      private String dateFormatPattern;
      private boolean noTemplateCache;
      private MissingAuthenticationHandler missingAuthenticationHandler;
      private MissingAuthenticationHandler csrfFailureHandler;
      private ThrottleFilter throttleFilter;
      private boolean async;
      private boolean reportAsyncProgress;
      private boolean byPassImplicityTemplateVariable;
      private boolean forceDataBinding;
      private boolean isLargeResponse;
      private boolean forceSmallResponse;
      private Class<? extends SerializeFilter> filters[];
      private SerializerFeature features[];
      private PropertyNamingStrategy propertyNamingStrategy;
      private $.Function<ActionContext, Result> pluginBeforeHandler;
      private $.Func2<Result, ActionContext, Result> pluginAfterHandler;
      private Map<String, Object> attributes = new HashMap<>();
      private boolean enableCircularReferenceDetect;
      // it shall do full JSON string check when Accept is JSON and return type is String
      // however if the first full check is good, then the following check shall rely on quick check
      private boolean returnString;
      private boolean fullJsonStringChecked;
      private boolean fullJsonStringCheckFailure;
      // See https://github.com/actframework/actframework/issues/797
      private boolean suppressJsonDateFormat;
      // see https://github.com/actframework/actframework/issues/829
      private String downloadFilename;
      // see https://github.com/actframework/actframework/issues/835
      private ReturnValueAdvice returnValueAdvice;
      // see https://github.com/actframework/actframework/issues/852
      private boolean returnIterable;
      // see https://github.com/actframework/actframework/issues/922
      private ValidateViolationAdvice validateViolationAdvice;
      private boolean returnSimpleType;
      private boolean returnIterableComponentIsSimpleType;
      private boolean shallTransformReturnVal;
      private int order;
      private String xmlRootTag;
      private List<JsonDtoPatch> dtoPatches = new ArrayList<>();
      private boolean hasDtoPatches;
      private Class<?> returnType;
      // see https://github.com/actframework/actframework/issues/1269
      private boolean allowQrCodeRendering;
  
      private ReflectedHandlerInvoker(M handlerMetaInfo, App app) {
          this.app = app;
          this.config = app.config();
          this.mock = config.mockServer();
          this.handler = handlerMetaInfo;
          this.controller = handlerMetaInfo.classInfo();
          this.controllerClass = app.classForName(controller.className());
          this.disabled = !Env.matches(controllerClass);
          this.paramLoaderService = app.service(ParamValueLoaderManager.class).get(ActionContext.class);
          this.jsonDTOClassManager = app.service(JsonDtoClassManager.class);
          this.xmlRootTag = config.xmlRootTag();
  
          Class[] paramTypes = paramTypes(app);
          method = $.getMethod(controllerClass, handlerMetaInfo.name(), paramTypes);
          E.unexpectedIf(null == method, "Unable to locate handler method: " + handlerMetaInfo.name());
          this.returnType = Generics.getReturnType(method, controllerClass);
          this.returnString = this.returnType == String.class;
          Integer priority = handler.priority();
          if (null != priority) {
              this.order = priority;
          } else {
              Order order = ReflectedInvokerHelper.getAnnotation(Order.class, method);
              this.order = null == order ? Order.HIGHEST_PRECEDENCE : order.value();
          }
          final boolean isBuiltIn = controllerClass.getName().startsWith("act.");
          if (handlerMetaInfo.hasReturn() && !isBuiltIn) {
              this.returnSimpleType = this.returnString || $.isSimpleType(returnType);
              this.returnIterable = !this.returnSimpleType && (handlerMetaInfo.isReturnArray() || Iterable.class.isAssignableFrom(returnType));
              if (this.returnIterable) {
                  if (handlerMetaInfo.isReturnArray()) {
                      this.returnIterableComponentIsSimpleType = $.isSimpleType(method.getReturnType().getComponentType());
                  } else {
                      Type type = method.getGenericReturnType();
                      if (type instanceof ParameterizedType) {
                          ParameterizedType ptype = $.cast(type);
                          Type[] typeParams = ptype.getActualTypeArguments();
                          if (typeParams.length > 1) {
                              this.returnIterableComponentIsSimpleType = false;
                          } else if (typeParams.length == 1) {
                              Type p0 = typeParams[0];
                              if (p0 instanceof Class) {
                                  this.returnIterableComponentIsSimpleType = $.isSimpleType((Class) p0);
                              } else {
                                  this.returnIterableComponentIsSimpleType = false;
                              }
                          }
                      } else {
                          warn("Cannot determine component type of handler method return type: " + method);
                          this.returnIterableComponentIsSimpleType = false;
                      }
                  }
              }
              this.shallTransformReturnVal = handlerMetaInfo.hasReturn() && (returnIterable && !returnIterableComponentIsSimpleType) || (!returnSimpleType);
          }
          this.pluginBeforeHandler = ControllerPlugin.Manager.INST.beforeHandler(controllerClass, method);
          this.pluginAfterHandler = ControllerPlugin.Manager.INST.afterHandler(controllerClass, method);
          this.disabled = this.disabled || !Env.matches(method);
          this.forceDataBinding = ReflectedInvokerHelper.isAnnotationPresent(RequireDataBind.class, method);
          this.async = null != ReflectedInvokerHelper.getAnnotation(Async.class, method);
          this.reportAsyncProgress = null != ReflectedInvokerHelper.getAnnotation(ReportProgress.class, method);
          this.isStatic = handlerMetaInfo.isStatic();
          if (!this.isStatic) {
              methodAccess = MethodAccess.get(controllerClass);
              handlerIndex = methodAccess.getIndex(handlerMetaInfo.name(), paramTypes);
          } else {
              method.setAccessible(true);
          }
          this.allowQrCodeRendering = ReflectedInvokerHelper.isAnnotationPresent(AllowQrCodeRendering.class, method);
  
          if (!isBuiltIn && handlerMetaInfo.hasReturn() && null == ReflectedInvokerHelper.getAnnotation(NoReturnValueAdvice.class, method)) {
              ReturnValueAdvisor advisor = getAnnotation(ReturnValueAdvisor.class);
              if (null != advisor) {
                  returnValueAdvice = app.getInstance(advisor.value());
              } else if (null == getAnnotation(NoReturnValueAdvice.class)) {
                  returnValueAdvice = app.config().globalReturnValueAdvice();
              }
          }
  
          ValidateViolationAdvisor vAdvisor = getAnnotation(ValidateViolationAdvisor.class);
          if (null != vAdvisor) {
              validateViolationAdvice = app.getInstance(vAdvisor.value());
          } else if (null == getAnnotation(NoValidateViolationAdvice.class)) {
              validateViolationAdvice = app.config().globalValidateViolationAdvice();
          }
  
          if (method.isAnnotationPresent(RequireCaptcha.class)) {
              this.requireCaptcha = true;
          }
  
          this.suppressJsonDateFormat = shouldSuppressJsonDateFormat();
  
          Throttled throttleControl = ReflectedInvokerHelper.getAnnotation(Throttled.class, method);
          if (null != throttleControl) {
              int throttle = throttleControl.value();
              if (throttle < 1) {
                  throttle = app.config().requestThrottle();
              }
              Throttled.ExpireScale expireScale = throttleControl.expireScale();
              throttleFilter = new ThrottleFilter(throttle, expireScale.enabled());
          }
  
          this.isLargeResponse = ReflectedInvokerHelper.getAnnotation(LargeResponse.class, method) != null;
          this.forceSmallResponse = ReflectedInvokerHelper.getAnnotation(SmallResponse.class, method) != null;
          if (isLargeResponse && forceSmallResponse) {
              warn("found both @LargeResponse and @SmallResponse, will ignore @SmallResponse");
              forceSmallResponse = false;
          }
  
          FastJsonFilter filterAnno = ReflectedInvokerHelper.getAnnotation(FastJsonFilter.class, method);
          if (null != filterAnno) {
              filters = filterAnno.value();
          }
          FastJsonFeature featureAnno = ReflectedInvokerHelper.getAnnotation(FastJsonFeature.class, method);
          if (null != featureAnno) {
              features = featureAnno.value();
          }
          FastJsonPropertyNamingStrategy propertyNamingStrategyAnno = ReflectedInvokerHelper.getAnnotation(FastJsonPropertyNamingStrategy.class, method);
          if (null != propertyNamingStrategyAnno) {
              propertyNamingStrategy = propertyNamingStrategyAnno.value();
          }
          enableCircularReferenceDetect = hasAnnotation(EnableCircularReferenceDetect.class);
  
          DateFormatPattern pattern = ReflectedInvokerHelper.getAnnotation(DateFormatPattern.class, method);
          if (null != pattern) {
              this.dateFormatPattern = pattern.value();
          } else {
              Pattern patternLegacy = ReflectedInvokerHelper.getAnnotation(Pattern.class, method);
              if (null != patternLegacy) {
                  this.dateFormatPattern = patternLegacy.value();
              }
          }
          if (null != this.dateFormatPattern) {
              handlerMetaInfo.dateFormatPattern(this.dateFormatPattern);
          }
  
          if (controllerClass.isAnnotationPresent(ExpressController.class)) {
              sessionFree = true;
              express = true;
              skipEvents = true;
          } else {
              sessionFree = hasAnnotation(SessionFree.class);
              express = hasAnnotation(NonBlock.class);
              skipEvents = hasAnnotation(SkipBuiltInEvents.class);
          }
          noTemplateCache = method.isAnnotationPresent(Template.NoCache.class);
  
          paramCount = handler.paramCount();
          paramSpecs = jsonDTOClassManager.beanSpecs(controllerClass, method);
          for (BeanSpec spec : paramSpecs) {
              JsonDtoPatch patch = JsonDtoPatch.of(spec);
              if (null != patch) {
                  dtoPatches.add(patch);
              }
          }
          hasDtoPatches = !dtoPatches.isEmpty();
          List<BeanSpec> paramSpecWithoutSessionVariables = new ArrayList<>();
          fieldsAndParamsCount = paramSpecs.size();
          for (BeanSpec spec : paramSpecs) {
              if (spec.hasAnnotation(SessionVariable.class)) {
                  sessionVariablesCount++;
              } else {
                  paramSpecWithoutSessionVariables.add(spec);
              }
          }
          if (1 == (fieldsAndParamsCount - sessionVariablesCount)) {
              singleJsonFieldName = paramSpecWithoutSessionVariables.get(0).name();
          }
  
          CORS.Spec corsSpec = CORS.spec(method).chain(CORS.spec(controllerClass));
          this.corsSpec = corsSpec;
  
          CSRF.Spec csrfSpec = CSRF.spec(method).chain(CSRF.spec(controllerClass));
          this.csrfSpec = csrfSpec;
  
          CSP.Disable cspDisableAnno = getAnnotation(CSP.Disable.class);
          if (null != cspDisableAnno) {
              this.disableCsp = true;
          }
  
          if (!this.disableCsp) {
              CSP cspAnno = getAnnotation(CSP.class);
              if (null != cspAnno) {
                  this.csp = cspAnno.value();
              }
          }
  
          if (!isStatic) {
              this.singleton = ReflectedInvokerHelper.tryGetSingleton(controllerClass, app);
          }
  
          if (null != controllerClass.getAnnotation(JsonView.class)) {
              forceResponseContentType = H.MediaType.JSON.format();
          }
          if (null != controllerClass.getAnnotation(CsvView.class)) {
              forceResponseContentType = H.MediaType.CSV.format();
          }
          // ResponseContentType takes priority of JsonView
          ResponseContentType contentType = controllerClass.getAnnotation(ResponseContentType.class);
          if (null != contentType) {
              String s = contentType.contentType();
              if (S.notBlank(s)) {
                  forceResponseContentType = H.Format.resolve(s);
              }
              if (null == forceResponseContentType || forceResponseContentType == H.Format.UNKNOWN) {
                  forceResponseContentType = contentType.mediaType().format();
              }
              if (null == forceResponseContentType || forceResponseContentType == H.Format.UNKNOWN) {
                  forceResponseContentType = contentType.value().format();
              }
          }
  
          DownloadFilename downloadFilename = ReflectedInvokerHelper.getAnnotation(DownloadFilename.class, method);
          if (null != downloadFilename) {
              this.downloadFilename = downloadFilename.value();
          }
  
          // method annotation takes priority of class annotation
          if (null != ReflectedInvokerHelper.getAnnotation(JsonView.class, method)) {
              forceResponseContentType = H.MediaType.JSON.format();
          }
          if (null != ReflectedInvokerHelper.getAnnotation(CsvView.class, method)) {
              forceResponseContentType = H.MediaType.CSV.format();
          }
          contentType = ReflectedInvokerHelper.getAnnotation(ResponseContentType.class, method);
          if (null != contentType) {
              forceResponseContentType = null;
              String s = contentType.contentType();
              if (S.notBlank(s)) {
                  forceResponseContentType = H.Format.resolve(s);
              }
              if (null == forceResponseContentType || forceResponseContentType == H.Format.UNKNOWN) {
                  forceResponseContentType = contentType.mediaType().format();
              }
              if (null == forceResponseContentType || forceResponseContentType == H.Format.UNKNOWN) {
                  forceResponseContentType = contentType.value().format();
              }
          }
  
          ResponseStatus status = ReflectedInvokerHelper.getAnnotation(ResponseStatus.class, method);
          if (null != status) {
              forceResponseStatus = H.Status.of(status.value());
          }
  
          PreventDoubleSubmission dsp = ReflectedInvokerHelper.getAnnotation(PreventDoubleSubmission.class, method);
          if (null != dsp) {
              dspToken = dsp.value();
              if (PreventDoubleSubmission.DEFAULT.equals(dspToken)) {
                  dspToken = app.config().dspToken();
              }
          }
  
          byPassImplicityTemplateVariable = (controllerClass.isAnnotationPresent(NoImplicitTemplateVariable.class) ||
                  ReflectedInvokerHelper.isAnnotationPresent(NoImplicitTemplateVariable.class, method));
  
          initOutputVariables();
          initCacheParams(app.config());
          initMissingAuthenticationAndCsrfCheckHandler();
          app.eventBus().emit(new ReflectedHandlerInvokerInit(this));
      }
  
      @Override
      public int hashCode() {
          return $.hc(method);
      }
  
      @Override
      public boolean equals(Object obj) {
          if (obj == this) {
              return true;
          }
          if (obj instanceof ReflectedHandlerInvoker) {
              return $.eq(((ReflectedHandlerInvoker) obj).method, method);
          }
          return false;
      }
  
      @Override
      public String toString() {
          return method.toString();
      }
  
      @Override
      protected void releaseResources() {
          app = null;
          controller = null;
          controllerClass = null;
          method = null;
          methodAccess = null;
          handler.destroy();
          handler = null;
          cacheSupport = null;
          super.releaseResources();
      }
  
      @Override
      public int order() {
          return this.order;
      }
  
      public M handlerMetaInfo() {
          return handler;
  
      }
  
      @Override
      public Integer priority() {
          return handler.priority();
      }
  
      @Override
      public Method invokeMethod() {
          return method;
      }
  
      public interface ReflectedHandlerInvokerVisitor extends Visitor, $.Func2<Class<?>, Method, Void> {
      }
  
      public ReflectedHandlerInvoker attribute(String key, Object value) {
          attributes.put(key, value);
          return this;
      }
  
      public <T> T attribute(String key) {
          return $.cast(attributes.get(key));
      }
  
      @Override
      public void accept(Visitor visitor) {
          ReflectedHandlerInvokerVisitor rv = (ReflectedHandlerInvokerVisitor) visitor;
          rv.apply(controllerClass, method);
      }
  
      public Class<?> controllerClass() {
          return controllerClass;
      }
  
      public Method method() {
          return method;
      }
  
      @Override
      public CacheSupportMetaInfo cacheSupport() {
          return cacheSupport;
      }
  
      @Override
      public MissingAuthenticationHandler missingAuthenticationHandler() {
          return missingAuthenticationHandler;
      }
  
      @Override
      public MissingAuthenticationHandler csrfFailureHandler() {
          return csrfFailureHandler;
      }
  
      public Result handle(final ActionContext context) {
          if (disabled) {
              if (INTERCEPTING == context.state()) {
                  return null;
              }
              return ActNotFound.get();
          }
  
          if (null != throttleFilter) {
              Result throttleResult = throttleFilter.handle(context);
              if (null != throttleResult) {
                  return throttleResult;
              }
          }
  
          Result result = pluginBeforeHandler.apply(context);
          if (null != result) {
              return result;
          }
  
          if (suppressJsonDateFormat) {
              context.suppressJsonDateFormat();
          }
  
          if (requireCaptcha) {
              context.markAsRequireCaptcha();
          }
  
          if (enableCircularReferenceDetect) {
              context.enableCircularReferenceDetect();
          }
  
          if (null != downloadFilename) {
              context.downloadFileName(downloadFilename);
          }
  
          if (allowQrCodeRendering) {
              context.markAllowQrCodeRendering();
          }
  
          context.setReflectedHandlerInvoker(this);
          if (!context.skipEvents()) {
              app.eventBus().emit(new ReflectedHandlerInvokerInvoke(this, context));
          }
  
          if (1 == fieldsAndParamsCount(context)) {
              context.allowIgnoreParamNamespace();
          } else {
              context.disallowIgnoreParamNamespace();
          }
  
          if (isLargeResponse) {
              context.setLargeResponse();
          }
  
          if (null != filters) {
              context.fastjsonFilters(filters);
          }
  
          if (null != features) {
              context.fastjsonFeatures(features);
          }
  
          if (null != propertyNamingStrategy) {
              context.fastjsonPropertyNamingStrategy(propertyNamingStrategy);
          }
  
          if (null != dateFormatPattern) {
              context.dateFormatPattern(dateFormatPattern);
          }
  
          if (byPassImplicityTemplateVariable && context.state().isHandling()) {
              context.byPassImplicitVariable();
          }
  
          context.templateChangeListener(templateChangeListener);
          if (noTemplateCache) {
              context.disableTemplateCaching();
          }
  
          context.currentMethod(method);
  
          String urlContext = this.controller.urlContext();
          if (S.notBlank(urlContext)) {
              context.urlContext(urlContext);
          }
  
          String templateContext = this.controller.templateContext();
          if (null != templateContext) {
              context.templateContext(templateContext);
          }
          preventDoubleSubmission(context);
          processForceResponse(context);
          if (forceDataBinding || context.state().isHandling()) {
              ensureJsonDtoGenerated(context);
          }
          final Object controller = controllerInstance(context);
  
          final Object[] params = params(controller, context);
  
          context.ensureCaptcha();
  
          Map<String, ConstraintViolation> violations = context.violations();
          if (HANDLING == context.state() && !violations.isEmpty()) {
              if (null != validateViolationAdvice) {
                  Result r = null;
                  try {
                      Object retVal = validateViolationAdvice.onValidateViolation(violations, context);
                      if (null != retVal) {
                          if (retVal instanceof Result) {
                              r = (Result) retVal;
                          } else {
                              String payload;
                              H.Format accept = context.accept();
                              boolean requireXML = accept.isSameTypeWith(H.Format.XML);
                              JSONObject json = $.deepCopy(retVal).to(JSONObject.class);
                              if (requireXML) {
                                  Document doc = $.convert(json).to(Document.class);
                                  payload = XML.toString(doc);
                                  r = new RenderXML(payload);
                              } else {
                                  payload = JSON.toJSONString(json);
                                  context.resp().contentType(H.Format.JSON);
                                  r = new RenderJSON(payload);
                              }
                              r.status(H.Status.BAD_REQUEST);
                          }
                      }
                  } catch (Result e) {
                      r = e;
                  }
                  if (null != r) {
                      return r;
                  }
              }
  
              /*
               * We will send back response immediately when param validation
               * failed in either of the following cases:
               * 1) this is an ajax call
               * 2) the accept content type is **NOT** html
               */
              boolean failOnViolation = context.isAjax() || !context.accept().isSameTypeWith(H.Format.HTML);
  
              if (failOnViolation) {
                  String msg = context.violationMessage(";");
                  return ActBadRequest.create(msg);
              }
          }
  
          if (async) {
              final JobManager jobManager = context.app().jobManager();
              final String jobId = jobManager.randomJobId();
              final String reqInfo = context.req().toString();
              context.attachmentName(); // ensure attachment name get initialized
              context.keep();
              jobManager.prepare(jobId, new TrackableWorker() {
                  @Override
                  protected void run(ProgressGauge progressGauge) {
                      if (isTraceEnabled()) {
                          trace("about invoking request handler[%s] asynchronously: %s", reqInfo, jobId);
                      }
                      try {
                          Object o = invoke(context, controller, params);
                          if (null == o) {
                              o = "done";
                          }
                          Map<String, Object> payload = C.Map("attachmentName", context.rawAttachmentName());
                          jobManager.cacheResult(jobId, o, method, payload);
                          context.progress().commitFinalState();
                          boolean canDestroy = false;
                          synchronized (context) {
                              if (context.isKeep()) {
                                  context.unKeep();
                              } else {
                                  canDestroy = true;
                              }
                          }
                          if (canDestroy) {
                              context.destroy();
                          }
                      } catch (Exception e) {
                          String errMsg = S.buffer("Error handling request: ").append(reqInfo).toString();
                          warn(e, errMsg);
                          ControllerProgressGauge gauge = context.progress();
                          gauge.fail(errMsg);
                          jobManager.cacheResult(jobId, C.Map("error", e.getLocalizedMessage()), method, null);
                          gauge.commitFinalState();
                      }
                  }
              });
              context.setJobId(jobId);
              WebSocketConnectionManager wscm = app.getInstance(WebSocketConnectionManager.class);
              wscm.subscribe(context.session(), SimpleProgressGauge.wsJobProgressTag(jobId));
              jobManager.now(jobId);
              /*
               * We need to decide how to render the response here:
               * 1. if it require json, then return json response directly
               * 2. otherwise we want to check the {@link H.Request#rawAccept()},
               * 2.1 in case raw accept is HTML then cache current accept and render Async status page, once done render
               *     result page using cached accept
               * 2.2 in case raw accept is not HTML then cache current accept and render JSON response
               */
              H.Format wantedAccept = null;
              boolean renderJson = !reportAsyncProgress;
              if (!renderJson) {
                  H.Request<?> req = context.req();
                  wantedAccept = req.accept();
                  renderJson = wantedAccept.isSameTypeWith(H.Format.JSON) || !req.rawAccept().isSameTypeWith(H.Format.HTML);
              }
              if (renderJson) {
                  String resultUrl = context.router().fullUrl("/~/jobs/%s/result", jobId);
                  return new RenderJSON(C.Map("jobId", jobId, "resultUrl", resultUrl));
              }
              context.renderArg("targetAccept", wantedAccept.name());
              context.accept(H.Format.HTML);
              context.templatePath("/act/asyncJob.html");
              context.renderArg("jobId", jobId);
              return RenderTemplate.get();
          }
  
          try {
              return pluginAfterHandler.apply(invoke(handler, context, controller, params), context);
          } finally {
              if (hasOutputVar) {
                  fillOutputVariables(controller, params, context);
              }
          }
      }
  
      @Override
      public Result handle(Result result, ActionContext actionContext) {
          actionContext.setResult(result);
          return handle(actionContext);
      }
  
      @Override
      public Result handle(Exception e, ActionContext actionContext) {
          actionContext.attribute(ActionContext.ATTR_EXCEPTION, e);
          return handle(actionContext);
      }
  
      @Override
      public boolean sessionFree() {
          return sessionFree;
      }
  
      @Override
      public boolean express() {
          return express;
      }
  
      @Override
      public boolean skipEvents() {
          return skipEvents;
      }
  
      public CORS.Spec corsSpec() {
          return corsSpec;
      }
  
      @Override
      public CSRF.Spec csrfSpec() {
          return csrfSpec;
      }
  
      @Override
      public String contentSecurityPolicy() {
          return disableCsp ? null : csp;
      }
  
      public boolean disableContentSecurityPolicy() {
          return disableCsp;
      }
  
      public void setLargeResponseHint() {
          if (!this.forceSmallResponse) {
              this.isLargeResponse = true;
          }
      }
  
      public String singleJsonFieldName() {
          return singleJsonFieldName;
      }
  
      public int fieldsAndParamsCount() {
          return fieldsAndParamsCount;
      }
  
      private void cacheJsonDto(ActContext<?> context, JsonDto dto) {
          context.attribute(JsonDto.CTX_ATTR_KEY, dto);
      }
  
      private void ensureJsonDtoGenerated(ActionContext context) {
          if (0 == fieldsAndParamsCount || (!context.jsonEncoded() && !context.xmlEncoded())) {
              return;
          }
          Class<? extends JsonDto> dtoClass = jsonDTOClassManager.get(paramSpecs, controllerClass);
          if (null == dtoClass) {
              // there are neither fields nor params
              return;
          }
          try {
              String patchedJsonBody = patchedJsonBody(context);
              context.patchedJsonBody(patchedJsonBody);
              JsonDto dto = JSON.parseObject(patchedJsonBody, dtoClass);
              if (null != dto) {
                  patchDtoBeans(dto);
                  cacheJsonDto(context, dto);
              }
          } catch (JSONException e) {
              if (e.getCause() != null) {
                  warn(e.getCause(), "error parsing JSON data");
              } else {
                  warn(e, "error parsing JSON data");
              }
              throw new BadRequest(e.getCause());
          }
      }
  
      private void patchDtoBeans(JsonDto dto) {
          if (hasDtoPatches) {
              for (JsonDtoPatch patch : dtoPatches) {
                  Object bean = dto.get(patch.name());
                  if (null != bean) {
                      patch.applyChildren(bean);
                  }
              }
          }
      }
  
      private int fieldsAndParamsCount(ActionContext context) {
          if (fieldsAndParamsCount < 2) {
              return fieldsAndParamsCount;
          }
          return fieldsAndParamsCount - context.pathVarCount() - sessionVariablesCount;
      }
  
      private String singleJsonFieldName(ActionContext context) {
          if (null != singleJsonFieldName) {
              return singleJsonFieldName;
          }
          for (BeanSpec spec : paramSpecs) {
              String name = spec.name();
              if (context.isPathVar(name)) {
                  continue;
              }
              return name;
          }
          return null;
      }
  
      /**
       * Suppose method signature is: `public void foo(Foo foo)`, and a JSON content is
       * not `{"foo": {foo-content}}`, then wrap it as `{"foo": body}`
       */
      private String patchedJsonBody(ActionContext context) {
          String body = context.body();
          if (!config.allowJsonBodyPatch() || S.blank(body)) {
              return body;
          }
          if (context.xmlEncoded()) {
              Document doc = XML.read(body);
              JSONObject json = $.convert(doc).to(JSONObject.class);
              body = JSON.toJSONString(json.containsKey(xmlRootTag) ? json.get(xmlRootTag) : json);
          }
          if (1 < fieldsAndParamsCount(context)) {
              return body;
          }
          String theName = singleJsonFieldName(context);
          if (null == theName) {
              return body;
          }
          int theNameLen = theName.length();
          body = body.trim();
          boolean needPatch = body.charAt(0) == '[';
          if (!needPatch) {
              if (body.charAt(0) != '{') {
                  throw new IllegalArgumentException("Cannot parse JSON string: " + body);
              }
              boolean startCheckName = false;
              int nameStart = -1;
              for (int i = 1; i < body.length(); ++i) {
                  char c = body.charAt(i);
                  if (c == ' ') {
                      continue;
                  }
                  if (startCheckName) {
                      int id = i - nameStart - 1;
                      if (c == '"') {
                          if (id < theNameLen) {
                              needPatch = true;
                          }
                          break;
                      }
                      if (id >= theNameLen || theName.charAt(i - nameStart - 1) != c) {
                          needPatch = true;
                          break;
                      }
                  } else if (c == '"') {
                      startCheckName = true;
                      nameStart = i;
                  }
              }
          }
          return needPatch ? S.fmt("{\"%s\": %s}", theName, body) : body;
      }
  
      private Class[] paramTypes(App app) {
          int sz = handler.paramCount();
          Class[] ca = new Class[sz];
          for (int i = 0; i < sz; ++i) {
              HandlerParamMetaInfo param = handler.param(i);
              ca[i] = app.classForName(param.type().getClassName());
          }
          return ca;
      }
  
      private void processForceResponse(ActionContext actionContext) {
          if (null != forceResponseContentType && $.not(actionContext.paramVal("_accept"))) {
              actionContext.accept(forceResponseContentType);
          }
          if (null != forceResponseStatus) {
              actionContext.forceResponseStatus(forceResponseStatus);
          }
      }
  
      private void preventDoubleSubmission(ActionContext context) {
          if (null == dspToken) {
              return;
          }
          H.Request req = context.req();
          if (req.method().safe()) {
              return;
          }
          String tokenValue = context.paramVal(dspToken);
          if (S.blank(tokenValue)) {
              return;
          }
          H.Session session = context.session();
          String cacheKey = S.concat("DSP-", dspToken);
          String cached = session.cached(cacheKey);
          if (S.eq(tokenValue, cached)) {
              throw Conflict.get();
          }
          session.cacheFor1Min(cacheKey, tokenValue);
      }
  
      private Object controllerInstance(ActionContext context) {
          if (isStatic) {
              return null;
          }
          if (null != singleton) {
              return singleton;
          }
          Class host = controllerClass;
          if (host.isAssignableFrom(context.handlerClass())) {
              host = context.handlerClass();
          }
          Object inst = context.__controllerInstance(host);
          if (null == inst) {
              inst = paramLoaderService.loadHostBean(host, context);
              context.__controllerInstance(host, inst);
          }
          return inst;
      }
  
      private void initCacheParams(AppConfig config) {
          if (Act.isDev() && !config.cacheForOnDevMode()) {
              cacheSupport = CacheSupportMetaInfo.disabled();
              return;
          }
          CacheFor cacheFor = ReflectedInvokerHelper.getAnnotation(CacheFor.class, method);
          cacheSupport = null == cacheFor ? CacheSupportMetaInfo.disabled() : CacheSupportMetaInfo.enabled(
                  new CacheKeyBuilder(cacheFor, S.concat(controllerClass.getName(), ".", method.getName())),
                  cacheFor.id(),
                  cacheFor.value(),
                  cacheFor.supportPost(),
                  cacheFor.usePrivate(),
                  cacheFor.noCacheControl(),
                  cacheFor.eTagOnly(),
                  cacheFor.noCache()
          );
      }
  
      private void fillOutputVariables(Object controller, Object[] params, ActionContext context) {
          if (!isStatic) {
              for (Map.Entry<Field, String> entry : outputFields.entrySet()) {
                  Field field = entry.getKey();
                  String outputName = entry.getValue();
                  try {
                      Object val = field.get(controller);
                      context.renderArg(outputName, val);
                  } catch (IllegalAccessException e) {
                      throw E.unexpected(e);
                  }
              }
              context.fieldOutputVarCount(outputFields.size());
          }
          if (0 == params.length) {
              return;
          }
          for (Map.Entry<Integer, String> entry : outputParams.entrySet()) {
              int i = entry.getKey();
              String outputName = entry.getValue();
              context.renderArg(outputName, params[i]);
          }
      }
  
      private void initMissingAuthenticationAndCsrfCheckHandler() {
          HandleMissingAuthentication hma = getAnnotation(HandleMissingAuthentication.class);
          if (null != hma) {
              missingAuthenticationHandler = hma.value().handler(hma.custom());
          }
  
          HandleCsrfFailure hcf = getAnnotation(HandleCsrfFailure.class);
          if (null != hcf) {
              csrfFailureHandler = hcf.value().handler(hcf.custom());
          }
      }
  
      private void initOutputVariables() {
          Set<String> outputNames = new HashSet<>();
          outputFields = new HashMap<>();
          if (!isStatic) {
              List<Field> fields = $.fieldsOf(controllerClass);
              for (Field field : fields) {
                  Output output = field.getAnnotation(Output.class);
                  if (null != output) {
                      String fieldName = field.getName();
                      String outputName = output.value();
                      if (S.blank(outputName)) {
                          outputName = fieldName;
                      }
                      E.unexpectedIf(outputNames.contains(outputName), "output name already used: %s", outputName);
                      field.setAccessible(true);
                      outputFields.put(field, outputName);
                      outputNames.add(outputName);
                  }
              }
          }
  
          try {
              outputParams = new HashMap<>();
              Class<?>[] paramTypes = method.getParameterTypes();
              int len = paramTypes.length;
              if (0 == len) {
                  return;
              }
              Annotation outputRequestParams = ReflectedInvokerHelper.getAnnotation(OutputRequestParams.class, method);
              if (null != outputRequestParams) {
                  DependencyInjector injector = app.injector();
                  for (int i = 0; i < len; ++i) {
                      if (!injector.isProvided(paramTypes[i])) {
                          String outputName = handler.param(i).name();
                          outputParams.put(i, outputName);
                          outputNames.add(outputName);
                      }
                  }
              } else {
                  Annotation[][] aaa = method.getParameterAnnotations();
                  for (int i = 0; i < len; ++i) {
                      Annotation[] aa = aaa[i];
                      if (null == aa) {
                          continue;
                      }
                      Output output = null;
                      for (int j = aa.length - 1; j >= 0; --j) {
                          Annotation a = aa[j];
                          if (a.annotationType() == Output.class) {
                              output = $.cast(a);
                              break;
                          }
                      }
                      if (null == output) {
                          continue;
                      }
                      String outputName = output.value();
                      if (S.blank(outputName)) {
                          HandlerParamMetaInfo paramMetaInfo = handler.param(i);
                          outputName = paramMetaInfo.name();
                      }
                      E.unexpectedIf(outputNames.contains(outputName), "output name already used: %s", outputName);
                      outputParams.put(i, outputName);
                      outputNames.add(outputName);
                  }
              }
          } finally {
              hasOutputVar = !outputNames.isEmpty();
          }
      }
  
      private Object mockReturn() {
          if (returnType == Void.class || returnType == void.class) {
              return null;
          }
          BeanSpec returnSpec = BeanSpec.of(method.getGenericReturnType(), Act.injector());
          return SampleData.generate(returnSpec, null);
      }
  
      private Result invoke(M handlerMetaInfo, ActionContext context, Object controller, Object[] params) {
          Object retVal;
          String invocationInfo = null;
          if (config.traceHandler()) {
              invocationInfo = S.fmt("%s(%s)", handlerMetaInfo.fullName(), $.toString2(params));
              Trace.LOGGER_HANDLER.trace(invocationInfo);
          }
          try {
              retVal = invoke(context, controller, params);
          } catch (Result r) {
              retVal = r;
          } catch (Exception e) {
              if (config.traceHandler()) {
                  Trace.LOGGER_HANDLER.trace(e, "error invoking %s", invocationInfo);
              }
              throw e;
          }
          return transform(retVal, this, context, returnValueAdvice, shallTransformReturnVal, returnIterable);
      }
  
      private Object invoke(ActionContext context, Object controller, Object[] params) {
          Object retVal;
          try {
              retVal = null == methodAccess ? $.invokeStatic(method, params) : methodAccess.invoke(controller, handlerIndex, params);
          } catch (ToBeImplemented e) {
              if (mock) {
                  retVal = mockReturn();
              } else {
                  throw e;
              }
          }
          if (returnString && context.acceptJson()) {
              retVal = null == retVal ? null : ensureValidJson(S.string(retVal));
          }
          if (null != retVal) {
              context.calcResultHashForEtag(retVal);
          }
          return retVal;
      }
  
      private static Result transform(Object retVal, ReflectedHandlerInvoker invoker, ActionContext context, ReturnValueAdvice returnValueAdvice, boolean transformRetVal, boolean returnIterable) {
          if (context.resp().isClosed()) {
              return null;
          }
          if (retVal instanceof Result) {
              Result result = (Result) retVal;
              if (result.status().isError()) {
                  throw result;
              }
              invoker.checkTemplate(context);
              return result;
          }
          final HandlerMethodMetaInfo handlerMetaInfo = invoker.handler;
          final boolean hasReturn = handlerMetaInfo.hasReturn() && !handlerMetaInfo.returnTypeInfo().isResult();
          if (null == retVal && hasReturn) {
              // ActFramework respond 404 Not Found when
              // handler invoker return `null`
              // and there are return type of the action method signature
              // and the return type is **NOT** Result
              return ActNotFound.create();
          }
          boolean hasTemplate = invoker.checkTemplate(context);
          if (!hasTemplate && hasReturn && null != returnValueAdvice) {
              if (transformRetVal) {
                  PropertySpec.MetaInfo propertySpec = PropertySpec.MetaInfo.withCurrent(handlerMetaInfo, context);
                  if (null != propertySpec) {
                      Lang._MappingStage stage = propertySpec.applyTo(Lang.map(retVal), context);
                      Object newRetVal = returnIterable ? new JSONArray() : new JSONObject();
                      retVal = stage.to(newRetVal);
                  } else {
                      String curSpec = PropertySpec.current.get();
                      if (S.notBlank(curSpec)) {
                          Object newRetVal = returnIterable ? new JSONArray() : new JSONObject();
                          retVal = Lang.deepCopy(retVal).filter(curSpec).to(newRetVal);
                          PropertySpec.current.remove();
                      }
                  }
              }
              retVal = returnValueAdvice.applyTo(retVal, context);
          }
          return Controller.Util.inferResult(handlerMetaInfo, retVal, context, hasTemplate);
      }
  
      private String ensureValidJson(String result) {
          if (S.blank(result)) {
              return "{}";
          }
          boolean looksOkay = (result.startsWith("{") && result.endsWith("}")) || (result.startsWith("[") && result.endsWith("]"));
          if (!looksOkay) {
              fullJsonStringCheckFailure = true;
              return "{\"result\":\"" + result + "\"}";
          }
          if (fullJsonStringCheckFailure || !fullJsonStringChecked) {
              fullJsonStringChecked = true;
              try {
                  JSON.parse(result);
              } catch (Exception e) {
                  fullJsonStringCheckFailure = true;
                  return "{\"result\":\"" + result + "\"}";
              }
          }
          return result;
      }
  
      private boolean checkTemplate(ActionContext context) {
          if (!context.state().isHandling()) {
              //we don't check template on interceptors
              return false;
          }
          H.Format fmt = context.accept();
          if (noTemplateCache || Act.isDev()) {
              return probeTemplate(fmt, context);
          }
          Boolean hasTemplate = context.hasTemplate();
          if (null != hasTemplate) {
              return hasTemplate;
          }
          hasTemplate = templateAvailabilityCache.get(fmt);
          if (null == hasTemplate) {
              hasTemplate = probeTemplate(fmt, context);
              templateAvailabilityCache.putIfAbsent(fmt, hasTemplate);
          }
          context.setHasTemplate(hasTemplate);
          return hasTemplate;
      }
  
      private boolean probeTemplate(H.Format fmt, ActionContext context) {
          if (!TemplatePathResolver.isAcceptFormatSupported(fmt)) {
              return false;
          } else {
              Template t = Act.viewManager().load(context);
              boolean hasTemplate = null != t;
              context.setHasTemplate(hasTemplate);
              return hasTemplate;
          }
      }
  
      private Object[] params(Object controller, ActionContext context) {
          if (0 == paramCount) {
              return DUMP_PARAMS;
          }
          return paramLoaderService.loadMethodParams(controller, method, context);
      }
  
  
      public <T extends Annotation> T getAnnotation(Class<T> annoType) {
          T anno = ReflectedInvokerHelper.getAnnotation(annoType, method);
          if (null == anno) {
              anno = controllerClass.getAnnotation(annoType);
          }
          return anno;
      }
  
      public boolean hasAnnotation(Class<? extends Annotation> annoType) {
          return (null != ReflectedInvokerHelper.getAnnotation(annoType, method)) || null != controllerClass.getAnnotation(annoType);
      }
  
      private boolean shouldSuppressJsonDateFormat() {
          List<Field> fields = $.fieldsOf(returnType);
          for (Field field : fields) {
              JSONField jsonField = field.getAnnotation(JSONField.class);
              if (null != jsonField) {
                  if (S.notBlank(jsonField.format())) {
                      return true;
                  }
              }
          }
          return false;
      }
  
      public static ControllerAction createControllerAction(ActionMethodMetaInfo meta, App app) {
          return new ControllerAction(new ReflectedHandlerInvoker(meta, app));
      }
  
      public static BeforeInterceptor createBeforeInterceptor(InterceptorMethodMetaInfo meta, App app) {
          return new _Before(new ReflectedHandlerInvoker(meta, app));
      }
  
      public static AfterInterceptor createAfterInterceptor(InterceptorMethodMetaInfo meta, App app) {
          return new _After(new ReflectedHandlerInvoker(meta, app));
      }
  
      public static ExceptionInterceptor createExceptionInterceptor(CatchMethodMetaInfo meta, App app) {
          return new _Exception(new ReflectedHandlerInvoker(meta, app), meta);
      }
  
      public static FinallyInterceptor createFinannyInterceptor(InterceptorMethodMetaInfo meta, App app) {
          return new _Finally(new ReflectedHandlerInvoker(meta, app));
      }
  
      private static class _Before extends BeforeInterceptor {
          private ActionHandlerInvoker invoker;
  
          _Before(ActionHandlerInvoker invoker) {
              super(invoker.priority());
              this.invoker = invoker;
          }
  
          @Override
          public Result handle(ActionContext actionContext) throws Exception {
              return invoker.handle(actionContext);
          }
  
          @Override
          public int order() {
              return invoker.order();
          }
  
          @Override
          public int hashCode() {
              return $.hc(invoker, getClass());
          }
  
          @Override
          public boolean equals(Object obj) {
              if (obj == this) {
                  return true;
              }
              if (obj instanceof _Before) {
                  return $.eq(((_Before) obj).invoker, invoker);
              }
              return false;
          }
  
          @Override
          public String toString() {
              return invoker.toString();
          }
  
          @Override
          public boolean sessionFree() {
              return invoker.sessionFree();
          }
  
          @Override
          public boolean express() {
              return invoker.express();
          }
  
          @Override
          public boolean skipEvents() {
              return invoker.skipEvents();
          }
  
          @Override
          public void accept(Visitor visitor) {
              invoker.accept(visitor.invokerVisitor());
          }
  
          @Override
          public CORS.Spec corsSpec() {
              return invoker.corsSpec();
          }
  
          @Override
          protected void releaseResources() {
              invoker.destroy();
              invoker = null;
          }
      }
  
      private static class _After extends AfterInterceptor {
          private AfterInterceptorInvoker invoker;
  
          _After(AfterInterceptorInvoker invoker) {
              super(invoker.priority());
              this.invoker = invoker;
          }
  
          @Override
          public Result handle(Result result, ActionContext actionContext) throws Exception {
              return invoker.handle(result, actionContext);
          }
  
          @Override
          public int hashCode() {
              return $.hc(invoker, getClass());
          }
  
          @Override
          public boolean equals(Object obj) {
              if (obj == this) {
                  return true;
              }
              if (obj instanceof _After) {
                  return $.eq(((_After) obj).invoker, invoker);
              }
              return false;
          }
  
          @Override
          public String toString() {
              return invoker.toString();
          }
  
          @Override
          public int order() {
              return invoker.order();
          }
  
          @Override
          public CORS.Spec corsSpec() {
              return invoker.corsSpec();
          }
  
          @Override
          public boolean sessionFree() {
              return invoker.sessionFree();
          }
  
          @Override
          public boolean express() {
              return invoker.express();
          }
  
          @Override
          public boolean skipEvents() {
              return invoker.skipEvents();
          }
  
          @Override
          public void accept(Visitor visitor) {
              invoker.accept(visitor.invokerVisitor());
          }
  
          @Override
          public void accept(ActionHandlerInvoker.Visitor visitor) {
              invoker.accept(visitor);
          }
  
          @Override
          protected void releaseResources() {
              invoker.destroy();
              invoker = null;
          }
      }
  
      private static class _Exception extends ExceptionInterceptor {
          private ExceptionInterceptorInvoker invoker;
  
          _Exception(ExceptionInterceptorInvoker invoker, CatchMethodMetaInfo metaInfo) {
              super(invoker.priority(), exceptionClassesOf(metaInfo));
              this.invoker = invoker;
          }
  
          @SuppressWarnings("unchecked")
          private static List<Class<? extends Exception>> exceptionClassesOf(CatchMethodMetaInfo metaInfo) {
              List<String> classNames = metaInfo.exceptionClasses();
              List<Class<? extends Exception>> clsList;
              clsList = C.newSizedList(classNames.size());
              App app = Act.app();
              for (String cn : classNames) {
                  clsList.add((Class) app.classForName(cn));
              }
              return clsList;
          }
  
          @Override
          public int hashCode() {
              return $.hc(invoker, getClass());
          }
  
          @Override
          public boolean equals(Object obj) {
              if (obj == this) {
                  return true;
              }
              if (obj instanceof _Exception) {
                  return $.eq(((_Exception) obj).invoker, invoker);
              }
              return false;
          }
  
          @Override
          public String toString() {
              return invoker.toString();
          }
  
          @Override
          public int order() {
              return invoker.order();
          }
  
          @Override
          protected Result internalHandle(Exception e, ActionContext actionContext) throws Exception {
              return invoker.handle(e, actionContext);
          }
  
          @Override
          public boolean sessionFree() {
              return invoker.sessionFree();
          }
  
          @Override
          public boolean express() {
              return invoker.express();
          }
  
          @Override
          public boolean skipEvents() {
              return invoker.skipEvents();
          }
  
          @Override
          public void accept(ActionHandlerInvoker.Visitor visitor) {
              invoker.accept(visitor);
          }
  
          @Override
          public void accept(Visitor visitor) {
              invoker.accept(visitor.invokerVisitor());
          }
  
          @Override
          public CORS.Spec corsSpec() {
              return invoker.corsSpec();
          }
  
          @Override
          protected void releaseResources() {
              invoker.destroy();
              invoker = null;
          }
      }
  
      private static class _Finally extends FinallyInterceptor {
          private ActionHandlerInvoker invoker;
  
          _Finally(ActionHandlerInvoker invoker) {
              super(invoker.priority());
              this.invoker = invoker;
          }
  
          @Override
          public void handle(ActionContext actionContext) throws Exception {
              invoker.handle(actionContext);
          }
  
          @Override
          public int hashCode() {
              return $.hc(invoker, getClass());
          }
  
          @Override
          public boolean equals(Object obj) {
              if (obj == this) {
                  return true;
              }
              if (obj instanceof _Finally) {
                  return $.eq(((_Finally) obj).invoker, invoker);
              }
              return false;
          }
  
          @Override
          public String toString() {
              return invoker.toString();
          }
  
          @Override
          public int order() {
              return invoker.order();
          }
  
          @Override
          public CORS.Spec corsSpec() {
              return invoker.corsSpec();
          }
  
          @Override
          public boolean sessionFree() {
              return invoker.sessionFree();
          }
  
          @Override
          public boolean express() {
              return invoker.express();
          }
  
          @Override
          public boolean skipEvents() {
              return invoker.skipEvents();
          }
  
          @Override
          public void accept(Visitor visitor) {
              invoker.accept(visitor.invokerVisitor());
          }
  
          @Override
          protected void releaseResources() {
              invoker.destroy();
              invoker = null;
          }
      }
  
      private static class CacheKeyBuilder extends $.F1<ActionContext, String> {
          private String[] keys;
          private final String base;
  
          CacheKeyBuilder(CacheFor cacheFor, String actionPath) {
              this.base = base(actionPath);
              this.keys = cacheFor.keys();
          }
  
          private String base(String actionPath) {
              S.Buffer buffer = S.newBuffer();
              String[] sa = actionPath.split("\\.");
              for (String s : sa) {
                  buffer.append(s.charAt(0));
              }
              buffer.append(actionPath.hashCode());
              return buffer.toString();
          }
  
          @Override
          public String apply(ActionContext context) throws NotAppliedException, $.Break {
              TreeMap<String, String> keyValues = keyValues(context);
              S.Buffer buffer = S.newBuffer(base);
              for (Map.Entry<String, String> entry : keyValues.entrySet()) {
                  buffer.append("-").append(entry.getKey()).append(":").append(entry.getValue());
              }
              buffer.append(context.userAgent().isMobile() ? "M" : "B");
              return buffer.toString();
          }
  
          private TreeMap<String, String> keyValues(ActionContext context) {
              TreeMap<String, String> map = new TreeMap<>();
              if (keys.length > 0) {
                  for (String key : keys) {
                      map.put(key, paramVal(key, context));
                  }
              } else {
                  for (String key : context.paramKeys()) {
                      map.put(key, paramVal(key, context));
                  }
              }
              map.put("__accept__", context.accept().name());
              return map;
          }
  
          private String paramVal(String key, ActionContext context) {
              String[] allValues = context.paramVals(key);
              if (0 == allValues.length) {
                  return "";
              } else if (1 == allValues.length) {
                  return allValues[0];
              } else {
                  return $.toString2(allValues);
              }
          }
      }
  
  }
  