All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 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