blob: 0840e2175be47af8f143da926809141859836a4f [file] [log] [blame]
/*
* $Id$
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.config;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.config.Configuration;
import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.config.PackageProvider;
import com.opensymphony.xwork2.config.entities.ActionConfig;
import com.opensymphony.xwork2.config.entities.PackageConfig;
import com.opensymphony.xwork2.config.entities.ResultConfig;
import com.opensymphony.xwork2.config.entities.ResultTypeConfig;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.ClassLoaderUtil;
import com.opensymphony.xwork2.util.ResolverUtil;
import com.opensymphony.xwork2.util.ResolverUtil.ClassTest;
import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.ServletContext;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* ClasspathPackageProvider loads the configuration
* by scanning the classpath or selected packages for Action classes.
* <p>
* This provider is only invoked if one or more action packages are passed to the dispatcher,
* usually from the web.xml.
* Configurations are created for objects that either implement Action or have classnames that end with "Action".
*/
public class ClasspathPackageProvider implements PackageProvider {
/**
* The default page prefix (or "path").
* Some applications may place pages under "/WEB-INF" as an extreme security precaution.
*/
protected static final String DEFAULT_PAGE_PREFIX = "struts.configuration.classpath.defaultPagePrefix";
/**
* The default page prefix (none).
*/
private String defaultPagePrefix = "";
/**
* The default page extension, to use in place of ".jsp".
*/
protected static final String DEFAULT_PAGE_EXTENSION = "struts.configuration.classpath.defaultPageExtension";
/**
* The defacto default page extension, usually associated with JavaServer Pages.
*/
private String defaultPageExtension = ".jsp";
/**
* A setting to indicate a custom default parent package,
* to use in place of "struts-default".
*/
protected static final String DEFAULT_PARENT_PACKAGE = "struts.configuration.classpath.defaultParentPackage";
/**
* A setting to disable action scanning.
*/
protected static final String DISABLE_ACTION_SCANNING = "struts.configuration.classpath.disableActionScanning";
/**
* Name of the framework's default configuration package,
* that application configuration packages automatically inherit.
*/
private String defaultParentPackage = "struts-default";
/**
* The default page prefix (or "path").
* Some applications may place pages under "/WEB-INF" as an extreme security precaution.
*/
protected static final String FORCE_LOWER_CASE = "struts.configuration.classpath.forceLowerCase";
/**
* Whether to use a lowercase letter as the initial letter of an action.
* If false, actions will retain the initial uppercase letter from the Action class.
* (<code>view.action</code> (true) versus <code>View.action</code> (false)).
*/
private boolean forceLowerCase = true;
protected static final String CLASS_SUFFIX = "struts.codebehind.classSuffix";
/**
* Default suffix that can be used to indicate POJO "Action" classes.
*/
protected String classSuffix = "Action";
protected static final String CHECK_IMPLEMENTS_ACTION = "struts.codebehind.checkImplementsAction";
/**
* When testing a class, check that it implements Action
*/
protected boolean checkImplementsAction = true;
protected static final String CHECK_ANNOTATION = "struts.codebehind.checkAnnotation";
/**
* When testing a class, check that it has an @Action annotation
*/
protected boolean checkAnnotation = true;
/**
* Helper class to scan class path for server pages.
*/
private PageLocator pageLocator = new ClasspathPageLocator();
/**
* Flag to indicate the packages have been loaded.
*
* @see #loadPackages
* @see #needsReload
*/
private boolean initialized = false;
private boolean disableActionScanning = false;
private PackageLoader packageLoader;
/**
* Logging instance for this class.
*/
private static final Logger LOG = LoggerFactory.getLogger(ClasspathPackageProvider.class);
/**
* The XWork Configuration for this application.
*
* @see #init
*/
private Configuration configuration;
private String actionPackages;
private ServletContext servletContext;
public ClasspathPackageProvider() {
}
/**
* PageLocator defines a locate method that can be used to discover server pages.
*/
public static interface PageLocator {
public URL locate(String path);
}
/**
* ClasspathPathLocator searches the classpath for server pages.
*/
public static class ClasspathPageLocator implements PageLocator {
public URL locate(String path) {
return ClassLoaderUtil.getResource(path, getClass());
}
}
@Inject("actionPackages")
public void setActionPackages(String packages) {
this.actionPackages = packages;
}
public void setServletContext(ServletContext ctx) {
this.servletContext = ctx;
}
/**
* Disables action scanning.
*
* @param disableActionScanning True to disable
*/
@Inject(value=DISABLE_ACTION_SCANNING, required=false)
public void setDisableActionScanning(String disableActionScanning) {
this.disableActionScanning = "true".equals(disableActionScanning);
}
/**
* Check that the class implements Action
*
* @param checkImplementsAction True to check
*/
@Inject(value=CHECK_IMPLEMENTS_ACTION, required=false)
public void setCheckImplementsAction(String checkImplementsAction) {
this.checkImplementsAction = "true".equals(checkImplementsAction);
}
/**
* Check that the class has an @Action annotation
*
* @param checkImplementsAction True to check
*/
@Inject(value=CHECK_ANNOTATION, required=false)
public void setCheckAnnotation(String checkAnnotation) {
this.checkAnnotation = "true".equals(checkAnnotation);
}
/**
* Register a default parent package for the actions.
*
* @param defaultParentPackage the new defaultParentPackage
*/
@Inject(value=DEFAULT_PARENT_PACKAGE, required=false)
public void setDefaultParentPackage(String defaultParentPackage) {
this.defaultParentPackage = defaultParentPackage;
}
/**
* Register a default page extension to use when locating pages.
*
* @param defaultPageExtension the new defaultPageExtension
*/
@Inject(value=DEFAULT_PAGE_EXTENSION, required=false)
public void setDefaultPageExtension(String defaultPageExtension) {
this.defaultPageExtension = defaultPageExtension;
}
/**
* Reigster a default page prefix to use when locating pages.
*
* @param defaultPagePrefix the defaultPagePrefix to set
*/
@Inject(value=DEFAULT_PAGE_PREFIX, required=false)
public void setDefaultPagePrefix(String defaultPagePrefix) {
this.defaultPagePrefix = defaultPagePrefix;
}
/**
* Default suffix that can be used to indicate POJO "Action" classes.
*
* @param classSuffix the classSuffix to set
*/
@Inject(value=CLASS_SUFFIX, required=false)
public void setClassSuffix(String classSuffix) {
this.classSuffix = classSuffix;
}
/**
* Whether to use a lowercase letter as the initial letter of an action.
*
* @param force If false, actions will retain the initial uppercase letter from the Action class.
* (<code>view.action</code> (true) versus <code>View.action</code> (false)).
*/
@Inject(value=FORCE_LOWER_CASE, required=false)
public void setForceLowerCase(String force) {
this.forceLowerCase = "true".equals(force);
}
/**
* Register a PageLocation to use to scan for server pages.
*
* @param locator
*/
public void setPageLocator(PageLocator locator) {
this.pageLocator = locator;
}
/**
* Scan a list of packages for Action classes.
*
* This method loads classes that implement the Action interface
* or have a class name that ends with the letters "Action".
*
* @param pkgs A list of packages to load
* @see #processActionClass
*/
protected void loadPackages(String[] pkgs) {
packageLoader = new PackageLoader();
ResolverUtil<Class> resolver = new ResolverUtil<Class>();
resolver.find(createActionClassTest(), pkgs);
Set<? extends Class<? extends Class>> actionClasses = resolver.getClasses();
for (Object obj : actionClasses) {
Class cls = (Class) obj;
if (!Modifier.isAbstract(cls.getModifiers())) {
processActionClass(cls, pkgs);
}
}
for (PackageConfig config : packageLoader.createPackageConfigs()) {
configuration.addPackageConfig(config.getName(), config);
}
}
protected ClassTest createActionClassTest() {
return new ClassTest() {
// Match Action implementations and classes ending with "Action"
public boolean matches(Class type) {
// TODO: should also find annotated classes
return ((checkImplementsAction && Action.class.isAssignableFrom(type)) ||
type.getSimpleName().endsWith(getClassSuffix()) ||
(checkAnnotation && type.getAnnotation(org.apache.struts2.config.Action.class) != null));
}
};
}
protected String getClassSuffix() {
return classSuffix;
}
/**
* Create a default action mapping for a class instance.
*
* The namespace annotation is honored, if found, otherwise
* the Java package is converted into the namespace
* by changing the dots (".") to slashes ("/").
*
* @param cls Action or POJO instance to process
* @param pkgs List of packages that were scanned for Actions
*/
protected void processActionClass(Class<?> cls, String[] pkgs) {
String name = cls.getName();
String actionPackage = cls.getPackage().getName();
String actionNamespace = null;
String actionName = null;
org.apache.struts2.config.Action actionAnn =
(org.apache.struts2.config.Action) cls.getAnnotation(org.apache.struts2.config.Action.class);
if (actionAnn != null) {
actionName = actionAnn.name();
if (actionAnn.namespace().equals(org.apache.struts2.config.Action.DEFAULT_NAMESPACE)) {
actionNamespace = "";
} else {
actionNamespace = actionAnn.namespace();
}
} else {
for (String pkg : pkgs) {
if (name.startsWith(pkg)) {
if (LOG.isDebugEnabled()) {
LOG.debug("ClasspathPackageProvider: Processing class "+name);
}
name = name.substring(pkg.length() + 1);
actionNamespace = "";
actionName = name;
int pos = name.lastIndexOf('.');
if (pos > -1) {
actionNamespace = "/" + name.substring(0, pos).replace('.','/');
actionName = name.substring(pos+1);
}
break;
}
}
// Truncate Action suffix if found
if (actionName.endsWith(getClassSuffix())) {
actionName = actionName.substring(0, actionName.length() - getClassSuffix().length());
}
// Force initial letter of action to lowercase, if desired
if ((forceLowerCase) && (actionName.length() > 1)) {
int lowerPos = actionName.lastIndexOf('/') + 1;
StringBuilder sb = new StringBuilder();
sb.append(actionName.substring(0, lowerPos));
sb.append(Character.toLowerCase(actionName.charAt(lowerPos)));
sb.append(actionName.substring(lowerPos + 1));
actionName = sb.toString();
}
}
PackageConfig.Builder pkgConfig = loadPackageConfig(actionNamespace, actionPackage, cls);
// In case the package changed due to namespace annotation processing
if (!actionPackage.equals(pkgConfig.getName())) {
actionPackage = pkgConfig.getName();
}
List<PackageConfig> parents = findAllParentPackages(cls);
if (parents.size() > 0) {
pkgConfig.addParents(parents);
// Try to guess the namespace from the first package
PackageConfig firstParent = parents.get(0);
if (StringUtils.isEmpty(pkgConfig.getNamespace()) && StringUtils.isNotEmpty(firstParent.getNamespace())) {
pkgConfig.namespace(firstParent.getNamespace());
}
}
ResultTypeConfig defaultResultType = packageLoader.getDefaultResultType(pkgConfig);
ActionConfig actionConfig = new ActionConfig.Builder(actionPackage, actionName, cls.getName())
.addResultConfigs(new ResultMap<String,ResultConfig>(cls, actionName, defaultResultType))
.build();
pkgConfig.addActionConfig(actionName, actionConfig);
}
/**
* Finds all parent packages by first looking at the ParentPackage annotation on the package, then the class
* @param cls The action class
* @return A list of unique packages to add
*/
private List<PackageConfig> findAllParentPackages(Class<?> cls) {
List<PackageConfig> parents = new ArrayList<PackageConfig>();
// Favor parent package annotations from the package
Set<String> parentNames = new LinkedHashSet<String>();
ParentPackage annotation = cls.getPackage().getAnnotation(ParentPackage.class);
if (annotation != null) {
parentNames.addAll(Arrays.asList(annotation.value()));
}
annotation = cls.getAnnotation(ParentPackage.class);
if (annotation != null) {
parentNames.addAll(Arrays.asList(annotation.value()));
}
if (parentNames.size() > 0) {
for (String parent : parentNames) {
PackageConfig parentPkg = configuration.getPackageConfig(parent);
if (parentPkg == null) {
throw new ConfigurationException("ClasspathPackageProvider: Unable to locate parent package "+parent, annotation);
}
parents.add(parentPkg);
}
}
return parents;
}
/**
* Finds or creates the package configuration for an Action class.
*
* The namespace annotation is honored, if found,
* and the namespace is checked for a parent configuration.
*
* @param actionNamespace The configuration namespace
* @param actionPackage The Java package containing our Action classes
* @param actionClass The Action class instance
* @return PackageConfig object for the Action class
*/
protected PackageConfig.Builder loadPackageConfig(String actionNamespace, String actionPackage, Class actionClass) {
PackageConfig.Builder parent = null;
// Check for the @Namespace annotation
if (actionClass != null) {
Namespace ns = (Namespace) actionClass.getAnnotation(Namespace.class);
if (ns != null) {
parent = loadPackageConfig(actionNamespace, actionPackage, null);
actionNamespace = ns.value();
actionPackage = actionClass.getName();
// See if the namespace has been overridden by the @Action annotation
} else {
org.apache.struts2.config.Action actionAnn =
(org.apache.struts2.config.Action) actionClass.getAnnotation(org.apache.struts2.config.Action.class);
if (actionAnn != null && !actionAnn.DEFAULT_NAMESPACE.equals(actionAnn.namespace())) {
// we pass null as the namespace in case the parent package hasn't been loaded yet
parent = loadPackageConfig(null, actionPackage, null);
actionPackage = actionClass.getName();
}
}
}
PackageConfig.Builder pkgConfig = packageLoader.getPackage(actionPackage);
if (pkgConfig == null) {
pkgConfig = new PackageConfig.Builder(actionPackage);
pkgConfig.namespace(actionNamespace);
if (parent == null) {
PackageConfig cfg = configuration.getPackageConfig(defaultParentPackage);
if (cfg != null) {
pkgConfig.addParent(cfg);
} else {
throw new ConfigurationException("ClasspathPackageProvider: Unable to locate default parent package: " +
defaultParentPackage);
}
}
packageLoader.registerPackage(pkgConfig);
// if the parent package was first created by a child, ensure the namespace is correct
} else if (pkgConfig.getNamespace() == null) {
pkgConfig.namespace(actionNamespace);
}
if (parent != null) {
packageLoader.registerChildToParent(pkgConfig, parent);
}
if (LOG.isDebugEnabled()) {
LOG.debug("class:"+actionClass+" parent:"+parent+" current:"+(pkgConfig != null ? pkgConfig.getName() : ""));
}
return pkgConfig;
}
/**
* Default destructor. Override to provide behavior.
*/
public void destroy() {
}
/**
* Register this application's configuration.
*
* @param config The configuration for this application.
*/
public void init(Configuration config) {
this.configuration = config;
}
/**
* Clears and loads the list of packages registered at construction.
*
* @throws ConfigurationException
*/
public void loadPackages() throws ConfigurationException {
if (actionPackages != null && !disableActionScanning) {
String[] names = actionPackages.split("\\s*[,]\\s*");
// Initialize the classloader scanner with the configured packages
if (names.length > 0) {
setPageLocator(new ServletContextPageLocator(servletContext));
}
loadPackages(names);
}
initialized = true;
}
/**
* Indicates whether the packages have been initialized.
*
* @return True if the packages have been initialized
*/
public boolean needsReload() {
return !initialized;
}
/**
* Creates ResultConfig objects from result annotations,
* and if a result isn't found, creates it on the fly.
*/
class ResultMap<K,V> extends HashMap<K,V> {
private Class actionClass;
private String actionName;
private ResultTypeConfig defaultResultType;
public ResultMap(Class actionClass, String actionName, ResultTypeConfig defaultResultType) {
this.actionClass = actionClass;
this.actionName = actionName;
this.defaultResultType = defaultResultType;
// check if any annotations are around
while (!actionClass.getName().equals(Object.class.getName())) {
//noinspection unchecked
Results results = (Results) actionClass.getAnnotation(Results.class);
if (results != null) {
// first check here...
for (int i = 0; i < results.value().length; i++) {
Result result = results.value()[i];
ResultConfig config = createResultConfig(result);
if (!containsKey((K)config.getName())) {
put((K)config.getName(), (V)config);
}
}
}
// what about a single Result annotation?
Result result = (Result) actionClass.getAnnotation(Result.class);
if (result != null) {
ResultConfig config = createResultConfig(result);
if (!containsKey((K)config.getName())) {
put((K)config.getName(), (V)config);
}
}
actionClass = actionClass.getSuperclass();
}
}
/**
* Extracts result name and value and calls {@link #createResultConfig}.
*
* @param result Result annotation reference representing result type to create
* @return New or cached ResultConfig object for result
*/
protected ResultConfig createResultConfig(Result result) {
Class<? extends Object> cls = result.type();
if (cls == NullResult.class) {
cls = null;
}
return createResultConfig(result.name(), cls, result.value(), createParameterMap(result.params()));
}
protected Map<String, String> createParameterMap(String[] parms) {
Map<String, String> map = new HashMap<String, String>();
int subtract = parms.length % 2;
if(subtract != 0) {
LOG.warn("Odd number of result parameters key/values specified. The final one will be ignored.");
}
for (int i = 0; i < parms.length - subtract; i++) {
String key = parms[i++];
String value = parms[i];
map.put(key, value);
if(LOG.isDebugEnabled()) {
LOG.debug("Adding parmeter["+key+":"+value+"] to result.");
}
}
return map;
}
/**
* Creates a default ResultConfig,
* using either the resultClass or the default ResultType for configuration package
* associated this ResultMap class.
*
* @param key The result type name
* @param resultClass The class for the result type
* @param location Path to the resource represented by this type
* @return A ResultConfig for key mapped to location
*/
private ResultConfig createResultConfig(Object key, Class<? extends Object> resultClass,
String location,
Map<? extends Object,? extends Object > configParams) {
if (resultClass == null) {
configParams = defaultResultType.getParams();
String className = defaultResultType.getClassName();
try {
resultClass = ClassLoaderUtil.loadClass(className, getClass());
} catch (ClassNotFoundException ex) {
throw new ConfigurationException("ClasspathPackageProvider: Unable to locate result class "+className, actionClass);
}
}
String defaultParam;
try {
defaultParam = (String) resultClass.getField("DEFAULT_PARAM").get(null);
} catch (Exception e) {
// not sure why this happened, but let's just use a sensible choice
defaultParam = "location";
}
HashMap params = new HashMap();
if (configParams != null) {
params.putAll(configParams);
}
params.put(defaultParam, location);
return new ResultConfig.Builder((String) key, resultClass.getName()).addParams(params).build();
}
}
/**
* Search classpath for a page.
*/
private final class ServletContextPageLocator implements PageLocator {
private final ServletContext context;
private ClasspathPageLocator classpathPageLocator = new ClasspathPageLocator();
private ServletContextPageLocator(ServletContext context) {
this.context = context;
}
public URL locate(String path) {
URL url = null;
try {
url = context.getResource(path);
if (url == null) {
url = classpathPageLocator.locate(path);
}
} catch (MalformedURLException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Unable to resolve path "+path+" against the servlet context");
}
}
return url;
}
}
private static class PackageLoader {
/**
* The package configurations for scanned Actions.
*/
private Map<String,PackageConfig.Builder> packageConfigBuilders = new HashMap<String,PackageConfig.Builder>();
private Map<PackageConfig.Builder,PackageConfig.Builder> childToParent = new HashMap<PackageConfig.Builder,PackageConfig.Builder>();
public PackageConfig.Builder getPackage(String name) {
return packageConfigBuilders.get(name);
}
public void registerChildToParent(PackageConfig.Builder child, PackageConfig.Builder parent) {
childToParent.put(child, parent);
}
public void registerPackage(PackageConfig.Builder builder) {
packageConfigBuilders.put(builder.getName(), builder);
}
public Collection<PackageConfig> createPackageConfigs() {
Map<String, PackageConfig> configs = new HashMap<String, PackageConfig>();
Set<PackageConfig.Builder> builders;
while ((builders = findPackagesWithNoParents()).size() > 0) {
for (PackageConfig.Builder parent : builders) {
PackageConfig config = parent.build();
configs.put(config.getName(), config);
packageConfigBuilders.remove(config.getName());
for (Iterator<Map.Entry<PackageConfig.Builder,PackageConfig.Builder>> i = childToParent.entrySet().iterator(); i.hasNext(); ) {
Map.Entry<PackageConfig.Builder,PackageConfig.Builder> entry = i.next();
if (entry.getValue() == parent) {
entry.getKey().addParent(config);
i.remove();
}
}
}
}
return configs.values();
}
Set<PackageConfig.Builder> findPackagesWithNoParents() {
Set<PackageConfig.Builder> builders = new HashSet<PackageConfig.Builder>();
for (PackageConfig.Builder child : packageConfigBuilders.values()) {
if (!childToParent.containsKey(child)) {
builders.add(child);
}
}
return builders;
}
public ResultTypeConfig getDefaultResultType(PackageConfig.Builder pkgConfig) {
PackageConfig.Builder parent;
PackageConfig.Builder current = pkgConfig;
while ((parent = childToParent.get(current)) != null) {
current = parent;
}
return current.getResultType(current.getFullDefaultResultType());
}
}
}