content: package act.app;
  
  /*-
   * #%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.controller.Controller.Util.*;
  import static org.osgl.http.H.Format.*;
  import static org.osgl.http.H.Header.Names.*;
  
  import act.*;
  import act.conf.AppConfig;
  import act.controller.Controller;
  import act.controller.ParamNames;
  import act.controller.ResponseCache;
  import act.controller.captcha.CaptchaViolation;
  import act.data.MapUtil;
  import act.data.RequestBodyParser;
  import act.event.*;
  import act.handler.RequestHandler;
  import act.handler.builtin.controller.RequestHandlerProxy;
  import act.handler.builtin.controller.impl.ReflectedHandlerInvoker;
  import act.i18n.LocaleResolver;
  import act.inject.param.ParamValueLoaderService;
  import act.job.JobManager;
  import act.route.*;
  import act.security.CORS;
  import act.session.SessionManager;
  import act.util.*;
  import act.view.RenderAny;
  import act.xio.undertow.UndertowRequest;
  import org.osgl.$;
  import org.osgl.concurrent.ContextLocal;
  import org.osgl.http.H;
  import org.osgl.logging.LogManager;
  import org.osgl.logging.Logger;
  import org.osgl.mvc.result.Result;
  import org.osgl.storage.ISObject;
  import org.osgl.util.*;
  import org.osgl.web.util.UserAgent;
  
  import java.io.File;
  import java.lang.annotation.Annotation;
  import java.net.URL;
  import java.util.*;
  import javax.enterprise.context.RequestScoped;
  import javax.inject.Inject;
  import javax.validation.ConstraintViolation;
  
  /**
   * {@code AppContext} encapsulate contextual properties needed by
   * an application session
   */
  @RequestScoped
  public class ActionContext extends ActContext.Base<ActionContext> implements Destroyable, RoutedContext {
  
      private static final Logger LOGGER = LogManager.get(ActionContext.class);
  
      public static final String ATTR_EXCEPTION = "__exception__";
      // used along with CollectionLoader to load multi-file uploads
      public static final String ATTR_CURRENT_FILE_INDEX = "__file_id__";
      public static final String REQ_BODY = "_body";
  
      private H.Request request;
      private ActResponse<?> response;
      private H.Session session;
      private H.Flash flash;
      private Set<Map.Entry<String, String[]>> requestParamCache;
      private Map<String, String> extraParams;
      private volatile Map<String, String[]> bodyParams;
      private Map<String, String[]> allParams;
      private String actionPath; // e.g. com.mycorp.myapp.controller.AbcController.foo
      private State state;
      private Map<Class, Object> controllerInstances;
      private Map<String, ISObject[]> uploads;
      private Router router;
      private String processedUrl;
      private RequestHandler handler;
      private UserAgent ua;
      private String sessionKeyUsername;
      private boolean sessionPassThrough;
      private LocaleResolver localeResolver;
      private boolean disableCors;
      private boolean disableCsrf;
      private Boolean hasTemplate;
      private $.Visitor<H.Format> templateChangeListener;
      private H.Status forceResponseStatus;
      private boolean cacheEnabled;
      private boolean allowQrCodeRendering;
      private MissingAuthenticationHandler forceMissingAuthenticationHandler;
      private MissingAuthenticationHandler forceCsrfCheckingFailureHandler;
      private String urlContext;
      private boolean byPassImplicitTemplateVariable;
      private boolean isLargeResponse;
      private boolean requireCaptcha;
      private int pathVarCount;
      private UrlPath urlPath;
      private Set<String> pathVarNames = new HashSet<>();
      private SessionManager sessionManager;
      private Trace.AccessLog accessLog;
      private ReflectedHandlerInvoker reflectedHandlerInvoker;
      private boolean requireBodyParsing;
      private boolean allowIgnoreParamNamespace;
      private boolean consumed;
      private boolean readyForDestroy;
      private int resultHash = Integer.MIN_VALUE;
      private PropertySpec.MetaInfo propSpec;
      private boolean suppressJsonDateFormat;
      private String attachmentName;
      private Class<?> handlerClass;
      private RouteSource routeSource;
      private String patchedJsonBody;
      private boolean nonBlock;
      public act.metric.Timer handleTimer;
      // indicate the current context need to be kept for async thread to access
      private boolean keep;
  
      // see https://github.com/actframework/actframework/issues/492
      public String encodedSessionToken;
  
      // -- replace attributres with fields -- perf tune
      // -- ATTR_CSRF_PREFETCHED
      private String csrfPrefetched;
  
      public void setCsrfPrefetched(String csrf) {
          csrfPrefetched = csrf;
      }
  
      public String csrfPrefetched() {
          return csrfPrefetched;
      }
  
      public void clearCsrfPrefetched() {
          csrfPrefetched = null;
      }
  
      // -- ATTR_WAS_UNAUTHENTICATED
      private boolean wasUnauthenticated;
  
      public boolean wasUnauthenticated() {
          return wasUnauthenticated;
      }
  
      public void setWasUnauthenticated() {
          wasUnauthenticated = true;
      }
  
      public void clearWasUnauthenticated() {
          wasUnauthenticated = false;
      }
  
      // -- ATTR_HANDLER
      // replaced with field handler
      // -- ATTR_RESULT
      private Result result;
  
      public Result result() {
          return result;
      }
  
      public void setResult(Result result) {
          this.result = result;
      }
  
      @Inject
      private ActionContext(App app, H.Request request, ActResponse<?> response) {
          super(app);
          E.NPE(app, request, response);
          request.context(this);
          response.context(this);
          this.request = request;
          this.response = response;
          this.accessLog = app.config().traceRequests() ? Trace.AccessLog.create(request) : null;
          this._init();
          this.state = State.CREATED;
          AppConfig config = app.config();
          this.disableCors = !config.corsEnabled();
          this.disableCsrf = req().method().safe();
          this.sessionKeyUsername = config.sessionKeyUsername();
          this.sessionPassThrough = config.sessionPassThrough();
          this.localeResolver = new LocaleResolver(this);
          this.sessionManager = app.sessionManager();
      }
  
      @Override
      public String toString() {
          H.Request req = request;
          if (null == req) {
              return "INVALID CONTEXT";
          }
          return S.buffer("[")
                  .a(req.ip())
                  .a("]")
                  .a(req.method())
                  .a(" ")
                  .a(req.url()).toString();
      }
  
      public State state() {
          return state;
      }
  
      public boolean isSessionDissolved() {
          return state == State.SESSION_DISSOLVED;
      }
  
      public boolean isSessionResolved() {
          return state == State.SESSION_RESOLVED;
      }
  
      public H.Request req() {
          return request;
      }
  
      public ActResponse<?> resp() {
          return response;
      }
  
      public ActResponse<?> prepareRespForResultEvaluation() {
          response.onResult();
          return response;
      }
  
      public boolean skipEvents() {
          if (null == handler) {
              return true;
          }
          return handler.skipEvents(this);
      }
  
      public ActionContext keep() {
          this.keep = true;
          return this;
      }
  
      public boolean isKeep() {
          return keep;
      }
  
      public void unKeep() {
          this.keep = false;
      }
  
      public void markAsConsumed() {
          consumed = true;
      }
  
      public boolean isConsumed() {
          return consumed;
      }
  
      public void markAsReadyForClose() {
          readyForDestroy = true;
      }
  
      public boolean isReadyForDestroy() {
          synchronized (this) {
              if (keep) {
                  keep = false;
                  return false;
              }
          }
          return readyForDestroy;
      }
  
      public H.Cookie cookie(String name) {
          return req().cookie(name);
      }
  
      public H.Session session() {
          return session;
      }
  
      public String session(String key) {
          return session.get(key);
      }
  
      public H.Session session(String key, String value) {
          return session.put(key, value);
      }
  
      /**
       * Returns HTTP session's id
       *
       * @return HTTP session id
       */
      public String sessionId() {
          return session().id();
      }
  
      public H.Flash flash() {
          return flash;
      }
  
      public String flash(String key) {
          return flash.get(key);
      }
  
      public H.Flash flash(String key, String value) {
          return flash.put(key, value);
      }
  
      public Router router() {
          return router;
      }
  
      public ActionContext router(Router router) {
          this.router = $.requireNotNull(router);
          return this;
      }
  
      public ActionContext markAsNonBlock() {
          this.nonBlock = true;
          return this;
      }
  
      public boolean isNonBlock() {
          return this.nonBlock;
      }
  
      public ActionContext processedUrl(String url) {
          this.processedUrl = url;
          return this;
      }
  
      private String processedUrl() {
          return null == processedUrl ? req().url() : processedUrl;
      }
  
      public String attachmentName() {
          if (null == attachmentName) {
              String s = processedUrl();
              if (s.contains("/")) {
                  s = S.cut(s).afterLast("/");
              } else {
                  s = methodPath();
                  if (s.contains(".")) {
                      s = S.cut(s).afterLast(".");
                  }
              }
              if (S.blank(s)) {
                  s = S.cut(actionPath).afterLast(".");
              }
              attachmentName = s;
          }
          if (!attachmentName.contains(accept().name())) {
              return attachmentName + "." + accept().name();
          }
          return attachmentName;
      }
  
      public String rawAttachmentName() {
          return attachmentName;
      }
  
      /**
       * Set download attachment name. This is used when response has
       * `Content-Disposition` header and the type is `attachment`.
       *
       * Note developer shall **NOT** add suffix to the filename, instead
       * the framework will automatically append suffix based on the
       * response content-type. E.g. if the response content type is
       * `text/csv`, it will append `.csv` to the name, while when the
       * content type is `application/vnd.ms-excel`, framework will append
       * `.xls` to the filename.
       *
       * @param filename
       *         the filename without suffix
       * @return this action context
       */
      public ActionContext downloadFileName(String filename) {
          this.attachmentName = filename;
          return this;
      }
  
      public String attachmentName(URL url) {
          if (null != attachmentName) {
              return attachmentName;
          }
          String path = url.getPath();
          String name = path.contains("/") ? S.cut(path).afterLast("/") : path;
          if (!name.contains(".")) {
              name = name + "." + accept().name();
          }
          return name;
      }
  
      public String attachmentName(File file) {
          if (null != attachmentName) {
              return attachmentName;
          }
          String path = file.getName();
          String name = path.contains("/") ? S.cut(path).afterLast("/") : path;
          if (!name.contains(".")) {
              name = name + "." + accept().name();
          }
          return name;
      }
  
      public boolean shouldSuppressJsonDateFormat() {
          return suppressJsonDateFormat;
      }
  
      public ActionContext suppressJsonDateFormat() {
          this.suppressJsonDateFormat = true;
          return this;
      }
  
      public void markAsRequireCaptcha() {
          this.requireCaptcha = true;
      }
  
      public void ensureCaptcha() {
          if (app().captchaManager().disabled()) {
              return;
          }
          if (!subjectToCaptchaProtection()) {
              return;
          }
          if (verifyReCaptcha()) {
              return;
          }
          if (!verifyActCaptcha()) {
              addViolation("captcha", new CaptchaViolation());
          }
      }
  
      private boolean verifyReCaptcha() {
          return app().httpClientService().verifyReCaptchaResponse(this);
      }
  
      private boolean verifyActCaptcha() {
          String token = paramVal("a-captcha-token");
          if (S.blank(token)) {
              return false;
          }
          String answer = paramVal("a-captcha-answer");
          if (S.blank(answer)) {
              return false;
          }
          Token theToken = app().crypto().parseToken(token);
          if (!theToken.isValid()) {
              return false;
          }
          theToken.consume();
          return S.eq(answer, theToken.id());
      }
  
      private boolean subjectToCaptchaProtection() {
          if (!requireCaptcha) {
              return false;
          }
          H.Method method = req().method();
          return method == H.Method.POST || method == H.Method.PUT || method == H.Method.PATCH;
      }
  
      public ActionContext calcResultHashForEtag(Object o) {
          if (cacheEnabled && null != o && !(o instanceof Result)) {
              resultHash = $.hc(o);
          }
          return this;
      }
  
      public int resultHashForEtag() {
          return resultHash;
      }
  
      public void applyResultHashToEtag() {
          if (resultHash > Integer.MIN_VALUE) {
              resp().addHeaderIfNotAdded(ETAG, S.string(resultHash));
          }
      }
  
      public ActionContext propertySpec(PropertySpec.MetaInfo spec) {
          this.propSpec = spec;
          return this;
      }
  
      public PropertySpec.MetaInfo propertySpec() {
          return this.propSpec;
      }
  
      public MissingAuthenticationHandler missingAuthenticationHandler() {
          if (null != forceMissingAuthenticationHandler) {
              return forceMissingAuthenticationHandler;
          }
          return isAjax() ? config().ajaxMissingAuthenticationHandler() : config().missingAuthenticationHandler();
      }
  
      public MissingAuthenticationHandler csrfFailureHandler() {
          if (null != forceCsrfCheckingFailureHandler) {
              return forceCsrfCheckingFailureHandler;
          }
          return isAjax() ? config().ajaxCsrfCheckFailureHandler() : config().csrfCheckFailureHandler();
      }
  
      public ActionContext forceMissingAuthenticationHandler(MissingAuthenticationHandler handler) {
          this.forceMissingAuthenticationHandler = handler;
          return this;
      }
  
      public ActionContext forceCsrfCheckingFailureHandler(MissingAuthenticationHandler handler) {
          this.forceCsrfCheckingFailureHandler = handler;
          return this;
      }
  
      public ActionContext byPassImplicitVariable() {
          this.byPassImplicitTemplateVariable = true;
          return this;
      }
  
      public boolean isByPassImplicitTemplateVariable() {
          return this.byPassImplicitTemplateVariable;
      }
  
      public ActionContext resetCache() {
          RequestHandler handler = handler();
          if (handler instanceof RequestHandlerProxy) {
              ((RequestHandlerProxy) handler).resetCache();
          }
          return this;
      }
  
      public ActionContext urlContext(String context) {
          this.urlContext = context;
          return this;
      }
  
      public String urlContext() {
          return urlContext;
      }
  
      /**
       * Return a {@link UrlPath} of this context.
       *
       * Note this method is used only by {@link Router} for dynamic path
       * matching.
       *
       * @return a {@link UrlPath} of this context
       */
      public UrlPath urlPath() {
          if (null == urlPath) {
              urlPath = UrlPath.of(null == processedUrl ? req().url() : processedUrl);
          }
          return urlPath;
      }
  
      /**
       * Implement Java Servlet's `RequestDispatcher.forward(String)` semantic.
       *
       * This method has the following restrictions:
       *
       * 1. the URL must be started with single `/`. i.e. it must be absolute PATH of current service.
       * 2. the request method must be `GET`.
       *
       * @param url
       *         the url template
       * @param args
       *         the url argument
       */
      public void forward(String url, Object... args) {
          badRequestIfNot(url.startsWith("/"), "forward URL must starts with single '/'");
          badRequestIf(url.startsWith("//"), "forward URL must starts with single `/`");
          badRequestIfNot(H.Method.GET == req().method(), "forward only support on HTTP GET request");
          uploads.clear();
          extraParams.clear();
          bodyParams = null;
          String target = S.fmt(url, args);
          urlPath = UrlPath.of(target);
          UndertowRequest req = $.cast(req());
          state = State.CREATED;
          req.forward(target);
          final RequestHandler requestHandler = router.getInvoker(H.Method.GET, url, this);
          requestHandler.handle(this);
      }
  
      // !!!IMPORTANT! the following methods needs to be kept to allow enhancer work correctly
      @Override
      public <T> T renderArg(String name) {
          return super.renderArg(name);
      }
  
      @Override
      public ActionContext renderArg(String name, Object val) {
          return super.renderArg(name, val);
      }
  
      @Override
      public Map<String, Object> renderArgs() {
          return super.renderArgs();
      }
  
      @Override
      public ActionContext templatePath(String templatePath) {
          hasTemplate = null;
          if (null != templateChangeListener) {
              templateChangeListener.visit(accept());
          }
          return super.templatePath(templatePath);
      }
  
      @Override
      public ActionContext templateLiteral(String literal) {
          // Need to declare this method for bytecode enhancement
          return super.templateLiteral(literal);
      }
  
      public ActionContext templateChangeListener($.Visitor<H.Format> listener) {
          this.templateChangeListener = $.requireNotNull(listener);
          return this;
      }
  
      public RequestHandler handler() {
          return handler;
      }
  
      public ActionContext handler(RequestHandler handler) {
          this.handler = $.requireNotNull(handler);
          return this;
      }
  
      @Override
      public RouteSource routeSource() {
          return routeSource;
      }
  
      @Override
      public ActionContext routeSource(RouteSource routeSource) {
          this.routeSource = routeSource;
          return this;
      }
  
      public ActionContext patchedJsonBody(String patchedJsonBody) {
          this.patchedJsonBody = patchedJsonBody;
          return this;
      }
  
      public String patchedJsonBody() {
          return patchedJsonBody;
      }
  
      public H.Format accept() {
          return req().accept();
      }
  
      public ActionContext accept(H.Format fmt) {
          req().accept(fmt);
          return this;
      }
  
      public Boolean hasTemplate() {
          return hasTemplate;
      }
  
      public ActionContext setHasTemplate(boolean b) {
          hasTemplate = b;
          return this;
      }
  
      public ActionContext enableCache() {
          E.illegalStateIf(this.cacheEnabled, "cache already enabled in the action context");
          this.cacheEnabled = true;
          this.response = new ResponseCache(response);
          return this;
      }
  
      public void markRequireBodyParsing() {
          requireBodyParsing = true;
      }
  
      public boolean isAllowIgnoreParamNamespace() {
          return allowIgnoreParamNamespace;
      }
  
      public void markAllowQrCodeRendering() {
          allowQrCodeRendering = true;
      }
  
      public boolean allowQrCodeRendering() {
          return allowQrCodeRendering;
      }
  
      public void allowIgnoreParamNamespace() {
          allowIgnoreParamNamespace = true;
      }
  
      public void disallowIgnoreParamNamespace() {
          allowIgnoreParamNamespace = false;
      }
  
      public int pathVarCount() {
          return pathVarCount;
      }
  
      public boolean isPathVar(String name) {
          return pathVarNames.contains(name);
      }
  
      public String portId() {
          return router().portId();
      }
  
      public int port() {
          return router().port();
      }
  
      public UserAgent userAgent() {
          if (null == ua) {
              ua = UserAgent.parse(req().header(H.Header.Names.USER_AGENT));
          }
          return ua;
      }
  
      public boolean jsonEncoded() {
          return req().contentType().isSameTypeWith(JSON);
      }
  
      public boolean xmlEncoded() {
          return req().contentType().isSameTypeWith(XML);
      }
  
      public boolean acceptJson() {
          return accept().isSameTypeWith(JSON);
      }
  
      public boolean acceptXML() {
          return accept().isSameTypeWith(XML);
      }
  
      public boolean isAjax() {
          return req().isAjax();
      }
  
      public boolean isOptionsMethod() {
          return req().method() == H.Method.OPTIONS;
      }
  
      public void setLargeResponse() {
          isLargeResponse = true;
      }
  
      public void setReflectedHandlerInvoker(ReflectedHandlerInvoker invoker) {
          this.reflectedHandlerInvoker = invoker;
      }
  
      public void setLargeResponseHint() {
          if (null != reflectedHandlerInvoker) {
              reflectedHandlerInvoker.setLargeResponseHint();
          }
      }
  
      public boolean isLargeResponse() {
          return isLargeResponse;
      }
  
      public String username() {
          return session().get(sessionKeyUsername);
      }
  
      public boolean isLoggedIn() {
          return S.notBlank(username());
      }
  
      public String body() {
          return paramVal(REQ_BODY);
      }
  
      public ActionContext param(String name, String value) {
          extraParams.put(name, value);
          return this;
      }
  
      public ActionContext urlPathParam(String name, String value) {
          pathVarCount++;
          pathVarNames.add(name);
          return param(name, value);
      }
  
      /**
       * Get all parameter keys.
       * @return a set of parameter keys
       */
      @Override
      public Set<String> paramKeys() {
          Set<String> set = new HashSet<String>();
          set.addAll(C.<String>list(request.paramNames()));
          set.addAll(extraParams.keySet());
          set.addAll(bodyParams().keySet());
          set.remove("_method");
          set.remove("_body");
          return set;
      }
  
      /**
       * Get all parameter names.
       *
       * This method is an alias to {@link #paramKeys()}. We provide this method
       * to make it consistent with {@link H.Request#paramNames()}.
       *
       * @return a set of parameter names
       */
      public Set<String> paramNames() {
          return paramKeys();
      }
  
      /**
       * Returns the param value associated with `__path`, i.e. calling to
       * {@link #paramVal(String)} with `__path` as param name.
       *
       * However, this method will do sanity check on the value returned, in case
       * there are `../` found in the value, an `BadRequest` will
       * be thrown out. This is to prevent the insecure direct object reference
       * attack.
       *
       * @return the param value of `__path`
       * @throws org.osgl.mvc.result.BadRequest when the value contains string `..`
       */
      public String __pathParamVal() {
          String s = paramVal(ParamNames.PATH);
          if (null == s) {
              return s;
          }
          badRequestIf(s.contains("../"), "`../` found in path which is not allowed");
          return s;
      }
  
      @Override
      public String paramVal(String name) {
          String val = _paramVal(name);
          return null != val ? val : allowIgnoreParamNamespace && name.contains(".") ? _paramVal(S.cut(name).afterFirst(".")) : null;
      }
  
      private String _paramVal(String name) {
          String val = extraParams.get(name);
          if (null != val) {
              return val;
          }
          val = request.paramVal(name);
          if (null == val) {
              String[] sa = getBody(name);
              if (null != sa && sa.length > 0) {
                  val = sa[0];
              }
          }
          return val;
      }
  
      public String paramValwithoutBodyParsing(String name) {
          String val = extraParams.get(name);
          if (null != val) {
              return val;
          }
          val = request.paramVal(name);
          if (null == val && null != bodyParams) {
              String[] sa = getBody(name);
              if (null != sa && sa.length > 0) {
                  val = sa[0];
              }
          }
          return val;
      }
  
      public String[] paramVals(String name) {
          String val = extraParams.get(name);
          if (null != val) {
              return new String[]{val};
          }
          String[] sa = request.paramVals(name);
          return null == sa ? getBody(name) : sa;
      }
  
      private String[] getBody(String name) {
          Map<String, String[]> body = bodyParams();
          String[] sa = body.get(name);
          return null == sa ? new String[0] : sa;
      }
  
      private Map<String, String[]> bodyParams() {
          if (null == bodyParams) {
              Map<String, String[]> map = new HashMap<>();
              H.Method method = request.method();
              if (H.Method.POST == method || H.Method.PUT == method || H.Method.PATCH == method || H.Method.DELETE == method) {
                  RequestBodyParser parser = RequestBodyParser.get(request);
                  map = parser.parse(this);
              }
              bodyParams = map;
              router.markRequireBodyParsing(handler);
          }
          return bodyParams;
      }
  
      public Map<String, String[]> allParams() {
          return allParams;
      }
  
      public ISObject upload(String name) {
          Integer index = attribute(ATTR_CURRENT_FILE_INDEX);
          if (null == index) {
              index = 0;
          }
          return upload(name, index);
      }
  
      public ISObject upload(String name, int index) {
          body();
          ISObject[] a = uploads.get(name);
          return null != a && a.length > index ? a[index] : null;
      }
  
      public ActionContext addUpload(String name, ISObject sobj) {
          ISObject[] a = uploads.get(name);
          if (null == a) {
              a = new ISObject[1];
              a[0] = sobj;
          } else {
              ISObject[] newA = new ISObject[a.length + 1];
              System.arraycopy(a, 0, newA, 0, a.length);
              newA[a.length] = sobj;
              a = newA;
          }
          uploads.put(name, a);
          return this;
      }
  
      public H.Status successStatus() {
          if (null != forceResponseStatus) {
              return forceResponseStatus;
          }
          return H.Method.POST == req().method() ? H.Status.CREATED : H.Status.OK;
      }
  
      public ActionContext forceResponseStatus(H.Status status) {
          this.forceResponseStatus = $.requireNotNull(status);
          return this;
      }
  
      public Result nullValueResult() {
          if (hasRenderArgs()) {
              RenderAny result = new RenderAny();
              if (renderArgs().size() == fieldOutputVarCount() && req().isAjax()) {
                  result.ignoreMissingTemplate();
              }
              return result;
          }
          return nullValueResultIgnoreRenderArgs();
      }
  
      public Result nullValueResultIgnoreRenderArgs() {
          if (null != forceResponseStatus) {
              return new Result(forceResponseStatus) {
              };
          } else {
              if (req().method() == H.Method.POST) {
                  H.Format accept = accept();
                  if (accept.isSameTypeWith(JSON)) {
                      return CREATED_JSON;
                  } else if (accept.isSameTypeWith(XML)) {
                      return CREATED_XML;
                  } else {
                      return CREATED;
                  }
              } else {
                  return NO_CONTENT;
              }
          }
      }
  
      public void preCheckCsrf() {
          if (!disableCsrf) {
              handler().csrfSpec().preCheck(this);
          }
      }
  
      public void checkCsrf(H.Session session) {
          if (!disableCsrf) {
              handler().csrfSpec().check(this, session);
          }
      }
  
      public void setCsrfCookieAndRenderArgs() {
          handler().csrfSpec().setCookieAndRenderArgs(this);
      }
  
      public void disableCORS() {
          this.disableCors = true;
      }
  
      /**
       * Apply content type to response with result provided.
       *
       * If `result` is an error then it might not apply content type as requested:
       * * If request is not ajax request, then use `text/html`
       * * If request is ajax request then apply requested content type only when `json` or `xml` is requested
       * * otherwise use `text/html`
       *
       * @param result
       *         the result used to check if it is error result
       * @return this `ActionContext`.
       */
      public ActionContext applyContentType(Result result) {
          if (!result.status().isError()) {
              return applyContentType();
          }
          return applyContentType(contentTypeForErrorResult(req()));
      }
  
      private static boolean isErrorCompatible(H.Format fmt) {
          return fmt.isSameTypeWithAny(JSON, HTML, CSV, TXT, XML);
      }
  
      public static H.Format contentTypeForErrorResult(H.Request<?> req) {
          H.Format fmt = req.accept();
          if (req.isAjax()) {
              if (fmt.isSameTypeWith(UNKNOWN)) {
                  fmt = req.contentType();
              }
              if (fmt.isSameTypeWithAny(XML, JSON)) {
                  return fmt;
              }
          }
          if (!isErrorCompatible(fmt)) {
              fmt = JSON;
          }
          return fmt;
      }
  
      public ActionContext applyContentType() {
          ActResponse resp = resp();
          H.Format lastContentType = resp.lastContentType();
          if (null != lastContentType && !lastContentType.isSameTypeWith(UNKNOWN)) {
              resp.commitContentType();
              return this;
          }
          H.Request req = req();
          H.Format fmt = req.accept();
          if (fmt.isSameTypeWith(UNKNOWN)) {
              fmt = req.contentType();
          }
          applyContentType(fmt);
          return this;
      }
  
      public ActionContext applyCorsSpec() {
          RequestHandler handler = handler();
          if (null != handler) {
              CORS.Spec spec = handler.corsSpec();
              spec.applyTo(this);
          }
          applyGlobalCorsSetting();
          return this;
      }
  
      public ActionContext applyContentSecurityPolicy() {
          RequestHandler handler = handler();
          if (null != handler) {
              boolean disableCSP = handler.disableContentSecurityPolicy();
              if (disableCSP) {
                  return this;
              }
              String csp = handler.contentSecurityPolicy();
              if (S.notBlank(csp)) {
                  H.Response resp = resp();
                  resp.addHeaderIfNotAdded(CONTENT_SECURITY_POLICY, csp);
              }
          }
          applyGlobalCspSetting();
          return this;
      }
  
      private ActionContext applyContentType(H.Format fmt) {
          if (null != fmt) {
              ActResponse resp = response;
              resp.initContentType(fmt.contentType());
              resp.commitContentType();
          }
          return this;
      }
  
      private void applyGlobalCspSetting() {
          String csp = config().contentSecurityPolicy();
          if (S.blank(csp)) {
              return;
          }
          H.Response r = resp();
          r.addHeaderIfNotAdded(CONTENT_SECURITY_POLICY, csp);
      }
  
      private void applyGlobalCorsSetting() {
          if (this.disableCors) {
              return;
          }
          AppConfig conf = config();
          if (!conf.corsEnabled()) {
              return;
          }
          H.Response r = response;
          r.addHeaderIfNotAdded(ACCESS_CONTROL_ALLOW_ORIGIN, conf.corsAllowOrigin());
          r.addHeaderIfNotAdded(ACCESS_CONTROL_ALLOW_CREDENTIALS, S.string(conf.corsAllowCredentials()));
          r.addHeaderIfNotAdded(ACCESS_CONTROL_EXPOSE_HEADERS, conf.corsExposeHeaders());
          if (request.method() == H.Method.OPTIONS || !conf.corsOptionCheck()) {
              r.addHeaderIfNotAdded(ACCESS_CONTROL_ALLOW_HEADERS, conf.corsAllowHeaders());
              r.addHeaderIfNotAdded(ACCESS_CONTROL_MAX_AGE, S.string(conf.corsMaxAge()));
          }
      }
  
      /**
       * Called by bytecode enhancer to set the name list of the render arguments that is update
       * by the enhancer
       *
       * @param names
       *         the render argument names separated by ","
       * @return this AppContext
       */
      public ActionContext __appRenderArgNames(String names) {
          return super.__appRenderArgNames(names);
      }
  
      public ActionContext __controllerInstance(Class<?> controllerClass, Object instance) {
          if (null == controllerInstances) {
              controllerInstances = new HashMap<>();
          }
          controllerInstances.put(controllerClass, instance);
          return this;
      }
  
      public Object __controllerInstance(Class<?> controllerClass) {
          if (null == controllerInstances) {
              return null;
          }
          Object inst = controllerInstances.get(controllerClass);
          if (null != inst) {
              return inst;
          }
          for (Object o : controllerInstances.values()) {
              if (controllerClass.isInstance(o)) {
                  return o;
              }
          }
          return null;
      }
  
      public ActionContext handlerClass(Class<?> clazz) {
          this.handlerClass = clazz;
          return this;
      }
  
      public Class<?> handlerClass() {
          return handlerClass;
      }
  
  
      /**
       * Return cached object by key. The key will be concatenated with
       * current session id when fetching the cached object
       *
       * @param key
       * @param <T>
       *         the object type
       * @return the cached object
       */
      public <T> T cached(String key) {
          H.Session sess = session();
          if (null != sess) {
              return sess.cached(key);
          } else {
              return app().cache().get(key);
          }
      }
  
      /**
       * Add an object into cache by key. The key will be used in conjunction with session id if
       * there is a session instance
       *
       * @param key
       *         the key to index the object within the cache
       * @param obj
       *         the object to be cached
       */
      public void cache(String key, Object obj) {
          H.Session sess = session();
          if (null != sess) {
              sess.cache(key, obj);
          } else {
              app().cache().put(key, obj);
          }
      }
  
      /**
       * Add an object into cache by key with expiration time specified
       *
       * @param key
       *         the key to index the object within the cache
       * @param obj
       *         the object to be cached
       * @param expiration
       *         the seconds after which the object will be evicted from the cache
       */
      public void cache(String key, Object obj, int expiration) {
          H.Session session = this.session;
          if (null != session) {
              session.cache(key, obj, expiration);
          } else {
              app().cache().put(key, obj, expiration);
          }
      }
  
      /**
       * Add an object into cache by key and expired after one hour
       *
       * @param key
       *         the key to index the object within the cache
       * @param obj
       *         the object to be cached
       */
      public void cacheForOneHour(String key, Object obj) {
          cache(key, obj, 60 * 60);
      }
  
      /**
       * Add an object into cache by key and expired after half hour
       *
       * @param key
       *         the key to index the object within the cache
       * @param obj
       *         the object to be cached
       */
      public void cacheForHalfHour(String key, Object obj) {
          cache(key, obj, 30 * 60);
      }
  
      /**
       * Add an object into cache by key and expired after 10 minutes
       *
       * @param key
       *         the key to index the object within the cache
       * @param obj
       *         the object to be cached
       */
      public void cacheForTenMinutes(String key, Object obj) {
          cache(key, obj, 10 * 60);
      }
  
      /**
       * Add an object into cache by key and expired after one minute
       *
       * @param key
       *         the key to index the object within the cache+
       * @param obj
       *         the object to be cached
       */
      public void cacheForOneMinute(String key, Object obj) {
          cache(key, obj, 60);
      }
  
      /**
       * Evict cached object
       *
       * @param key
       *         the key indexed the cached object to be evicted
       */
      public void evictCache(String key) {
          H.Session sess = session();
          if (null != sess) {
              sess.evict(key);
          } else {
              app().cache().evict(key);
          }
      }
  
      public S.Buffer buildViolationMessage(S.Buffer builder) {
          return buildViolationMessage(builder, "\n");
      }
  
      public S.Buffer buildViolationMessage(S.Buffer builder, String separator) {
          return buildViolationMessage(violations(), builder, separator);
      }
  
      public static S.Buffer buildViolationMessage(Map<String, ConstraintViolation> violations, S.Buffer builder, String separator) {
          if (violations.isEmpty()) return builder;
          for (Map.Entry<String, ConstraintViolation> entry : violations.entrySet()) {
              String message = entry.getValue().getMessage();
              if (!message.startsWith(":")) {
                  builder.append(entry.getKey()).append(": ").append(message);
              } else {
                  builder.append(message.substring(1));
              }
              builder.append(separator);
          }
          int n = builder.length();
          builder.delete(n - separator.length(), n);
          return builder;
      }
  
      public String violationMessage(String separator) {
          return buildViolationMessage(S.newBuffer(), separator).toString();
      }
  
      public String violationMessage() {
          return violationMessage("\n");
      }
  
      public ActionContext flashViolationMessage() {
          return flashViolationMessage("\n");
      }
  
      public ActionContext flashViolationMessage(String separator) {
          if (violations().isEmpty()) return this;
          flash().error(violationMessage(separator));
          return this;
      }
  
      public String actionPath() {
          return actionPath;
      }
  
      public ActionContext actionPath(String path) {
          actionPath = path;
          return this;
      }
  
      @Override
      public String methodPath() {
          return actionPath;
      }
  
      public void startIntercepting() {
          state = State.INTERCEPTING;
      }
  
      public void startHandling() {
          state = State.HANDLING;
      }
  
      /**
       * Update the context session to mark a user logged in
       *
       * @param userIdentifier
       *         the user identifier, could be either userId or username
       */
      public void login(Object userIdentifier) {
          session().put(config().sessionKeyUsername(), userIdentifier);
          app().eventBus().trigger(new LoginEvent(userIdentifier.toString()));
      }
  
      /**
       * Return the logged in user.
       *
       * Note this API returns the string set to context with {@link #login(Object)} call.
       *
       * @return the logged in user
       */
      public String loginUser() {
          return session.get(config().sessionKeyUsername());
      }
  
      /**
       * Login the user and redirect back to original URL
       *
       * @param userIdentifier
       *         the user identifier, could be either userId or username
       */
      public void loginAndRedirectBack(Object userIdentifier) {
          login(userIdentifier);
          RedirectToLoginUrl.redirectToOriginalUrl(this);
      }
  
      /**
       * Login the user and redirect back to original URL. If no
       * original URL found then redirect to `defaultLandingUrl`.
       *
       * @param userIdentifier
       *         the user identifier, could be either userId or username
       * @param defaultLandingUrl
       *         the URL to be redirected if original URL not found
       */
      public void loginAndRedirectBack(Object userIdentifier, String defaultLandingUrl) {
          login(userIdentifier);
          RedirectToLoginUrl.redirectToOriginalUrl(this, defaultLandingUrl);
      }
  
      /**
       * Login the user and redirect to specified URL
       *
       * @param userIdentifier
       *         the user identifier, could be either userId or username
       * @param url
       *         the URL to be redirected to
       */
      public void loginAndRedirect(Object userIdentifier, String url) {
          login(userIdentifier);
          throw redirect(url);
      }
  
      /**
       * Logout the current session. After calling this method,
       * the session will be cleared
       */
      public void logout() {
          String userIdentifier = session.get(config().sessionKeyUsername());
          SessionManager sessionManager = app().sessionManager();
          sessionManager.logout(session);
          if (S.notBlank(userIdentifier)) {
              app().eventBus().trigger(new LogoutEvent(userIdentifier));
          }
      }
  
      /**
       * Initialize params/renderArgs/attributes and then
       * resolve session and flash from cookies
       */
      public void resolve() {
          E.illegalStateIf(state != State.CREATED);
          localeResolver.resolve();
          boolean sessionFree = handler.sessionFree() || sessionPassThrough;
          setWasUnauthenticated();
          H.Request req = req();
          if (!sessionFree) {
              resolveSession(req);
              app().eventBus().emit(new PreFireSessionResolvedEvent(session, this));
              resolveFlash(req);
          }
          state = State.SESSION_RESOLVED;
          if (!sessionFree) {
              handler.prepareAuthentication(this);
              app().eventBus().emit(new SessionResolvedEvent(session, this));
              if (isLoggedIn()) {
                  clearWasUnauthenticated();
              }
          }
      }
  
      public void proceedWithHandler(RequestHandler handler) {
          /**
           * TODO: fix Dalian-Dong issue
           if (requireBodyParsing) {
           ((RequestImplBase) req()).receiveFullBytesAndProceed(this, handler);
           } else {
           handler.handle(this);
           }
           */
          handler.handle(this);
      }
  
      @Override
      public Locale locale(boolean required) {
          if (required) {
              if (null == locale()) {
                  localeResolver.resolve();
              }
          }
          return super.locale(required);
      }
  
      public void logAccess(H.Response response) {
          if (null != accessLog) {
              accessLog.logWithResponseCode(response.statusCode());
          }
      }
  
      /**
       * Dissolve session and flash into cookies.
       * <p><b>Note</b> this method must be called
       * before any content has been committed to
       * response output stream/writer</p>
       */
      public void dissolve() {
          if (state == State.SESSION_DISSOLVED) {
              return;
          }
          if (handler.sessionFree() || sessionPassThrough) {
              return;
          }
          if (null == session) {
              // only case is when CSRF token check failed
              // while resolving session
              // we need to generate new session anyway
              // because it is required to cache the
              // original URL
              // see RedirectToLoginUrl
              session = new H.Session();
          }
          localeResolver.dissolve();
          app().eventBus().emit(new SessionWillDissolveEvent(this));
          try {
              setCsrfCookieAndRenderArgs();
              sessionManager().dissolveState(session(), flash(), resp());
  //            dissolveFlash();
  //            dissolveSession();
              state = State.SESSION_DISSOLVED;
          } finally {
              app().eventBus().emit(new SessionDissolvedEvent(this));
          }
      }
  
      /**
       * Clear all internal data store/cache and then
       * remove this context from thread local
       */
      @Override
      protected void releaseResources() {
          super.releaseResources();
          PropertySpec.current.remove();
          PropertySpec.currentSpec.remove();
          if (this.state != State.DESTROYED) {
              ParamValueLoaderService.clearParamTree();
              sessionManager = null;
              this.allParams = null;
              this.extraParams.clear();
              this.extraParams = null;
              this.requestParamCache = null;
              this.router = null;
              this.handler = null;
              // xio impl might need this this.request = null;
              // xio impl might need this this.response = null;
              this.flash = null;
              this.session = null;
              this.result = null;
              this.uploads.clear();
              this.uploads = null;
              this.request = null;
              this.response = null;
              this.actionPath = null;
              this.urlContext = null;
              this.urlPath = null;
              if (null != this.pathVarNames) {
                  this.pathVarNames.clear();
                  this.pathVarNames = null;
              }
              if (null != this.controllerInstances) {
                  this.controllerInstances.clear();
                  this.controllerInstances = null;
              }
              this.reflectedHandlerInvoker = null;
              this.handlerClass = null;
              this.encodedSessionToken = null;
              this.localeResolver = null;
              if (null != this.handleTimer) {
                  this.handleTimer.stop();
                  this.handleTimer = null;
              }
              ActionContext.clearLocal();
          }
          this.state = State.DESTROYED;
      }
  
      @Override
      public Class<? extends Annotation> scope() {
          return RequestScoped.class;
      }
  
      public void saveLocal() {
          _local.set(this);
      }
  
      public static void clearLocal() {
          clearCurrent();
      }
  
      private Set<Map.Entry<String, String[]>> requestParamCache() {
          if (null != requestParamCache) {
              return requestParamCache;
          }
          requestParamCache = new HashSet<>();
          Map<String, String[]> map = new HashMap<>();
          // url queries
          Iterator<String> paramNames = request.paramNames().iterator();
          while (paramNames.hasNext()) {
              final String key = paramNames.next();
              final String[] val = request.paramVals(key);
              MapUtil.mergeValueInMap(map, key, val);
          }
          // post bodies
          Map<String, String[]> map2 = bodyParams();
          for (String key : map2.keySet()) {
              String[] val = map2.get(key);
              if (null != val) {
                  MapUtil.mergeValueInMap(map, key, val);
              }
          }
          requestParamCache.addAll(map.entrySet());
          return requestParamCache;
      }
  
      private void _init() {
          uploads = new HashMap<>();
          extraParams = new HashMap<>();
          final Set<Map.Entry<String, String[]>> paramEntrySet = new AbstractSet<Map.Entry<String, String[]>>() {
              @Override
              public Iterator<Map.Entry<String, String[]>> iterator() {
                  final Iterator<Map.Entry<String, String[]>> extraItr = new Iterator<Map.Entry<String, String[]>>() {
                      Iterator<Map.Entry<String, String>> parent = extraParams.entrySet().iterator();
  
                      @Override
                      public boolean hasNext() {
                          return parent.hasNext();
                      }
  
                      @Override
                      public Map.Entry<String, String[]> next() {
                          final Map.Entry<String, String> parentEntry = parent.next();
                          return new Map.Entry<String, String[]>() {
                              @Override
                              public String getKey() {
                                  return parentEntry.getKey();
                              }
  
                              @Override
                              public String[] getValue() {
                                  return new String[]{parentEntry.getValue()};
                              }
  
                              @Override
                              public String[] setValue(String[] value) {
                                  throw E.unsupport();
                              }
                          };
                      }
  
                      @Override
                      public void remove() {
                          throw E.unsupport();
                      }
                  };
                  final Iterator<Map.Entry<String, String[]>> reqParamItr = requestParamCache().iterator();
                  return new Iterator<Map.Entry<String, String[]>>() {
                      @Override
                      public boolean hasNext() {
                          return extraItr.hasNext() || reqParamItr.hasNext();
                      }
  
                      @Override
                      public Map.Entry<String, String[]> next() {
                          if (extraItr.hasNext()) return extraItr.next();
                          return reqParamItr.next();
                      }
  
                      @Override
                      public void remove() {
                          throw E.unsupport();
                      }
                  };
              }
  
              @Override
              public int size() {
                  int size = extraParams.size();
                  if (null != request) {
                      size += requestParamCache().size();
                  }
                  return size;
              }
          };
  
          allParams = new AbstractMap<String, String[]>() {
              @Override
              public Set<Entry<String, String[]>> entrySet() {
                  return paramEntrySet;
              }
          };
      }
  
      private void resolveSession(H.Request req) {
          preCheckCsrf();
          session = sessionManager().resolveSession(req, this);
          checkCsrf(session);
      }
  
      private void resolveFlash(H.Request req) {
          //this.flash = Act.sessionManager().resolveFlash(this);
          flash = sessionManager().resolveFlash(req);
      }
  
      private SessionManager sessionManager() {
          if (null == sessionManager || sessionManager.isDestroyed()) {
              // in case session manager get destroyed during hot reload
              sessionManager = app().sessionManager();
          }
          return sessionManager;
      }
  
      //    private void dissolveSession() {
  //        Cookie c = Act.sessionManager().dissolveSession(this);
  //        if (null != c) {
  //            config().sessionMapper().serializeSession(c, this);
  //        }
  //    }
  //
  //    private void dissolveFlash() {
  //        Cookie c = Act.sessionManager().dissolveFlash(this);
  //        if (null != c) {
  //            config().sessionMapper().serializeFlash(c, this);
  //        }
  //    }
  //
      private static ContextLocal<ActionContext> _local = $.contextLocal();
  
      public static final String METHOD_GET_CURRENT = "current";
  
      public static ActionContext current() {
          return _local.get();
      }
  
      public static void clearCurrent() {
          _local.remove();
      }
  
      /**
       * Create an new {@code AppContext} and return the new instance
       */
      public static ActionContext create(App app, H.Request request, ActResponse<?> resp) {
          return new ActionContext(app, request, resp);
      }
  
      public enum State {
          CREATED,
          SESSION_RESOLVED,
          SESSION_DISSOLVED,
          INTERCEPTING,
          HANDLING,
          DESTROYED;
  
          public boolean isHandling() {
              return this == HANDLING;
          }
  
          public boolean isIntercepting() {
              return this == INTERCEPTING;
          }
      }
  
      public static class ActionContextEvent extends ActEvent<ActionContext> implements SystemEvent {
          public ActionContextEvent(ActionContext source) {
              super(source);
          }
  
          public ActionContext context() {
              return source();
          }
      }
  
      private static class SessionEvent extends ActionContextEvent {
          private H.Session session;
  
          public SessionEvent(H.Session session, ActionContext source) {
              super(source);
              this.session = session;
          }
  
          public H.Session session() {
              return session;
          }
      }
  
      /**
       * This event is fired after session resolved and before
       * context state changed to {@link State#SESSION_RESOLVED}
       * and in turn before {@link SessionResolvedEvent} is fired.
       */
      public static class PreFireSessionResolvedEvent extends SessionEvent {
          public PreFireSessionResolvedEvent(H.Session session, ActionContext context) {
              super(session, context);
          }
  
          @Override
          public Class<? extends ActEvent<ActionContext>> eventType() {
              return PreFireSessionResolvedEvent.class;
          }
      }
  
      /**
       * This event is fired after session resolved after
       * all event listeners that listen to the
       * {@link PreFireSessionResolvedEvent} get notified.
       */
      public static class SessionResolvedEvent extends SessionEvent {
          public SessionResolvedEvent(H.Session session, ActionContext context) {
              super(session, context);
          }
  
          @Override
          public Class<? extends ActEvent<ActionContext>> eventType() {
              return SessionResolvedEvent.class;
          }
      }
  
      public static class SessionWillDissolveEvent extends ActionContextEvent {
          public SessionWillDissolveEvent(ActionContext source) {
              super(source);
          }
  
          @Override
          public Class<? extends ActEvent<ActionContext>> eventType() {
              return SessionWillDissolveEvent.class;
          }
      }
  
      public static class SessionDissolvedEvent extends ActionContextEvent {
          public SessionDissolvedEvent(ActionContext source) {
              super(source);
          }
  
          @Override
          public Class<? extends ActEvent<ActionContext>> eventType() {
              return SessionDissolvedEvent.class;
          }
      }
  }
  