
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 com.appslandia.common.base.Bind;
import com.appslandia.common.base.CaseInsensitiveMap;
import com.appslandia.common.base.InitializeObject;
import com.appslandia.common.base.Out;
import com.appslandia.common.utils.Asserts;
import com.appslandia.common.utils.CollectionUtils;
import com.appslandia.common.utils.STR;
import com.appslandia.common.utils.SplitUtils;
import com.appslandia.common.utils.SplittingBehavior;
import com.appslandia.common.utils.StringUtils;
import com.appslandia.common.utils.ValueUtils;
import com.appslandia.plum.utils.ServletUtils;
import jakarta.inject.Inject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
/**
*
* @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 this.formLogins.get(module);
}
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
Module module = controllerClass.getDeclaredAnnotation(Module.class);
String moduleId = ((module == null) || module.value().isEmpty())
? this.appConfig.getStringReq(AppConfig.CONFIG_DEFAULT_MODULE)
: module.value();
actionDesc.setModule(moduleId);
// 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));
actionDesc.setAuthorize(authorize);
// @CacheControl
CacheControl cacheControl = ValueUtils.valueOrAlt(actionMethod.getDeclaredAnnotation(CacheControl.class),
controllerClass.getDeclaredAnnotation(CacheControl.class));
if (cacheControl != null) {
if (cacheControl.nocache()) {
actionDesc.setCacheControl(CacheControl.NO_CACHE);
} else {
actionDesc.setCacheControl(cacheControl);
}
}
// @EnableCors
EnableCors enableCors = ValueUtils.valueOrAlt(actionMethod.getDeclaredAnnotation(EnableCors.class),
controllerClass.getDeclaredAnnotation(EnableCors.class));
actionDesc.setEnableCors(enableCors);
// @EnableEncoding
if (!this.appConfig.getBool(AppConfig.CONFIG_DISABLE_ENCODING)) {
EnableEncoding enableEncoding = ValueUtils.valueOrAlt(
actionMethod.getDeclaredAnnotation(EnableEncoding.class),
controllerClass.getDeclaredAnnotation(EnableEncoding.class));
actionDesc.setEnableEncoding(enableEncoding);
}
// @EnableParts
EnableParts enableParts = actionMethod.getDeclaredAnnotation(EnableParts.class);
actionDesc.setEnableParts(enableParts);
// @EnableAsync
EnableAsync enableAsync = actionMethod.getDeclaredAnnotation(EnableAsync.class);
actionDesc.setEnableAsync(enableAsync);
// @EnableEtag
EnableEtag enableEtag = actionMethod.getDeclaredAnnotation(EnableEtag.class);
actionDesc.setEnableEtag(enableEtag);
// @EnableCsrf
EnableCsrf enableCsrf = actionMethod.getDeclaredAnnotation(EnableCsrf.class);
actionDesc.setEnableCsrf(enableCsrf);
// @EnableCaptcha
EnableCaptcha enableCaptcha = actionMethod.getDeclaredAnnotation(EnableCaptcha.class);
actionDesc.setEnableCaptcha(enableCaptcha);
// @EnableJsonError
EnableJsonError enableJsonError = ValueUtils.valueOrAlt(
actionMethod.getDeclaredAnnotation(EnableJsonError.class),
controllerClass.getDeclaredAnnotation(EnableJsonError.class));
if ((enableJsonError == null) && !ActionDescUtils.isActionResultOrVoid(actionMethod)) {
enableJsonError = EnableJsonError.IMPL;
}
actionDesc.setEnableJsonError(enableJsonError);
// @BypassAuthorization
BypassAuthorization bypassAuthorization = actionMethod.getDeclaredAnnotation(BypassAuthorization.class);
actionDesc.setBypassAuthorization(bypassAuthorization);
}
// @Removed
if (ValueUtils.valueOrAlt(actionMethod.getDeclaredAnnotation(Removed.class),
controllerClass.getDeclaredAnnotation(Removed.class)) != null) {
continue;
}
// ActionRoute
ActionRoute actionRoute = new ActionRoute(controller, action);
Asserts.isTrue(!this.actionDescMap.containsKey(actionRoute),
() -> STR.fmt("controller/action is duplicated: controller={}, action={}.", controllerClass, actionMethod));
// Index
if (action.equalsIgnoreCase(ServletUtils.ACTION_INDEX)) {
// @Home
if (controllerClass.getAnnotation(Home.class) != null) {
Asserts.isNull(this.homeController, () -> STR.fmt("@Home is duplicated: controller={}.", controllerClass));
this.homeController = controller;
}
}
// @FormLogin
if (actionMethod.getDeclaredAnnotation(FormLogin.class) != null) {
Asserts.isTrue(!this.formLogins.containsKey(actionDesc.getModule()),
() -> STR.fmt("@FormLogin is duplicated: module=", actionDesc.getModule()));
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 paramDesc = new ParamDesc();
paramDesc.setParameter(parameter);
paramDescs.add(paramDesc);
// HttpServletRequest | HttpServletResponse
if ((parameter.getType() == HttpServletRequest.class) || (parameter.getType() == HttpServletResponse.class)) {
continue;
}
// RequestWrapper | RequestContext
if ((parameter.getType() == RequestWrapper.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);
Asserts.isNull(parameter.getDeclaredAnnotation(Valid.class),
() -> STR.fmt("@Valid is unsupported at this location: {}.", parameter));
continue;
}
// @Bind
Bind bind = parameter.getDeclaredAnnotation(Bind.class);
paramDesc.setBind(bind);
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) {
Asserts.notNull(value);
Asserts.isTrue(PATH_PARAMS_PATTERN.matcher(value).matches(), () -> STR.fmt("pathParams '{}' is invalid.", value));
List pathParams = new ArrayList<>();
String[] parts = SplitUtils.split(value, '/', SplittingBehavior.ORIGINAL);
for (String pathItem : parts) {
if (pathItem.isEmpty()) {
continue;
}
// 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<>();
String[] parts = SplitUtils.split(value, '-', SplittingBehavior.ORIGINAL);
for (String subParam : parts) {
// 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());
Asserts.notNull(route, () -> STR.fmt("Couldn't determime action route: action={}.", actionMethod));
return StringUtils.firstLowerCase(route, Locale.ENGLISH);
}
public static String getController(Class> clazz) {
Controller controller = clazz.getDeclaredAnnotation(Controller.class);
Asserts.notNull(controller, () -> STR.fmt("@Controller is required: controller={}.", clazz));
String route = StringUtils.trimToNull(controller.value());
if (route == null) {
Asserts.isTrue(StringUtils.endsWith(clazz.getSimpleName(), "Controller"),
() -> STR.fmt("Couldn't determime controller route: controller={}.", clazz));
route = clazz.getSimpleName().substring(0, clazz.getSimpleName().length() - 10);
Asserts.isTrue(!route.isEmpty(), () -> STR.fmt("Controller is invalid: controller={}.", clazz));
}
return StringUtils.firstLowerCase(route, Locale.ENGLISH);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy