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 act.Act;
import act.app.ActionContext;
import act.app.App;
import act.app.AppClassLoader;
import act.controller.CacheSupportMetaInfo;
import act.controller.Controller;
import act.controller.annotation.HandleCsrfFailure;
import act.controller.annotation.HandleMissingAuthentication;
import act.controller.annotation.TemplateContext;
import act.controller.meta.*;
import act.handler.NonBlock;
import act.handler.PreventDoubleSubmission;
import act.handler.builtin.controller.*;
import act.inject.param.JsonDTO;
import act.inject.param.JsonDTOClassManager;
import act.inject.param.ParamValueLoaderManager;
import act.inject.param.ParamValueLoaderService;
import act.security.CORS;
import act.security.CSRF;
import act.sys.Env;
import act.util.*;
import act.view.*;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.esotericsoftware.reflectasm.MethodAccess;
import org.osgl.$;
import org.osgl.Osgl;
import org.osgl.exception.NotAppliedException;
import org.osgl.http.H;
import org.osgl.inject.BeanSpec;
import org.osgl.mvc.annotation.ResponseContentType;
import org.osgl.mvc.annotation.ResponseStatus;
import org.osgl.mvc.annotation.SessionFree;
import org.osgl.mvc.result.BadRequest;
import org.osgl.mvc.result.Conflict;
import org.osgl.mvc.result.NotFound;
import org.osgl.mvc.result.Result;
import org.osgl.util.C;
import org.osgl.util.E;
import org.osgl.util.S;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Implement handler using
* https://github.com/EsotericSoftware/reflectasm
*/
public class ReflectedHandlerInvoker<M extends HandlerMethodMetaInfo> extends DestroyableBase
implements ActionHandlerInvoker, AfterInterceptorInvoker, ExceptionInterceptorInvoker {
private static final Object[] DUMP_PARAMS = new Object[0];
private ClassLoader cl;
private ControllerClassMetaInfo controller;
private Class<?> controllerClass;
private MethodAccess methodAccess;
private M handler;
private int handlerIndex;
private ConcurrentMap<H.Format, Boolean> templateCache = new ConcurrentHashMap<>();
protected Method method; //
private ParamValueLoaderService paramLoaderService;
private JsonDTOClassManager jsonDTOClassManager;
private final int paramCount;
private final int fieldsAndParamsCount;
private String singleJsonFieldName;
private final boolean sessionFree;
private final boolean express;
private List<BeanSpec> paramSpecs;
private Set<String> pathVariables;
private CORS.Spec corsSpec;
private CSRF.Spec csrfSpec;
private String jsonDTOKey;
private boolean isStatic;
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 templateContext;
private MissingAuthenticationHandler missingAuthenticationHandler;
private MissingAuthenticationHandler csrfFailureHandler;
private ReflectedHandlerInvoker(M handlerMetaInfo, App app) {
this.cl = app.classLoader();
this.handler = handlerMetaInfo;
this.controller = handlerMetaInfo.classInfo();
this.controllerClass = $.classForName(controller.className(), cl);
this.disabled = !Env.matches(controllerClass);
this.paramLoaderService = app.service(ParamValueLoaderManager.class).get(ActionContext.class);
this.jsonDTOClassManager = app.service(JsonDTOClassManager.class);
Class[] paramTypes = paramTypes(cl);
try {
method = controllerClass.getMethod(handlerMetaInfo.name(), paramTypes);
this.disabled = this.disabled || !Env.matches(method);
} catch (NoSuchMethodException e) {
throw E.unexpected(e);
}
this.isStatic = handlerMetaInfo.isStatic();
if (!this.isStatic) {
//constructorAccess = ConstructorAccess.get(controllerClass);
methodAccess = MethodAccess.get(controllerClass);
handlerIndex = methodAccess.getIndex(handlerMetaInfo.name(), paramTypes);
} else {
method.setAccessible(true);
}
sessionFree = method.isAnnotationPresent(SessionFree.class);
express = method.isAnnotationPresent(NonBlock.class);
paramCount = handler.paramCount();
paramSpecs = jsonDTOClassManager.beanSpecs(controllerClass, method);
fieldsAndParamsCount = paramSpecs.size();
if (fieldsAndParamsCount == 1) {
singleJsonFieldName = paramSpecs.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;
this.jsonDTOKey = app.cuid();
this.singleton = singleton(app);
ResponseContentType contentType = getAnnotation(ResponseContentType.class);
if (null != contentType) {
forceResponseContentType = contentType.value().format();
}
ResponseStatus status = method.getAnnotation(ResponseStatus.class);
if (null != status) {
forceResponseStatus = H.Status.of(status.value());
}
PreventDoubleSubmission dsp = method.getAnnotation(PreventDoubleSubmission.class);
if (null != dsp) {
dspToken = dsp.value();
if (PreventDoubleSubmission.DEFAULT.equals(dspToken)) {
dspToken = app.config().dspToken();
}
}
initOutputVariables();
initCacheParams();
checkTemplateContext();
initMissingAuthenticationAndCsrfCheckHandler();
}
@Override
protected void releaseResources() {
cl = null;
controller = null;
controllerClass = null;
method = null;
methodAccess = null;
handler.destroy();
handler = null;
cacheSupport = null;
super.releaseResources();
}
@Override
public int priority() {
return handler.priority();
}
public interface ReflectedHandlerInvokerVisitor extends Visitor, $.Func2<Class<?>, Method, Void> {
}
@Override
public void accept(Visitor visitor) {
ReflectedHandlerInvokerVisitor rv = (ReflectedHandlerInvokerVisitor) visitor;
rv.apply(controllerClass, method);
}
@Override
public CacheSupportMetaInfo cacheSupport() {
return cacheSupport;
}
@Override
public MissingAuthenticationHandler missingAuthenticationHandler() {
return missingAuthenticationHandler;
}
@Override
public MissingAuthenticationHandler csrfFailureHandler() {
return csrfFailureHandler;
}
public Result handle(ActionContext context) throws Exception {
if (disabled) {
return ActNotFound.get();
}
String urlContext = this.controller.contextPath();
if (S.notBlank(urlContext)) {
context.urlContext(urlContext);
}
context.attribute("reflected_handler", this);
if (null != templateContext && context.state().isHandling()) {
context.templateContext(templateContext);
}
preventDoubleSubmission(context);
processForceResponse(context);
ensureJsonDTOGenerated(context);
Object controller = controllerInstance(context);
/*
* We will send back response immediately when param validation
* failed in the following cases:
* a) this is a data endpoint and accept JSON data
* b) there is no template associated with the endpoint
* TODO: fix me - if method use arbitrary templates, then this check will fail
*/
boolean failOnViolation = context.acceptJson() || checkTemplate(context);
Object[] params = params(controller, context);
if (failOnViolation && context.hasViolation()) {
String msg = context.violationMessage(";");
return new BadRequest(msg);
}
if (hasOutputVar) {
fillOutputVariables(controller, params, context);
}
return invoke(handler, context, controller, params);
}
@Override
public Result handle(Result result, ActionContext actionContext) throws Exception {
actionContext.attribute(ActionContext.ATTR_RESULT, result);
return handle(actionContext);
}
@Override
public Result handle(Exception e, ActionContext actionContext) throws Exception {
actionContext.attribute(ActionContext.ATTR_EXCEPTION, e);
return handle(actionContext);
}
@Override
public boolean sessionFree() {
return sessionFree;
}
@Override
public boolean express() {
return express;
}
public CORS.Spec corsSpec() {
return corsSpec;
}
@Override
public CSRF.Spec csrfSpec() {
return csrfSpec;
}
public JsonDTO cachedJsonDTO(ActContext<?> context) {
return context.attribute(jsonDTOKey);
}
private void ensureJsonDTOGenerated(ActionContext context) {
if (0 == fieldsAndParamsCount || !context.jsonEncoded() || null != context.attribute(jsonDTOKey)) {
return;
}
Class<? extends JsonDTO> dtoClass = jsonDTOClassManager.get(controllerClass, method);
if (null == dtoClass) {
// there are neither fields nor params
return;
}
try {
JsonDTO dto = JSON.parseObject(patchedJsonBody(context), dtoClass);
context.attribute(jsonDTOKey, dto);
} catch (JSONException e) {
if (e.getCause() != null) {
App.LOGGER.warn(e.getCause(), "error parsing JSON data");
} else {
App.LOGGER.warn(e, "error parsing JSON data");
}
throw new BadRequest(e.getCause());
}
}
private int fieldsAndParamsCount(ActionContext context) {
if (fieldsAndParamsCount < 2) {
return fieldsAndParamsCount;
}
return fieldsAndParamsCount - pathVariables(context).size();
}
private Set<String> pathVariables(ActionContext context) {
if (null == pathVariables) {
pathVariables = context.attribute(ActionContext.ATTR_PATH_VARS);
}
return pathVariables;
}
private String singleJsonFieldName(ActionContext context) {
if (null != singleJsonFieldName) {
return singleJsonFieldName;
}
Set<String> set = context.paramKeys();
for (BeanSpec spec: paramSpecs) {
String name = spec.name();
if (!set.contains(name)) {
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 (S.blank(body) || 1 < fieldsAndParamsCount(context)) {
return body;
}
String theName = singleJsonFieldName(context);
int theNameLen = theName.length();
if (null == theName) {
return body;
}
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) {
if (c == '"') {
break;
}
int id = i - nameStart - 1;
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(ClassLoader cl) {
int sz = handler.paramCount();
Class[] ca = new Class[sz];
for (int i = 0; i < sz; ++i) {
HandlerParamMetaInfo param = handler.param(i);
ca[i] = $.classForName(param.type().getClassName(), cl);
}
return ca;
}
private void processForceResponse(ActionContext actionContext) {
if (null != forceResponseContentType) {
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;
}
String controllerName = controllerClass.getName();
Object inst = context.__controllerInstance(controllerName);
if (null == inst) {
inst = paramLoaderService.loadHostBean(controllerClass, context);
context.__controllerInstance(controllerName, inst);
}
return inst;
}
private void initCacheParams() {
CacheFor cacheFor = method.getAnnotation(CacheFor.class);
cacheSupport = null == cacheFor ? CacheSupportMetaInfo.disabled() : CacheSupportMetaInfo.enabled(
new CacheKeyBuilder(cacheFor, S.concat(controllerClass.getName(), ".", method.getName())),
cacheFor.value(),
cacheFor.supportPost()
);
}
private void checkTemplateContext() {
TemplateContext templateContext = method.getAnnotation(TemplateContext.class);
if (null != templateContext) {
this.templateContext = templateContext.value();
} else {
templateContext = controllerClass.getAnnotation(TemplateContext.class);
if (null != templateContext) {
this.templateContext = templateContext.value();
}
}
}
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);
}
}
}
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[][] 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 Result invoke(M handlerMetaInfo, ActionContext context, Object controller, Object[] params) throws Exception {
Object result;
if (null != methodAccess) {
try {
result = methodAccess.invoke(controller, handlerIndex, params);
} catch (Result r) {
return r;
}
} else {
try {
result = method.invoke(null, params);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof Result) {
return (Result) cause;
}
throw (Exception) cause;
}
}
if (null == result && handler.hasReturn() && !handler.returnTypeInfo().isResult()) {
// 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 notFoundOnMethod(null);
}
boolean hasTemplate = checkTemplate(context);
if (hasTemplate && result instanceof RenderAny) {
result = RenderTemplate.INSTANCE;
}
return Controller.Util.inferResult(handlerMetaInfo, result, context, hasTemplate);
}
public NotFound notFoundOnMethod(String message) {
return ActNotFound.create(method, message);
}
private boolean checkTemplate(ActionContext context) {
if (!context.state().isHandling()) {
//we don't check template on interceptors
return false;
}
Boolean hasTemplate = context.hasTemplate();
if (null != hasTemplate) {
return hasTemplate;
}
H.Format fmt = context.accept();
hasTemplate = templateCache.get(fmt);
if (null == hasTemplate || Act.isDev()) {
hasTemplate = probeTemplate(fmt, context);
templateCache.putIfAbsent(fmt, hasTemplate);
}
context.hasTemplate(hasTemplate);
return hasTemplate;
}
private boolean probeTemplate(H.Format fmt, ActionContext context) {
if (!TemplatePathResolver.isAcceptFormatSupported(fmt)) {
return false;
} else {
Template t = Act.viewManager().load(context);
return t != null;
}
}
private Object[] params(Object controller, ActionContext context) {
if (0 == paramCount) {
return DUMP_PARAMS;
}
return paramLoaderService.loadMethodParams(controller, method, context);
}
private Object singleton(App app) {
Object singleton = app.singleton(controllerClass);
if (null == singleton) {
// check if there are fields
List<Field> fields = $.fieldsOf(controllerClass, JsonDTOClassManager.CLASS_FILTER, JsonDTOClassManager.FIELD_FILTER);
if (fields.isEmpty()) {
singleton = app.getInstance(controllerClass);
}
boolean stateful = false;
for (Field field : fields) {
if (!isGlobal(field)) {
stateful = true;
break;
}
}
if (!stateful) {
singleton = app.getInstance(controllerClass);
}
}
return singleton;
}
private <T extends Annotation> T getAnnotation(Class<T> annoType) {
T anno = method.getAnnotation(annoType);
if (null == anno) {
anno = controllerClass.getAnnotation(annoType);
}
return anno;
}
private boolean isGlobal(Field field) {
if (null != field.getAnnotation(Global.class)) {
return true;
}
Class<?> fieldType = field.getType();
return fieldType.isAnnotationPresent(Stateless.class);
}
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 boolean sessionFree() {
return invoker.sessionFree();
}
@Override
public boolean express() {
return invoker.express();
}
@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 CORS.Spec corsSpec() {
return invoker.corsSpec();
}
@Override
public boolean sessionFree() {
return invoker.sessionFree();
}
@Override
public boolean express() {
return invoker.express();
}
@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());
AppClassLoader cl = App.instance().classLoader();
for (String cn : classNames) {
clsList.add((Class) $.classForName(cn, cl));
}
return clsList;
}
@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 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 CORS.Spec corsSpec() {
return invoker.corsSpec();
}
@Override
public boolean sessionFree() {
return invoker.sessionFree();
}
@Override
public boolean express() {
return invoker.express();
}
@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, Osgl.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));
}
}
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);
}
}
}
}