1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.apache.struts2.config;
22
23 import java.lang.annotation.Annotation;
24 import java.lang.reflect.Modifier;
25 import java.net.URL;
26 import java.util.HashMap;
27 import java.util.Map;
28 import java.util.Set;
29
30 import org.apache.commons.logging.Log;
31 import org.apache.commons.logging.LogFactory;
32
33 import com.opensymphony.xwork2.Action;
34 import com.opensymphony.xwork2.config.Configuration;
35 import com.opensymphony.xwork2.config.ConfigurationException;
36 import com.opensymphony.xwork2.config.ConfigurationProvider;
37 import com.opensymphony.xwork2.config.entities.ActionConfig;
38 import com.opensymphony.xwork2.config.entities.PackageConfig;
39 import com.opensymphony.xwork2.config.entities.ResultConfig;
40 import com.opensymphony.xwork2.config.entities.ResultTypeConfig;
41 import com.opensymphony.xwork2.inject.ContainerBuilder;
42 import com.opensymphony.xwork2.inject.Inject;
43 import com.opensymphony.xwork2.util.ClassLoaderUtil;
44 import com.opensymphony.xwork2.util.ResolverUtil;
45 import com.opensymphony.xwork2.util.TextUtils;
46 import com.opensymphony.xwork2.util.ResolverUtil.Test;
47 import com.opensymphony.xwork2.util.location.LocatableProperties;
48
49 /***
50 * ClasspathConfigurationProvider loads the configuration
51 * by scanning the classpath or selected packages for Action classes.
52 * <p>
53 * This provider is only invoked if one or more action packages are passed to the dispatcher,
54 * usually from the web.xml.
55 * Configurations are created for objects that either implement Action or have classnames that end with "Action".
56 */
57 public class ClasspathConfigurationProvider implements ConfigurationProvider {
58
59 /***
60 * The default page prefix (or "path").
61 * Some applications may place pages under "/WEB-INF" as an extreme security precaution.
62 */
63 private static final String DEFAULT_PAGE_PREFIX = "struts.configuration.classpath.defaultPagePrefix";
64
65 /***
66 * The default page prefix (none).
67 */
68 private String defaultPagePrefix = "";
69
70 /***
71 * The default page extension, to use in place of ".jsp".
72 */
73 private static final String DEFAULT_PAGE_EXTENSION = "struts.configuration.classpath.defaultPageExtension";
74
75 /***
76 * The defacto default page extension, usually associated with JavaServer Pages.
77 */
78 private String defaultPageExtension = ".jsp";
79
80 /***
81 * A setting to indicate a custom default parent package,
82 * to use in place of "struts-default".
83 */
84 private static final String DEFAULT_PARENT_PACKAGE = "struts.configuration.classpath.defaultParentPackage";
85
86 /***
87 * Name of the framework's default configuration package,
88 * that application configuration packages automatically inherit.
89 */
90 private String defaultParentPackage = "struts-default";
91
92 /***
93 * The default page prefix (or "path").
94 * Some applications may place pages under "/WEB-INF" as an extreme security precaution.
95 */
96 private static final String FORCE_LOWER_CASE = "struts.configuration.classpath.forceLowerCase";
97
98 /***
99 * Whether to use a lowercase letter as the initial letter of an action.
100 * If false, actions will retain the initial uppercase letter from the Action class.
101 * (<code>view.action</code> (true) versus <code>View.action</code> (false)).
102 */
103 private boolean forceLowerCase = true;
104
105 /***
106 * Default suffix that can be used to indicate POJO "Action" classes.
107 */
108 private static final String ACTION = "Action";
109
110 /***
111 * Helper class to scan class path for server pages.
112 */
113 private PageLocator pageLocator = new ClasspathPageLocator();
114
115 /***
116 * Flag to indicate the packages have been loaded.
117 *
118 * @see #loadPackages
119 * @see #needsReload
120 */
121 private boolean initialized = false;
122
123 /***
124 * The list of packages to scan for Action classes.
125 */
126 private String[] packages;
127
128 /***
129 * The package configurations for scanned Actions.
130 *
131 * @see #loadPackageConfig
132 */
133 private Map<String,PackageConfig> loadedPackageConfigs = new HashMap<String,PackageConfig>();
134
135 /***
136 * Logging instance for this class.
137 */
138 private static final Log LOG = LogFactory.getLog(ClasspathConfigurationProvider.class);
139
140 /***
141 * The XWork Configuration for this application.
142 *
143 * @see #init
144 */
145 private Configuration configuration;
146
147 /***
148 * Create instance utilizing a list of packages to scan for Action classes.
149 *
150 * @param pkgs List of pacaktges to scan for Action Classes.
151 */
152 public ClasspathConfigurationProvider(String[] pkgs) {
153 this.packages = pkgs;
154 }
155
156 /***
157 * PageLocator defines a locate method that can be used to discover server pages.
158 */
159 public static interface PageLocator {
160 public URL locate(String path);
161 }
162
163 /***
164 * ClasspathPathLocator searches the classpath for server pages.
165 */
166 public static class ClasspathPageLocator implements PageLocator {
167 public URL locate(String path) {
168 return ClassLoaderUtil.getResource(path, getClass());
169 }
170 }
171
172 /***
173 * Register a default parent package for the actions.
174 *
175 * @param defaultParentPackage the new defaultParentPackage
176 */
177 @Inject(value=DEFAULT_PARENT_PACKAGE, required=false)
178 public void setDefaultParentPackage(String defaultParentPackage) {
179 this.defaultParentPackage = defaultParentPackage;
180 }
181
182 /***
183 * Register a default page extension to use when locating pages.
184 *
185 * @param defaultPageExtension the new defaultPageExtension
186 */
187 @Inject(value=DEFAULT_PAGE_EXTENSION, required=false)
188 public void setDefaultPageExtension(String defaultPageExtension) {
189 this.defaultPageExtension = defaultPageExtension;
190 }
191
192 /***
193 * Reigster a default page prefix to use when locating pages.
194 *
195 * @param defaultPagePrefix the defaultPagePrefix to set
196 */
197 @Inject(value=DEFAULT_PAGE_PREFIX, required=false)
198 public void setDefaultPagePrefix(String defaultPagePrefix) {
199 this.defaultPagePrefix = defaultPagePrefix;
200 }
201
202 /***
203 * Whether to use a lowercase letter as the initial letter of an action.
204 *
205 * @param force If false, actions will retain the initial uppercase letter from the Action class.
206 * (<code>view.action</code> (true) versus <code>View.action</code> (false)).
207 */
208 @Inject(value=FORCE_LOWER_CASE, required=false)
209 public void setForceLowerCase(String force) {
210 this.forceLowerCase = "true".equals(force);
211 }
212
213 /***
214 * Register a PageLocation to use to scan for server pages.
215 *
216 * @param locator
217 */
218 public void setPageLocator(PageLocator locator) {
219 this.pageLocator = locator;
220 }
221
222 /***
223 * Scan a list of packages for Action classes.
224 *
225 * This method loads classes that implement the Action interface
226 * or have a class name that ends with the letters "Action".
227 *
228 * @param pkgs A list of packages to load
229 * @see #processActionClass
230 */
231 protected void loadPackages(String[] pkgs) {
232
233 ResolverUtil<Class> resolver = new ResolverUtil<Class>();
234 resolver.find(new Test() {
235
236 public boolean matches(Class type) {
237
238 return (Action.class.isAssignableFrom(type) ||
239 type.getSimpleName().endsWith("Action"));
240 }
241
242 }, pkgs);
243
244 Set<? extends Class<? extends Class>> actionClasses = resolver.getClasses();
245 for (Object obj : actionClasses) {
246 Class cls = (Class) obj;
247 if (!Modifier.isAbstract(cls.getModifiers())) {
248 processActionClass(cls, pkgs);
249 }
250 }
251
252 for (String key : loadedPackageConfigs.keySet()) {
253 configuration.addPackageConfig(key, loadedPackageConfigs.get(key));
254 }
255 }
256
257 /***
258 * Create a default action mapping for a class instance.
259 *
260 * The namespace annotation is honored, if found, otherwise
261 * the Java package is converted into the namespace
262 * by changing the dots (".") to slashes ("/").
263 *
264 * @param cls Action or POJO instance to process
265 * @param pkgs List of packages that were scanned for Actions
266 */
267 protected void processActionClass(Class cls, String[] pkgs) {
268 String name = cls.getName();
269 String actionPackage = cls.getPackage().getName();
270 String actionNamespace = null;
271 String actionName = null;
272 for (String pkg : pkgs) {
273 if (name.startsWith(pkg)) {
274 if (LOG.isDebugEnabled()) {
275 LOG.debug("ClasspathConfigurationProvider: Processing class "+name);
276 }
277 name = name.substring(pkg.length() + 1);
278
279 actionNamespace = "";
280 actionName = name;
281 int pos = name.lastIndexOf('.');
282 if (pos > -1) {
283 actionNamespace = "/" + name.substring(0, pos).replace('.','/');
284 actionName = name.substring(pos+1);
285 }
286 break;
287 }
288 }
289
290 PackageConfig pkgConfig = loadPackageConfig(actionNamespace, actionPackage, cls);
291
292
293 if (!actionPackage.equals(pkgConfig.getName())) {
294 actionPackage = pkgConfig.getName();
295 }
296
297 Annotation annotation = cls.getAnnotation(ParentPackage.class);
298 if (annotation != null) {
299 String parent = ((ParentPackage)annotation).value();
300 PackageConfig parentPkg = configuration.getPackageConfig(parent);
301 if (parentPkg == null) {
302 throw new ConfigurationException("ClasspathConfigurationProvider: Unable to locate parent package "+parent, annotation);
303 }
304 pkgConfig.addParent(parentPkg);
305
306 if (!TextUtils.stringSet(pkgConfig.getNamespace()) && TextUtils.stringSet(parentPkg.getNamespace())) {
307 pkgConfig.setNamespace(parentPkg.getNamespace());
308 }
309 }
310
311
312 if (actionName.endsWith(ACTION)) {
313 actionName = actionName.substring(0, actionName.length() - ACTION.length());
314 }
315
316
317 if ((forceLowerCase) && (actionName.length() > 1)) {
318 int lowerPos = actionName.lastIndexOf('/') + 1;
319 StringBuilder sb = new StringBuilder();
320 sb.append(actionName.substring(0, lowerPos));
321 sb.append(Character.toLowerCase(actionName.charAt(lowerPos)));
322 sb.append(actionName.substring(lowerPos + 1));
323 actionName = sb.toString();
324 }
325
326 ActionConfig actionConfig = new ActionConfig();
327 actionConfig.setClassName(cls.getName());
328 actionConfig.setPackageName(actionPackage);
329
330 actionConfig.setResults(new ResultMap<String,ResultConfig>(cls, actionName, pkgConfig));
331 pkgConfig.addActionConfig(actionName, actionConfig);
332 }
333
334 /***
335 * Finds or creates the package configuration for an Action class.
336 *
337 * The namespace annotation is honored, if found,
338 * and the namespace is checked for a parent configuration.
339 *
340 * @param actionNamespace The configuration namespace
341 * @param actionPackage The Java package containing our Action classes
342 * @param actionClass The Action class instance
343 * @return PackageConfig object for the Action class
344 */
345 protected PackageConfig loadPackageConfig(String actionNamespace, String actionPackage, Class actionClass) {
346 PackageConfig parent = null;
347
348 if (actionClass != null) {
349 Namespace ns = (Namespace) actionClass.getAnnotation(Namespace.class);
350 if (ns != null) {
351 parent = loadPackageConfig(actionNamespace, actionPackage, null);
352 actionNamespace = ns.value();
353 actionPackage = actionClass.getName();
354 }
355 }
356
357 PackageConfig pkgConfig = loadedPackageConfigs.get(actionPackage);
358 if (pkgConfig == null) {
359 pkgConfig = new PackageConfig();
360 pkgConfig.setName(actionPackage);
361
362 if (parent == null) {
363 parent = configuration.getPackageConfig(defaultParentPackage);
364 }
365
366 if (parent == null) {
367 throw new ConfigurationException("ClasspathConfigurationProvider: Unable to locate default parent package: " +
368 defaultParentPackage);
369 }
370 pkgConfig.addParent(parent);
371
372 pkgConfig.setNamespace(actionNamespace);
373
374 loadedPackageConfigs.put(actionPackage, pkgConfig);
375 }
376 return pkgConfig;
377 }
378
379 /***
380 * Default destructor. Override to provide behavior.
381 */
382 public void destroy() {
383
384 }
385
386 /***
387 * Register this application's configuration.
388 *
389 * @param config The configuration for this application.
390 */
391 public void init(Configuration config) {
392 this.configuration = config;
393 }
394
395 /***
396 * Clears and loads the list of packages registered at construction.
397 *
398 * @throws ConfigurationException
399 */
400 public void loadPackages() throws ConfigurationException {
401 loadedPackageConfigs.clear();
402 loadPackages(packages);
403 initialized = true;
404 }
405
406 /***
407 * Indicates whether the packages have been initialized.
408 *
409 * @return True if the packages have been initialized
410 */
411 public boolean needsReload() {
412 return !initialized;
413 }
414
415 /***
416 * Creates ResultConfig objects from result annotations,
417 * and if a result isn't found, creates it on the fly.
418 */
419 class ResultMap<K,V> extends HashMap<K,V> {
420 private Class actionClass;
421 private String actionName;
422 private PackageConfig pkgConfig;
423
424 public ResultMap(Class actionClass, String actionName, PackageConfig pkgConfig) {
425 this.actionClass = actionClass;
426 this.actionName = actionName;
427 this.pkgConfig = pkgConfig;
428
429
430 while (!actionClass.getName().equals(Object.class.getName())) {
431
432 Results results = (Results) actionClass.getAnnotation(Results.class);
433 if (results != null) {
434
435 for (int i = 0; i < results.value().length; i++) {
436 Result result = results.value()[i];
437 ResultConfig config = createResultConfig(result);
438 put((K)config.getName(), (V)config);
439 }
440 }
441
442
443 Result result = (Result) actionClass.getAnnotation(Result.class);
444 if (result != null) {
445 ResultConfig config = createResultConfig(result);
446 put((K)config.getName(), (V)config);
447 }
448
449 actionClass = actionClass.getSuperclass();
450 }
451 }
452
453 /***
454 * Extracts result name and value and calls {@link #createResultConfig}.
455 *
456 * @param result Result annotation reference representing result type to create
457 * @return New or cached ResultConfig object for result
458 */
459 protected ResultConfig createResultConfig(Result result) {
460 Class<? extends Object> cls = result.type();
461 if (cls == NullResult.class) {
462 cls = null;
463 }
464 return createResultConfig(result.name(), cls, result.value(), createParameterMap(result.params()));
465 }
466
467 protected Map<String, String> createParameterMap(String[] parms) {
468 Map<String, String> map = new HashMap<String, String>();
469 int subtract = parms.length % 2;
470 if(subtract != 0) {
471 LOG.warn("Odd number of result parameters key/values specified. The final one will be ignored.");
472 }
473 for (int i = 0; i < parms.length - subtract; i++) {
474 String key = parms[i++];
475 String value = parms[i];
476 map.put(key, value);
477 if(LOG.isDebugEnabled()) {
478 LOG.debug("Adding parmeter["+key+":"+value+"] to result.");
479 }
480 }
481 return map;
482 }
483
484 /***
485 * Creates a default ResultConfig,
486 * using either the resultClass or the default ResultType for configuration package
487 * associated this ResultMap class.
488 *
489 * @param key The result type name
490 * @param resultClass The class for the result type
491 * @param location Path to the resource represented by this type
492 * @return A ResultConfig for key mapped to location
493 */
494 private ResultConfig createResultConfig(Object key, Class<? extends Object> resultClass,
495 String location,
496 Map<? extends Object,? extends Object > configParams) {
497 if (resultClass == null) {
498 String defaultResultType = pkgConfig.getFullDefaultResultType();
499 ResultTypeConfig resultType = pkgConfig.getAllResultTypeConfigs().get(defaultResultType);
500 configParams = resultType.getParams();
501 String className = resultType.getClazz();
502 try {
503 resultClass = ClassLoaderUtil.loadClass(className, getClass());
504 } catch (ClassNotFoundException ex) {
505 throw new ConfigurationException("ClasspathConfigurationProvider: Unable to locate result class "+className, actionClass);
506 }
507 }
508
509 String defaultParam;
510 try {
511 defaultParam = (String) resultClass.getField("DEFAULT_PARAM").get(null);
512 } catch (Exception e) {
513
514 defaultParam = "location";
515 }
516
517 HashMap params = new HashMap();
518 if (configParams != null) {
519 params.putAll(configParams);
520 }
521
522 params.put(defaultParam, location);
523 return new ResultConfig((String) key, resultClass.getName(), params);
524 }
525 }
526
527
528 public void register(ContainerBuilder builder, LocatableProperties props) throws ConfigurationException {
529
530 }
531 }