// Copyright (c) 2003-2014, Jodd Team (jodd.org). All Rights Reserved.
package jodd.madvoc.component;
import jodd.introspector.ClassIntrospector;
import jodd.introspector.MethodDescriptor;
import jodd.madvoc.MadvocException;
import jodd.madvoc.MadvocUtil;
import jodd.madvoc.RootPackages;
import jodd.madvoc.ScopeData;
import jodd.madvoc.ScopeType;
import jodd.madvoc.filter.ActionFilter;
import jodd.madvoc.interceptor.ActionInterceptor;
import jodd.madvoc.meta.ActionAnnotationData;
import jodd.madvoc.meta.ActionAnnotation;
import jodd.madvoc.meta.FilteredBy;
import jodd.madvoc.meta.InterceptedBy;
import jodd.madvoc.meta.MadvocAction;
import jodd.madvoc.meta.Action;
import jodd.madvoc.ActionConfig;
import jodd.util.ArraysUtil;
import jodd.util.ClassLoaderUtil;
import jodd.util.StringUtil;
import jodd.util.StringPool;
import jodd.petite.meta.PetiteInject;
import java.lang.reflect.Method;
/**
* Creates {@link ActionConfig action configurations} from action java method.
* Reads all annotations and builds action path (i.e. configuration).
* <p>
* Invoked only during registration, therefore performance is not most important.
*/
public class ActionMethodParser {
protected static final String REPL_PACKAGE = "[package]";
protected static final String REPL_CLASS = "[class]";
protected static final String REPL_METHOD = "[method]";
protected static final String REPL_EXTENSION = "[ext]";
@PetiteInject
protected ActionsManager actionsManager;
@PetiteInject
protected InterceptorsManager interceptorsManager;
@PetiteInject
protected FiltersManager filtersManager;
@PetiteInject
protected MadvocConfig madvocConfig;
@PetiteInject
protected ScopeDataResolver scopeDataResolver;
// ---------------------------------------------------------------- resolve method
/**
* Resolves action method for given string ane method name.
*/
public Method resolveActionMethod(Class<?> actionClass, String methodName) {
MethodDescriptor methodDescriptor = ClassIntrospector.lookup(actionClass).getMethodDescriptor(methodName, false);
if (methodDescriptor == null) {
throw new MadvocException("Action class '" + actionClass.getSimpleName() + "' doesn't have public method: " + methodName);
}
return methodDescriptor.getMethod();
}
// ---------------------------------------------------------------- parse
public ActionConfig parse(Class<?> actionClass, Method actionMethod) {
return parse(actionClass, actionMethod, null);
}
/**
* @see #parse(Class, java.lang.reflect.Method, String)
*/
public ActionConfig parse(Class<?> actionClass, String actionMethodName, String actionPath) {
Method method = resolveActionMethod(actionClass, actionMethodName);
return parse(actionClass, method, actionPath);
}
/**
* Parses java action method annotations and returns its action configuration.
* Returns <code>null</code> if method is not a madvoc action.
*/
public ActionConfig parse(final Class<?> actionClass, final Method actionMethod, String actionPath) {
// interceptors
Class<? extends ActionInterceptor>[] interceptorClasses = readMethodInterceptors(actionMethod);
if (interceptorClasses == null) {
interceptorClasses = readClassInterceptors(actionClass);
}
if (interceptorClasses == null) {
interceptorClasses = madvocConfig.getDefaultInterceptors();
}
ActionInterceptor[] actionInterceptors = interceptorsManager.resolveAll(interceptorClasses);
// filters
Class<? extends ActionFilter>[] filterClasses = readMethodFilters(actionMethod);
if (filterClasses == null) {
filterClasses = readClassFilters(actionClass);
}
if (filterClasses == null) {
filterClasses = madvocConfig.getDefaultFilters();
}
ActionFilter[] actionFilters = filtersManager.resolveAll(filterClasses);
// actions
//HashMap<String, String> replacementMap = new HashMap<String, String>();
String[] actionPathElements = new String[4];
// action path not specified, build it
String packageActionPath = readPackageActionPath(actionClass, actionPathElements);
// class annotation: class action path
String classActionPath = readClassActionPath(actionClass, actionPathElements);
if (classActionPath == null) {
return null;
}
// method annotation: detect
ActionAnnotationData annotationData = null;
for (ActionAnnotation actionAnnotation : madvocConfig.getActionAnnotationInstances()) {
annotationData = actionAnnotation.readAnnotationData(actionMethod);
if (annotationData != null) {
break;
}
}
// read method annotation values
String actionMethodName = actionMethod.getName();
String methodActionPath = readMethodActionPath(actionMethodName, annotationData, actionPathElements);
String extension = readMethodExtension(annotationData);
String alias = readMethodAlias(annotationData);
String httpMethod = readMethodHttpMethod(annotationData);
boolean async = readMethodAsyncFlag(annotationData);
if (methodActionPath != null) {
// additional changes
actionPathElements[3] = extension;
// check for defaults
for (String path : madvocConfig.getDefaultActionMethodNames()) {
if (methodActionPath.equals(path)) {
methodActionPath = null;
break;
}
}
}
if (actionPath == null) {
// finally, build the action path if it is not already specified
actionPath = buildActionPath(packageActionPath, classActionPath, methodActionPath, extension, httpMethod);
}
// apply replacements
{
actionPath = StringUtil.replace(actionPath, REPL_PACKAGE, actionPathElements[0]);
actionPath = StringUtil.replace(actionPath, REPL_CLASS, actionPathElements[1]);
actionPath = StringUtil.replace(actionPath, REPL_METHOD, actionPathElements[2]);
actionPath = StringUtil.replace(actionPath, REPL_EXTENSION, actionPathElements[3]);
}
// register alias
if (alias != null) {
String aliasPath = StringUtil.cutToIndexOf(actionPath, StringPool.HASH);
actionsManager.registerPathAlias(alias, aliasPath);
}
return createActionConfig(
actionClass, actionMethod,
actionFilters, actionInterceptors,
actionPath, httpMethod,
async, actionPathElements);
}
/**
* Builds action path. Method action path and extension may be <code>null</code>.
* @param packageActionPath action path from package (optional)
* @param classActionPath action path from class
* @param methodActionPath action path from method (optional)
* @param extension extension (optional)
* @param httpMethod HTTP method name (not used by default)
*/
protected String buildActionPath(String packageActionPath, String classActionPath, String methodActionPath, String extension, String httpMethod) {
String pathSeparator = StringPool.SLASH;
String actionPath = classActionPath;
if (methodActionPath != null) {
if (methodActionPath.startsWith(pathSeparator)) {
return methodActionPath; // absolute path
}
if (extension != null) { // add extension
methodActionPath += '.' + extension;
}
if (classActionPath.endsWith(pathSeparator) == false) {
actionPath += StringPool.DOT;
}
actionPath += methodActionPath; // method separator
} else {
if (extension != null) {
actionPath += '.' + extension;
}
}
if (actionPath.startsWith(pathSeparator)) {
return actionPath;
}
if (packageActionPath != null) {
actionPath = packageActionPath + actionPath;
} else {
actionPath = pathSeparator + actionPath;
}
return actionPath;
}
// ---------------------------------------------------------------- interceptors
/**
* Reads class interceptors when method interceptors are not available.
*/
protected Class<? extends ActionInterceptor>[] readClassInterceptors(Class actionClass) {
Class<? extends ActionInterceptor>[] result = null;
InterceptedBy interceptedBy = ((Class<?>)actionClass).getAnnotation(InterceptedBy.class);
if (interceptedBy != null) {
result = interceptedBy.value();
if (result.length == 0) {
result = null;
}
}
return result;
}
/**
* Reads method interceptors.
*/
protected Class<? extends ActionInterceptor>[] readMethodInterceptors(Method actionMethod) {
Class<? extends ActionInterceptor>[] result = null;
InterceptedBy interceptedBy = actionMethod.getAnnotation(InterceptedBy.class);
if (interceptedBy != null) {
result = interceptedBy.value();
if (result.length == 0) {
result = null;
}
}
return result;
}
// ---------------------------------------------------------------- filters
/**
* Reads class filters when method filters are not available.
*/
protected Class<? extends ActionFilter>[] readClassFilters(Class actionClass) {
Class<? extends ActionFilter>[] result = null;
FilteredBy filteredBy = ((Class<?>)actionClass).getAnnotation(FilteredBy.class);
if (filteredBy != null) {
result = filteredBy.value();
if (result.length == 0) {
result = null;
}
}
return result;
}
/**
* Reads method filters.
*/
protected Class<? extends ActionFilter>[] readMethodFilters(Method actionMethod) {
Class<? extends ActionFilter>[] result = null;
FilteredBy filteredBy = actionMethod.getAnnotation(FilteredBy.class);
if (filteredBy != null) {
result = filteredBy.value();
if (result.length == 0) {
result = null;
}
}
return result;
}
// ---------------------------------------------------------------- readers
/**
* Reads action path for package. It can be used only if root package is set in
* {@link MadvocConfig madvoc configuration}.
* If annotation is not set on package-level, class package will be used for
* package action path part.
*/
protected String readPackageActionPath(Class actionClass, String[] actionPathElements) {
Package actionPackage = actionClass.getPackage();
String actionPackageName = actionPackage.getName();
final RootPackages rootPackages = madvocConfig.getRootPackages();
String packagePath = rootPackages.getPackageActionPath(actionPackageName);
if (packagePath == null) {
packagePath = rootPackages.findPackagePathForActionPackage(actionPackageName);
String rootPackage = null;
if (packagePath != null) {
rootPackage = rootPackages.findRootPackageForActionPath(packagePath);
}
// try locating marker class
{
String packageName = actionPackageName;
String madvocRootPackageClassName = madvocConfig.getMadvocRootPackageClassName();
if (madvocRootPackageClassName != null) {
while (true) {
String className = packageName + '.' + madvocRootPackageClassName;
try {
Class<?> madvocRootPackageClass = ClassLoaderUtil.loadClass(className, actionClass.getClassLoader());
// class found, find the mapping
String mapping = StringPool.EMPTY;
MadvocAction madvocAction = madvocRootPackageClass.getAnnotation(MadvocAction.class);
if (madvocAction != null) {
mapping = madvocAction.value();
}
// register root package - so not to lookup twice
madvocConfig.getRootPackages().addRootPackage(packageName, mapping);
// repeat lookup
packagePath = rootPackages.findPackagePathForActionPackage(actionPackageName);
break;
} catch (ClassNotFoundException ignore) {
// continue
int dotNdx = packageName.lastIndexOf('.');
if (dotNdx == -1) {
break;
}
packageName = packageName.substring(0, dotNdx);
if (rootPackage != null) {
// don't go beyond found root package
if (packageName.equals(rootPackage)) {
break;
}
}
}
}
}
}
rootPackages.registerPackageActionPath(actionPackageName, packagePath);
}
// read package-level annotation
MadvocAction madvocActionAnnotation = actionPackage.getAnnotation(MadvocAction.class);
String packageActionPath = madvocActionAnnotation != null ? madvocActionAnnotation.value().trim() : null;
if (StringUtil.isEmpty(packageActionPath)) {
packageActionPath = null;
}
// package-level annotation overrides everything
// if not set, resolve value
if (packageActionPath == null) {
// no package-level annotation
if (packagePath == null) {
// no root package path, just return
return null;
}
packageActionPath = packagePath;
}
actionPathElements[0] = StringUtil.stripChar(packagePath, '/');
return StringUtil.surround(packageActionPath, StringPool.SLASH);
}
/**
* Reads action path from class. If the class is annotated with {@link MadvocAction} annotation,
* class action path will be read from annotation value. Otherwise, action class path will be built from the
* class name. This is done by removing the package name and the last contained word
* (if there is more then one) from the class name. Such name is finally uncapitalized.
* <p>
* If this method returns <code>null</code> class will be ignored.
*/
protected String readClassActionPath(Class actionClass, String[] actionPathElements) {
// read annotation
MadvocAction madvocActionAnnotation = ((Class<?>)actionClass).getAnnotation(MadvocAction.class);
String classActionPath = madvocActionAnnotation != null ? madvocActionAnnotation.value().trim() : null;
if (StringUtil.isEmpty(classActionPath)) {
classActionPath = null;
}
String name = actionClass.getSimpleName();
name = StringUtil.uncapitalize(name);
name = MadvocUtil.stripLastCamelWord(name);
if (classActionPath == null) {
classActionPath = name;
}
actionPathElements[1] = name;
return classActionPath;
}
/**
* Reads action method.
*/
protected String readMethodActionPath(String methodName, ActionAnnotationData annotationData, String[] actionPathElements) {
// read annotation
String methodActionPath = annotationData != null ? annotationData.getValue() : null;
if (methodActionPath == null) {
methodActionPath = methodName;
} else {
if (methodActionPath.equals(Action.NONE)) {
return null;
}
}
actionPathElements[2] = methodName;
return methodActionPath;
}
/**
* Reads method's extension.
*/
protected String readMethodExtension(ActionAnnotationData annotationData) {
String extension = madvocConfig.getDefaultExtension();
if (annotationData != null) {
String annExtension = annotationData.getExtension();
if (annExtension != null) {
if (annExtension.equals(Action.NONE)) {
extension = null;
} else {
extension = StringUtil.replace(annExtension, REPL_EXTENSION, extension);
}
}
}
return extension;
}
/**
* Reads method's alias value.
*/
protected String readMethodAlias(ActionAnnotationData annotationData) {
String alias = null;
if (annotationData != null) {
alias = annotationData.getAlias();
}
return alias;
}
/**
* Reads method's http method.
*/
private String readMethodHttpMethod(ActionAnnotationData annotationData) {
String method = null;
if (annotationData != null) {
method = annotationData.getMethod();
}
return method;
}
/**
* Reads method's async flag.
*/
private boolean readMethodAsyncFlag(ActionAnnotationData annotationData) {
boolean sync = false;
if (annotationData != null) {
sync = annotationData.isAsync();
}
return sync;
}
// ---------------------------------------------------------------- create action configuration
/**
* Creates new instance of action configuration.
* Initialize caches.
*/
public ActionConfig createActionConfig(
Class actionClass,
Method actionClassMethod,
ActionFilter[] filters,
ActionInterceptor[] interceptors,
String actionPath,
String actionMethod,
boolean async,
String[] pathElements)
{
// uppercase
if (actionMethod != null) {
actionMethod = actionMethod.toUpperCase();
}
// find ins and outs
Class[] paramTypes = actionClassMethod.getParameterTypes();
Class[] types = ArraysUtil.insert(paramTypes, actionClass, 0);
ScopeData.In[][][] ins;
ScopeData.Out[][][] outs;
ins = new ScopeData.In[ScopeType.values().length][][];
outs = new ScopeData.Out[ScopeType.values().length][][];
for (int i = 0; i < ScopeType.values().length; i++) {
ins[i] = new ScopeData.In[types.length][];
outs[i] = new ScopeData.Out[types.length][];
for (int j = 0; j < types.length; j++) {
Class type = types[j];
ScopeData[] scopeData = scopeDataResolver.resolveScopeData(type);
if (scopeData == null) {
continue;
}
if (scopeData[i] != null) {
ins[i][j] = scopeData[i].in;
outs[i][j] = scopeData[i].out;
}
}
}
return new ActionConfig(
actionClass,
actionClassMethod,
filters,
interceptors,
actionPath,
actionMethod,
async,
ins,
outs,
pathElements);
}
}