
com.appslandia.plum.base.ActionDescProvider Maven / Gradle / Ivy
// The MIT License (MIT)
// Copyright © 2015 AppsLandia. All rights reserved.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package com.appslandia.plum.base;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import com.appslandia.common.base.CaseInsensitiveMap;
import com.appslandia.common.base.InitializeObject;
import com.appslandia.common.base.Out;
import com.appslandia.common.utils.AssertUtils;
import com.appslandia.common.utils.CollectionUtils;
import com.appslandia.common.utils.SplitUtils;
import com.appslandia.common.utils.StringUtils;
import com.appslandia.common.utils.ValueUtils;
import com.appslandia.plum.utils.ServletUtils;
/**
*
* @author Loc Ha
*
*/
public abstract class ActionDescProvider extends InitializeObject {
private Map actionDescMap = new HashMap<>();
private String homeController;
private Map formLogins = new CaseInsensitiveMap<>();
@Inject
protected AppConfig appConfig;
@Override
protected void init() throws Exception {
this.actionDescMap = Collections.unmodifiableMap(this.actionDescMap);
this.formLogins = Collections.unmodifiableMap(this.formLogins);
}
public Map getActionDescMap() {
this.initialize();
return this.actionDescMap;
}
public ActionDesc getActionDesc(String controller, String action) {
this.initialize();
return this.actionDescMap.get(new ActionRoute(controller, action));
}
public ActionDesc getHomeDesc() {
this.initialize();
if (this.homeController == null) {
return null;
}
return this.actionDescMap.get(new ActionRoute(this.homeController, ServletUtils.ACTION_INDEX));
}
public String getHomeController() {
this.initialize();
return this.homeController;
}
public ActionDesc getFormLogin(String module) {
this.initialize();
return AssertUtils.assertNotNull(this.formLogins.get(module), "formLogin is required.");
}
protected void addControllerClass(Class> controllerClass) {
String controller = getController(controllerClass);
// Actions
for (Method actionMethod : controllerClass.getMethods()) {
if (actionMethod.getDeclaringClass() == Object.class) {
continue;
}
if (Modifier.isStatic(actionMethod.getModifiers())) {
continue;
}
String action = getAction(actionMethod);
// ActionDesc
ActionDesc actionDesc = new ActionDesc();
actionDesc.setController(controller);
actionDesc.setAction(action);
actionDesc.setMethod(actionMethod);
actionDesc.setControllerClass(controllerClass);
// Module
Controller controllerAnt = controllerClass.getDeclaredAnnotation(Controller.class);
String module = controllerAnt.module().isEmpty() ? this.appConfig.getModule() : controllerAnt.module();
actionDesc.setModule(AssertUtils.assertNotNull(module));
// HTTP Methods
Out httpMethod = new Out<>();
List allowMethods = parseAllowMethods(actionMethod, httpMethod);
// @ChildAction
ChildAction childAction = actionMethod.getDeclaredAnnotation(ChildAction.class);
if (!httpMethod.value && (childAction == null)) {
continue;
}
actionDesc.setChildAction(childAction);
// PathParams
PathParams pathParams = actionMethod.getDeclaredAnnotation(PathParams.class);
List parsedPathParams = (pathParams != null) ? parsePathParams(pathParams.value()) : null;
actionDesc.setPathParams(CollectionUtils.unmodifiable(parsedPathParams));
// ParamDescs
List parsedParamDescs = parseParamDescs(actionMethod, actionDesc.getPathParams());
actionDesc.setParamDescs(CollectionUtils.unmodifiable(parsedParamDescs));
// Accessible?
if (actionDesc.getChildAction() == null) {
actionDesc.setAllowMethods(CollectionUtils.unmodifiable(allowMethods));
// @ConsumeType
if (parsedParamDescs.stream().anyMatch(p -> (p.getModel() != null) && p.getModel().value() == Model.Source.JSON_BODY)) {
actionDesc.setConsumeType(ConsumeType.APP_JSON);
} else {
ConsumeType consumeType = ValueUtils.valueOrAlt(actionMethod.getDeclaredAnnotation(ConsumeType.class),
controllerClass.getDeclaredAnnotation(ConsumeType.class));
actionDesc.setConsumeType(consumeType);
}
// @EnableFilters
EnableFilters enableFilters = ValueUtils.valueOrAlt(actionMethod.getDeclaredAnnotation(EnableFilters.class),
controllerClass.getDeclaredAnnotation(EnableFilters.class));
actionDesc.setEnableFilters(enableFilters);
// @Authorize
Authorize authorize = ValueUtils.valueOrAlt(actionMethod.getDeclaredAnnotation(Authorize.class), controllerClass.getDeclaredAnnotation(Authorize.class));
if (authorize == null) {
if (this.appConfig.getBool(AppConfig.CONFIG_ENABLE_AUTHORIZE, false)) {
authorize = Authorize.IMPL;
}
}
actionDesc.setAuthorize(((authorize != null) && !authorize.removed()) ? authorize : null);
// @CacheControl
CacheControl cacheControl = ValueUtils.valueOrAlt(actionMethod.getDeclaredAnnotation(CacheControl.class),
controllerClass.getDeclaredAnnotation(CacheControl.class));
if (cacheControl != null) {
if (!cacheControl.nocache()) {
actionDesc.setCacheControl(cacheControl);
AssertUtils.assertNotBlank(cacheControl.value());
} else {
actionDesc.setCacheControl(CacheControl.NO_CACHE_IMPL);
}
}
// @EnableCors
EnableCors enableCors = ValueUtils.valueOrAlt(actionMethod.getDeclaredAnnotation(EnableCors.class), controllerClass.getDeclaredAnnotation(EnableCors.class));
if (enableCors == null) {
if (this.appConfig.getBool(AppConfig.CONFIG_ENABLE_CORS, false)) {
enableCors = EnableCors.IMPL;
}
}
actionDesc.setEnableCors(((enableCors != null) && !enableCors.removed()) ? enableCors : null);
// @EnableHttps
EnableHttps enableHttps = ValueUtils.valueOrAlt(actionMethod.getDeclaredAnnotation(EnableHttps.class), controllerClass.getDeclaredAnnotation(EnableHttps.class));
if (enableHttps == null) {
if (this.appConfig.getBool(AppConfig.CONFIG_ENABLE_HTTPS, false)) {
enableHttps = EnableHttps.IMPL;
}
}
actionDesc.setEnableHttps(((enableHttps != null) && !enableHttps.removed()) ? enableHttps : null);
// @EnableGzip
EnableGzip enableGzip = ValueUtils.valueOrAlt(actionMethod.getDeclaredAnnotation(EnableGzip.class), controllerClass.getDeclaredAnnotation(EnableGzip.class));
if (enableGzip == null) {
if (this.appConfig.getBool(AppConfig.CONFIG_ENABLE_GZIP, false)) {
enableGzip = EnableGzip.IMPL;
}
}
actionDesc.setEnableGzip(((enableGzip != null) && !enableGzip.removed()) ? enableGzip : null);
// @EnableParts
EnableParts enableParts = actionMethod.getDeclaredAnnotation(EnableParts.class);
actionDesc.setEnableParts(enableParts);
// @EnableEtag
EnableEtag enableEtag = actionMethod.getDeclaredAnnotation(EnableEtag.class);
actionDesc.setEnableEtag(enableEtag);
// @EnableCache
EnableCache enableCache = actionMethod.getDeclaredAnnotation(EnableCache.class);
actionDesc.setEnableCache(enableCache);
// @EnableCsrf
EnableCsrf enableCsrf = actionMethod.getDeclaredAnnotation(EnableCsrf.class);
actionDesc.setEnableCsrf(enableCsrf);
// @EnableCaptcha
EnableCaptcha enableCaptcha = actionMethod.getDeclaredAnnotation(EnableCaptcha.class);
actionDesc.setEnableCaptcha(enableCaptcha);
// @EnableLang
EnableLang enableLang = ValueUtils.valueOrAlt(actionMethod.getDeclaredAnnotation(EnableLang.class), controllerClass.getDeclaredAnnotation(EnableLang.class));
if (enableLang == null) {
if (this.appConfig.getBool(AppConfig.CONFIG_ENABLE_LANG, false)) {
enableLang = EnableLang.IMPL;
}
}
actionDesc.setEnableLang(((enableLang != null) && !enableLang.removed()) ? enableLang : null);
// @EnableAsync
EnableAsync enableAsync = ValueUtils.valueOrAlt(actionMethod.getDeclaredAnnotation(EnableAsync.class), controllerClass.getDeclaredAnnotation(EnableAsync.class));
if (enableAsync == null) {
if (this.appConfig.getBool(AppConfig.CONFIG_ENABLE_ASYNC, false)) {
enableAsync = EnableAsync.IMPL;
}
}
actionDesc.setEnableAsync(((enableAsync != null) && !enableAsync.removed()) ? enableAsync : null);
// @EnableJsonError
EnableJsonError enableJsonError = ValueUtils.valueOrAlt(actionMethod.getDeclaredAnnotation(EnableJsonError.class),
controllerClass.getDeclaredAnnotation(EnableJsonError.class));
if (enableJsonError == null) {
if (this.appConfig.getBool(AppConfig.CONFIG_ENABLE_JSON_ERROR, false)) {
enableJsonError = EnableJsonError.IMPL;
}
}
actionDesc.setEnableJsonError(((enableJsonError != null) && !enableJsonError.removed()) ? enableJsonError : null);
if (actionDesc.getEnableJsonError() == null) {
if (!ActionDescUtils.isActionResultOrVoid(actionMethod)) {
actionDesc.setEnableJsonError(EnableJsonError.IMPL);
}
}
}
// @Removed
if (ValueUtils.valueOrAlt(actionMethod.getDeclaredAnnotation(Removed.class), controllerClass.getDeclaredAnnotation(Removed.class)) != null) {
continue;
}
// ActionRoute
ActionRoute actionRoute = new ActionRoute(controller, action);
AssertUtils.assertTrue(!this.actionDescMap.containsKey(actionRoute),
"controller/action is duplicated (controller=" + controllerClass + ", action=" + actionMethod + ")");
// Index
if (action.equalsIgnoreCase(ServletUtils.ACTION_INDEX)) {
// @Home
if (controllerClass.getAnnotation(Home.class) != null) {
AssertUtils.assertNull(this.homeController, "@Home is duplicated (controller=" + controllerClass + ")");
this.homeController = controller;
}
}
// @FormLogin
if (actionMethod.getDeclaredAnnotation(FormLogin.class) != null) {
AssertUtils.assertFalse(this.formLogins.containsKey(actionDesc.getModule()), "@FormLogin is duplicated");
this.formLogins.put(actionDesc.getModule(), actionDesc);
}
this.actionDescMap.put(actionRoute, actionDesc);
}
}
public static List parseParamDescs(Method actionMethod, List pathParams) {
List paramDescs = new ArrayList<>(actionMethod.getParameterCount());
for (Parameter parameter : actionMethod.getParameters()) {
ParamDesc paramDesc = new ParamDesc();
paramDesc.setParameter(parameter);
paramDescs.add(paramDesc);
// HttpServletRequest | HttpServletResponse
if ((parameter.getType() == HttpServletRequest.class) || (parameter.getType() == HttpServletResponse.class)) {
continue;
}
// RequestAccessor | RequestContext
if ((parameter.getType() == RequestAccessor.class) || (parameter.getType() == RequestContext.class)) {
continue;
}
// ModelState
if (parameter.getType() == ModelState.class) {
continue;
}
// @Model
Model model = parameter.getDeclaredAnnotation(Model.class);
if (model != null) {
paramDesc.setModel(model);
AssertUtils.assertNull(parameter.getDeclaredAnnotation(Valid.class), "@Valid is invalid location.");
continue;
}
// @Param
Param param = parameter.getDeclaredAnnotation(Param.class);
if (param != null) {
paramDesc.setParamName(!param.value().isEmpty() ? param.value() : parameter.getName());
paramDesc.setFormatter(!param.fmt().isEmpty() ? param.fmt() : null);
} else {
paramDesc.setParamName(parameter.getName());
}
paramDesc.setPathParam(pathParams.stream().anyMatch(p -> p.hasPathParam(paramDesc.getParamName())));
}
return paramDescs;
}
public static List parseAllowMethods(Method method, Out httpMethod) {
Set allow = new LinkedHashSet<>();
if (parseAllowMethod(method, HttpGet.class, allow)) {
allow.add(HttpMethod.HEAD);
}
if (parseAllowMethod(method, HttpGetPost.class, allow)) {
allow.add(HttpMethod.HEAD);
}
parseAllowMethod(method, HttpPost.class, allow);
parseAllowMethod(method, HttpPut.class, allow);
parseAllowMethod(method, HttpDelete.class, allow);
parseAllowMethod(method, HttpPatch.class, allow);
httpMethod.value = (allow.size() > 0);
if (method.getDeclaredAnnotation(EnableCors.class) != null) {
allow.add(HttpMethod.OPTIONS);
}
return new ArrayList<>(allow);
}
private static boolean parseAllowMethod(Method method, Class extends Annotation> httpMethodAntClass, Set allow) {
if (method.getDeclaredAnnotation(httpMethodAntClass) == null) {
return false;
}
if (httpMethodAntClass == HttpGetPost.class) {
CollectionUtils.toSet(allow, HttpMethod.GET, HttpMethod.POST);
return true;
}
HttpMethod ant = httpMethodAntClass.getDeclaredAnnotation(HttpMethod.class);
allow.add(ant.value());
return true;
}
private static final String PARAM_FORMAT = "\\{[a-z\\d._]+}";
private static final Pattern PARAM_PATTERN = Pattern.compile(PARAM_FORMAT, Pattern.CASE_INSENSITIVE);
private static final Pattern PATH_PARAMS_PATTERN = Pattern.compile(String.format("(/%s(-%s)*)+", PARAM_FORMAT, PARAM_FORMAT), Pattern.CASE_INSENSITIVE);
// value: (/{parameter}(-{parameter})*)+
public static List parsePathParams(String value) {
AssertUtils.assertTrue(PATH_PARAMS_PATTERN.matcher(value).matches(), "pathParams is invalid (value=" + value + ")");
List pathParams = new ArrayList<>();
for (String pathItem : SplitUtils.split(value, '/')) {
// pathItem: {parameter}
if (PARAM_PATTERN.matcher(pathItem).matches()) {
pathParams.add(new PathParam(pathItem.substring(1, pathItem.length() - 1)));
} else {
// pathItem: {parameter}(-{parameter})+
List parsedSubParams = parseSubParams(pathItem);
pathParams.add(new PathParam(Collections.unmodifiableList(parsedSubParams)));
}
}
return pathParams;
}
// value: {parameter}(-{parameter})+
public static List parseSubParams(String value) {
List subParams = new ArrayList<>();
for (String subParam : SplitUtils.split(value, '-')) {
// subParam: {parameter}
subParams.add(new PathParam(subParam.substring(1, subParam.length() - 1)));
}
return subParams;
}
public static int getPathParamCount(List pathParams) {
Queue q = new LinkedList<>();
for (PathParam pathParam : pathParams) {
q.add(pathParam);
}
int count = 0;
while (!q.isEmpty()) {
PathParam pathParam = q.remove();
if (pathParam.getParamName() != null) {
count++;
} else {
for (PathParam subParam : pathParam.getSubParams()) {
q.add(subParam);
}
}
}
return count;
}
public static String getAction(Method actionMethod) {
Action action = actionMethod.getDeclaredAnnotation(Action.class);
if (action == null) {
return actionMethod.getName();
}
String route = StringUtils.trimToNull(action.value());
AssertUtils.assertNotNull(route, "Can't determime action route (action=" + actionMethod + ")");
return StringUtils.firstLowerCase(route, Locale.ENGLISH);
}
public static String getController(Class> clazz) {
Controller controller = clazz.getDeclaredAnnotation(Controller.class);
AssertUtils.assertNotNull(controller, "@Controller is required (controller=" + clazz + ")");
String route = StringUtils.trimToNull(controller.value());
if (route == null) {
AssertUtils.assertTrue(StringUtils.endsWithIgnoreCase(clazz.getSimpleName(), "Controller") && !clazz.getSimpleName().equalsIgnoreCase("Controller"),
"Can't determime controller route (controller=" + clazz + ")");
route = clazz.getSimpleName().substring(0, clazz.getSimpleName().length() - 10);
}
return StringUtils.firstLowerCase(route, Locale.ENGLISH);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy