// 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.ActionConfig;
import jodd.madvoc.ActionConfigSet;
import jodd.madvoc.ActionDef;
import jodd.madvoc.MadvocException;
import jodd.madvoc.macro.PathMacros;
import jodd.petite.meta.PetiteInject;
import jodd.util.ClassLoaderUtil;
import jodd.util.StringUtil;
import jodd.util.collection.SortedArrayList;
import jodd.log.Logger;
import jodd.log.LoggerFactory;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Manages all Madvoc action and aliases registrations.
*/
public class ActionsManager {
private static final Logger log = LoggerFactory.getLogger(ActionsManager.class);
@PetiteInject
protected ActionMethodParser actionMethodParser;
@PetiteInject
protected ActionPathMacroManager actionPathMacroManager;
@PetiteInject
protected MadvocConfig madvocConfig;
protected int actionsCount;
protected boolean asyncMode;
protected final HashMap<String, ActionConfigSet> map; // map of all action paths w/o macros
protected final SortedArrayList<ActionConfigSet> list; // list of all action paths with macros
protected final HashMap<String, ActionConfig> configs; // another map of all action configs
protected Map<String, String> pathAliases; // path aliases
public ActionsManager() {
this.map = new HashMap<String, ActionConfigSet>();
this.list = new SortedArrayList<ActionConfigSet>(new ActionConfigSetComparator());
this.pathAliases = new HashMap<String, String>();
this.configs = new HashMap<String, ActionConfig>();
this.asyncMode = false;
}
/**
* Comparator that considers first chunks number then action path.
*/
public static class ActionConfigSetComparator implements Comparator<ActionConfigSet> {
public int compare(ActionConfigSet set1, ActionConfigSet set2) {
int deep1 = set1.deep;
int deep2 = set2.deep;
if (deep1 == deep2) {
return set1.actionPath.compareTo(set2.actionPath);
}
return deep1 - deep2;
}
}
/**
* Returns all registered action configurations.
* Returned list is a join of action paths
* with and without the macro.
*/
public List<ActionConfig> getAllActionConfigurations() {
List<ActionConfig> all = new ArrayList<ActionConfig>(actionsCount);
for (ActionConfigSet set : map.values()) {
all.addAll(set.getActionConfigs());
}
for (ActionConfigSet set : list) {
all.addAll(set.getActionConfigs());
}
return all;
}
/**
* Returns total number of registered actions.
*/
public int getActionsCount() {
return actionsCount;
}
/**
* Returns <code>true</code> if at least one action has
* async mode turned on.
*/
public boolean isAsyncModeOn() {
return asyncMode;
}
// ---------------------------------------------------------------- register variations
/**
* Resolves action method for given action class ane method name.
*/
public Method resolveActionMethod(Class<?> actionClass, String methodName) {
MethodDescriptor methodDescriptor = ClassIntrospector.lookup(actionClass).getMethodDescriptor(methodName, false);
if (methodDescriptor == null) {
throw new MadvocException("Public method not found: " + actionClass.getSimpleName() + "#" + methodName);
}
return methodDescriptor.getMethod();
}
/**
* Registers action with provided action signature.
*/
public ActionConfig register(String actionSignature) {
return register(actionSignature, null);
}
/**
* Registers action with provided action signature.
*/
public ActionConfig register(String actionSignature, ActionDef actionDef) {
int ndx = actionSignature.indexOf('#');
if (ndx == -1) {
throw new MadvocException("Madvoc action signature syntax error: " + actionSignature);
}
String actionClassName = actionSignature.substring(0, ndx);
String actionMethodName = actionSignature.substring(ndx + 1);
Class actionClass;
try {
actionClass = ClassLoaderUtil.loadClass(actionClassName);
} catch (ClassNotFoundException cnfex) {
throw new MadvocException("Madvoc action class not found: " + actionClassName, cnfex);
}
return register(actionClass, actionMethodName, actionDef);
}
public ActionConfig register(Class actionClass, Method actionMethod) {
return registerAction(actionClass, actionMethod, null);
}
public ActionConfig register(Class actionClass, Method actionMethod, ActionDef actionDef) {
return registerAction(actionClass, actionMethod, actionDef);
}
/**
* Registers action with provided action class and method name.
*/
public ActionConfig register(Class actionClass, String actionMethodName) {
Method actionMethod = resolveActionMethod(actionClass, actionMethodName);
return registerAction(actionClass, actionMethod, null);
}
public ActionConfig register(Class actionClass, String actionMethodName, ActionDef actionDef) {
Method actionMethod = resolveActionMethod(actionClass, actionMethodName);
return registerAction(actionClass, actionMethod, actionDef);
}
// ---------------------------------------------------------------- registration
/**
* Registration main point. Does two things:
* <ul>
* <li>{@link jodd.madvoc.component.ActionMethodParser#parse(Class, java.lang.reflect.Method, jodd.madvoc.ActionDef) parse action}
* and creates {@link jodd.madvoc.ActionConfig}</li>
* <li>{@link #registerAction(jodd.madvoc.ActionConfig) registers} created {@link jodd.madvoc.ActionConfig}</li>
* </ul>
* Returns created {@link ActionConfig}.
* @see #registerAction(jodd.madvoc.ActionConfig)
*/
protected ActionConfig registerAction(Class actionClass, Method actionMethod, ActionDef actionDef) {
ActionConfig actionConfig = actionMethodParser.parse(actionClass, actionMethod, actionDef);
if (actionConfig == null) {
return null;
}
return registerAction(actionConfig);
}
/**
* Registers manually created {@link ActionConfig action configurations}.
* Optionally, if action path with the same name already exist,
* exception will be thrown.
*/
public ActionConfig registerAction(ActionConfig actionConfig) {
String actionPath = actionConfig.actionPath;
if (log.isDebugEnabled()) {
log.debug("Registering Madvoc action: " + actionConfig.actionPath + " to: " +
actionConfig.getActionString());
}
ActionConfigSet set = createActionConfigSet(actionConfig.actionPath);
if (set.actionPathMacros != null) {
// new action patch contain macros
int ndx = -1;
for (int i = 0; i < list.size(); i++) {
if (list.get(i).actionPath.equals(actionPath)) {
ndx = i;
break;
}
}
if (ndx < 0) {
list.add(set);
} else {
set = list.get(ndx);
}
} else {
// action path is without macros
if (map.containsKey(actionConfig.actionPath) == false) {
map.put(actionConfig.actionPath, set);
} else {
set = map.get(actionConfig.actionPath);
}
}
boolean isDuplicate = set.add(actionConfig);
if (madvocConfig.isDetectDuplicatePathsEnabled()) {
if (isDuplicate) {
throw new MadvocException("Duplicate action path for " + actionConfig);
}
}
// finally
configs.put(actionConfig.getActionString(), actionConfig);
if (isDuplicate == false) {
actionsCount++;
}
// async check
if (actionConfig.isAsync()) {
asyncMode = true;
}
return actionConfig;
}
/**
* Creates new action config set from the action path.
*/
protected ActionConfigSet createActionConfigSet(String actionPath) {
PathMacros pathMacros = actionPathMacroManager.buildActionPathMacros(actionPath);
return new ActionConfigSet(actionPath, pathMacros);
}
// ---------------------------------------------------------------- look-up
/**
* Returns action configurations for provided action path.
* First it lookups for exact <code>actionPath</code>.
* If action path is not registered, it is split into chunks
* and match against macros.
* Returns <code>null</code> if action path is not registered.
* <code>method</code> must be in uppercase.
*/
public ActionConfig lookup(String actionPath, String method) {
// 1st try: the map
ActionConfigSet actionConfigSet = map.get(actionPath);
if (actionConfigSet != null) {
ActionConfig actionConfig = actionConfigSet.lookup(method);
if (actionConfig != null) {
return actionConfig;
}
}
// 2nd try: the list
int actionPathDeep = StringUtil.count(actionPath, '/');
int len = list.size();
int lastMatched = -1;
int maxMatchedChars = -1;
for (int i = 0; i < len; i++) {
actionConfigSet = list.get(i);
int deep = actionConfigSet.deep;
if (deep < actionPathDeep) {
continue;
}
if (deep > actionPathDeep) {
break;
}
// same deep level, try the fully match
int matchedChars = actionConfigSet.actionPathMacros.match(actionPath);
if (matchedChars == -1) {
continue;
}
if (matchedChars > maxMatchedChars) {
maxMatchedChars = matchedChars;
lastMatched = i;
}
}
if (lastMatched < 0) {
return null;
}
ActionConfigSet set = list.get(lastMatched);
return set.lookup(method);
}
/**
* Lookups action config for given action class and method string (aka 'action string').
* The action string has the following format: <code>className#methodName</code>.
* @see jodd.madvoc.ActionConfig#getActionString()
*/
public ActionConfig lookup(String actionString) {
return configs.get(actionString);
}
// ---------------------------------------------------------------- aliases
/**
* Registers new path alias.
*/
public void registerPathAlias(String alias, String path) {
pathAliases.put(alias, path);
}
/**
* Returns path alias.
*/
public String lookupPathAlias(String alias) {
return pathAliases.get(alias);
}
}