1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.apache.struts2.config;
23
24 import java.lang.annotation.Annotation;
25 import java.lang.reflect.Modifier;
26 import java.net.MalformedURLException;
27 import java.net.URL;
28 import java.util.*;
29
30 import javax.servlet.ServletContext;
31
32 import com.opensymphony.xwork2.Action;
33 import com.opensymphony.xwork2.config.Configuration;
34 import com.opensymphony.xwork2.config.ConfigurationException;
35 import com.opensymphony.xwork2.config.ConfigurationProvider;
36 import com.opensymphony.xwork2.config.PackageProvider;
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.ClassTest;
47 import com.opensymphony.xwork2.util.location.LocatableProperties;
48 import com.opensymphony.xwork2.util.logging.Logger;
49 import com.opensymphony.xwork2.util.logging.LoggerFactory;
50
51 /***
52 * ClasspathPackageProvider loads the configuration
53 * by scanning the classpath or selected packages for Action classes.
54 * <p>
55 * This provider is only invoked if one or more action packages are passed to the dispatcher,
56 * usually from the web.xml.
57 * Configurations are created for objects that either implement Action or have classnames that end with "Action".
58 */
59 public class ClasspathPackageProvider implements PackageProvider {
60
61 /***
62 * The default page prefix (or "path").
63 * Some applications may place pages under "/WEB-INF" as an extreme security precaution.
64 */
65 protected static final String DEFAULT_PAGE_PREFIX = "struts.configuration.classpath.defaultPagePrefix";
66
67 /***
68 * The default page prefix (none).
69 */
70 private String defaultPagePrefix = "";
71
72 /***
73 * The default page extension, to use in place of ".jsp".
74 */
75 protected static final String DEFAULT_PAGE_EXTENSION = "struts.configuration.classpath.defaultPageExtension";
76
77 /***
78 * The defacto default page extension, usually associated with JavaServer Pages.
79 */
80 private String defaultPageExtension = ".jsp";
81
82 /***
83 * A setting to indicate a custom default parent package,
84 * to use in place of "struts-default".
85 */
86 protected static final String DEFAULT_PARENT_PACKAGE = "struts.configuration.classpath.defaultParentPackage";
87
88 /***
89 * A setting to disable action scanning.
90 */
91 protected static final String DISABLE_ACTION_SCANNING = "struts.configuration.classpath.disableActionScanning";
92
93 /***
94 * Name of the framework's default configuration package,
95 * that application configuration packages automatically inherit.
96 */
97 private String defaultParentPackage = "struts-default";
98
99 /***
100 * The default page prefix (or "path").
101 * Some applications may place pages under "/WEB-INF" as an extreme security precaution.
102 */
103 protected static final String FORCE_LOWER_CASE = "struts.configuration.classpath.forceLowerCase";
104
105 /***
106 * Whether to use a lowercase letter as the initial letter of an action.
107 * If false, actions will retain the initial uppercase letter from the Action class.
108 * (<code>view.action</code> (true) versus <code>View.action</code> (false)).
109 */
110 private boolean forceLowerCase = true;
111
112 /***
113 * Default suffix that can be used to indicate POJO "Action" classes.
114 */
115 private static final String ACTION = "Action";
116
117 /***
118 * Helper class to scan class path for server pages.
119 */
120 private PageLocator pageLocator = new ClasspathPageLocator();
121
122 /***
123 * Flag to indicate the packages have been loaded.
124 *
125 * @see #loadPackages
126 * @see #needsReload
127 */
128 private boolean initialized = false;
129
130 private boolean disableActionScanning = false;
131
132 private PackageLoader packageLoader;
133
134 /***
135 * Logging instance for this class.
136 */
137 private static final Logger LOG = LoggerFactory.getLogger(ClasspathPackageProvider.class);
138
139 /***
140 * The XWork Configuration for this application.
141 *
142 * @see #init
143 */
144 private Configuration configuration;
145
146 private String actionPackages;
147
148 private ServletContext servletContext;
149
150 public ClasspathPackageProvider() {
151 }
152
153 /***
154 * PageLocator defines a locate method that can be used to discover server pages.
155 */
156 public static interface PageLocator {
157 public URL locate(String path);
158 }
159
160 /***
161 * ClasspathPathLocator searches the classpath for server pages.
162 */
163 public static class ClasspathPageLocator implements PageLocator {
164 public URL locate(String path) {
165 return ClassLoaderUtil.getResource(path, getClass());
166 }
167 }
168
169 @Inject("actionPackages")
170 public void setActionPackages(String packages) {
171 this.actionPackages = packages;
172 }
173
174 public void setServletContext(ServletContext ctx) {
175 this.servletContext = ctx;
176 }
177
178 /***
179 * Disables action scanning.
180 *
181 * @param disableActionScanning True to disable
182 */
183 @Inject(value=DISABLE_ACTION_SCANNING, required=false)
184 public void setDisableActionScanning(String disableActionScanning) {
185 this.disableActionScanning = "true".equals(disableActionScanning);
186 }
187
188 /***
189 * Register a default parent package for the actions.
190 *
191 * @param defaultParentPackage the new defaultParentPackage
192 */
193 @Inject(value=DEFAULT_PARENT_PACKAGE, required=false)
194 public void setDefaultParentPackage(String defaultParentPackage) {
195 this.defaultParentPackage = defaultParentPackage;
196 }
197
198 /***
199 * Register a default page extension to use when locating pages.
200 *
201 * @param defaultPageExtension the new defaultPageExtension
202 */
203 @Inject(value=DEFAULT_PAGE_EXTENSION, required=false)
204 public void setDefaultPageExtension(String defaultPageExtension) {
205 this.defaultPageExtension = defaultPageExtension;
206 }
207
208 /***
209 * Reigster a default page prefix to use when locating pages.
210 *
211 * @param defaultPagePrefix the defaultPagePrefix to set
212 */
213 @Inject(value=DEFAULT_PAGE_PREFIX, required=false)
214 public void setDefaultPagePrefix(String defaultPagePrefix) {
215 this.defaultPagePrefix = defaultPagePrefix;
216 }
217
218 /***
219 * Whether to use a lowercase letter as the initial letter of an action.
220 *
221 * @param force If false, actions will retain the initial uppercase letter from the Action class.
222 * (<code>view.action</code> (true) versus <code>View.action</code> (false)).
223 */
224 @Inject(value=FORCE_LOWER_CASE, required=false)
225 public void setForceLowerCase(String force) {
226 this.forceLowerCase = "true".equals(force);
227 }
228
229 /***
230 * Register a PageLocation to use to scan for server pages.
231 *
232 * @param locator
233 */
234 public void setPageLocator(PageLocator locator) {
235 this.pageLocator = locator;
236 }
237
238 /***
239 * Scan a list of packages for Action classes.
240 *
241 * This method loads classes that implement the Action interface
242 * or have a class name that ends with the letters "Action".
243 *
244 * @param pkgs A list of packages to load
245 * @see #processActionClass
246 */
247 protected void loadPackages(String[] pkgs) {
248
249 packageLoader = new PackageLoader();
250 ResolverUtil<Class> resolver = new ResolverUtil<Class>();
251 resolver.find(createActionClassTest(), pkgs);
252
253 Set<? extends Class<? extends Class>> actionClasses = resolver.getClasses();
254 for (Object obj : actionClasses) {
255 Class cls = (Class) obj;
256 if (!Modifier.isAbstract(cls.getModifiers())) {
257 processActionClass(cls, pkgs);
258 }
259 }
260
261 for (PackageConfig config : packageLoader.createPackageConfigs()) {
262 configuration.addPackageConfig(config.getName(), config);
263 }
264 }
265
266 protected ClassTest createActionClassTest() {
267 return new ClassTest() {
268
269 public boolean matches(Class type) {
270
271 return (Action.class.isAssignableFrom(type) ||
272 type.getSimpleName().endsWith(getClassSuffix()) ||
273 type.getAnnotation(org.apache.struts2.config.Action.class) != null);
274 }
275
276 };
277 }
278
279 protected String getClassSuffix() {
280 return ACTION;
281 }
282
283 /***
284 * Create a default action mapping for a class instance.
285 *
286 * The namespace annotation is honored, if found, otherwise
287 * the Java package is converted into the namespace
288 * by changing the dots (".") to slashes ("/").
289 *
290 * @param cls Action or POJO instance to process
291 * @param pkgs List of packages that were scanned for Actions
292 */
293 protected void processActionClass(Class<?> cls, String[] pkgs) {
294 String name = cls.getName();
295 String actionPackage = cls.getPackage().getName();
296 String actionNamespace = null;
297 String actionName = null;
298
299 org.apache.struts2.config.Action actionAnn =
300 (org.apache.struts2.config.Action) cls.getAnnotation(org.apache.struts2.config.Action.class);
301 if (actionAnn != null) {
302 actionName = actionAnn.name();
303 if (actionAnn.namespace().equals(org.apache.struts2.config.Action.DEFAULT_NAMESPACE)) {
304 actionNamespace = "";
305 } else {
306 actionNamespace = actionAnn.namespace();
307 }
308 } else {
309 for (String pkg : pkgs) {
310 if (name.startsWith(pkg)) {
311 if (LOG.isDebugEnabled()) {
312 LOG.debug("ClasspathPackageProvider: Processing class "+name);
313 }
314 name = name.substring(pkg.length() + 1);
315
316 actionNamespace = "";
317 actionName = name;
318 int pos = name.lastIndexOf('.');
319 if (pos > -1) {
320 actionNamespace = "/" + name.substring(0, pos).replace('.','/');
321 actionName = name.substring(pos+1);
322 }
323 break;
324 }
325 }
326
327 if (actionName.endsWith(getClassSuffix())) {
328 actionName = actionName.substring(0, actionName.length() - getClassSuffix().length());
329 }
330
331
332 if ((forceLowerCase) && (actionName.length() > 1)) {
333 int lowerPos = actionName.lastIndexOf('/') + 1;
334 StringBuilder sb = new StringBuilder();
335 sb.append(actionName.substring(0, lowerPos));
336 sb.append(Character.toLowerCase(actionName.charAt(lowerPos)));
337 sb.append(actionName.substring(lowerPos + 1));
338 actionName = sb.toString();
339 }
340 }
341
342 PackageConfig.Builder pkgConfig = loadPackageConfig(actionNamespace, actionPackage, cls);
343
344
345 if (!actionPackage.equals(pkgConfig.getName())) {
346 actionPackage = pkgConfig.getName();
347 }
348
349 Annotation annotation = cls.getAnnotation(ParentPackage.class);
350 if (annotation != null) {
351 String parent = ((ParentPackage)annotation).value();
352 PackageConfig parentPkg = configuration.getPackageConfig(parent);
353 if (parentPkg == null) {
354 throw new ConfigurationException("ClasspathPackageProvider: Unable to locate parent package "+parent, annotation);
355 }
356 pkgConfig.addParent(parentPkg);
357
358 if (!TextUtils.stringSet(pkgConfig.getNamespace()) && TextUtils.stringSet(parentPkg.getNamespace())) {
359 pkgConfig.namespace(parentPkg.getNamespace());
360 }
361 }
362
363 ResultTypeConfig defaultResultType = packageLoader.getDefaultResultType(pkgConfig);
364 ActionConfig actionConfig = new ActionConfig.Builder(actionPackage, actionName, cls.getName())
365 .addResultConfigs(new ResultMap<String,ResultConfig>(cls, actionName, defaultResultType))
366 .build();
367 pkgConfig.addActionConfig(actionName, actionConfig);
368 }
369
370 /***
371 * Finds or creates the package configuration for an Action class.
372 *
373 * The namespace annotation is honored, if found,
374 * and the namespace is checked for a parent configuration.
375 *
376 * @param actionNamespace The configuration namespace
377 * @param actionPackage The Java package containing our Action classes
378 * @param actionClass The Action class instance
379 * @return PackageConfig object for the Action class
380 */
381 protected PackageConfig.Builder loadPackageConfig(String actionNamespace, String actionPackage, Class actionClass) {
382 PackageConfig.Builder parent = null;
383
384
385 if (actionClass != null) {
386 Namespace ns = (Namespace) actionClass.getAnnotation(Namespace.class);
387 if (ns != null) {
388 parent = loadPackageConfig(actionNamespace, actionPackage, null);
389 actionNamespace = ns.value();
390 actionPackage = actionClass.getName();
391
392
393 } else {
394 org.apache.struts2.config.Action actionAnn =
395 (org.apache.struts2.config.Action) actionClass.getAnnotation(org.apache.struts2.config.Action.class);
396 if (actionAnn != null && !actionAnn.DEFAULT_NAMESPACE.equals(actionAnn.namespace())) {
397
398 parent = loadPackageConfig(null, actionPackage, null);
399 actionPackage = actionClass.getName();
400 }
401 }
402 }
403
404
405 PackageConfig.Builder pkgConfig = packageLoader.getPackage(actionPackage);
406 if (pkgConfig == null) {
407 pkgConfig = new PackageConfig.Builder(actionPackage);
408
409 pkgConfig.namespace(actionNamespace);
410 if (parent == null) {
411 PackageConfig cfg = configuration.getPackageConfig(defaultParentPackage);
412 if (cfg != null) {
413 pkgConfig.addParent(cfg);
414 } else {
415 throw new ConfigurationException("ClasspathPackageProvider: Unable to locate default parent package: " +
416 defaultParentPackage);
417 }
418 }
419
420 packageLoader.registerPackage(pkgConfig);
421
422
423 } else if (pkgConfig.getNamespace() == null) {
424 pkgConfig.namespace(actionNamespace);
425 }
426
427 if (parent != null) {
428 packageLoader.registerChildToParent(pkgConfig, parent);
429 }
430
431 System.out.println("class:"+actionClass+" parent:"+parent+" current:"+(pkgConfig != null ? pkgConfig.getName() : ""));
432
433 return pkgConfig;
434 }
435
436 /***
437 * Default destructor. Override to provide behavior.
438 */
439 public void destroy() {
440
441 }
442
443 /***
444 * Register this application's configuration.
445 *
446 * @param config The configuration for this application.
447 */
448 public void init(Configuration config) {
449 this.configuration = config;
450 }
451
452 /***
453 * Clears and loads the list of packages registered at construction.
454 *
455 * @throws ConfigurationException
456 */
457 public void loadPackages() throws ConfigurationException {
458 if (actionPackages != null && !disableActionScanning) {
459 String[] names = actionPackages.split("//s*[,]//s*");
460
461 if (names.length > 0) {
462 setPageLocator(new ServletContextPageLocator(servletContext));
463 }
464 loadPackages(names);
465 }
466 initialized = true;
467 }
468
469 /***
470 * Indicates whether the packages have been initialized.
471 *
472 * @return True if the packages have been initialized
473 */
474 public boolean needsReload() {
475 return !initialized;
476 }
477
478 /***
479 * Creates ResultConfig objects from result annotations,
480 * and if a result isn't found, creates it on the fly.
481 */
482 class ResultMap<K,V> extends HashMap<K,V> {
483 private Class actionClass;
484 private String actionName;
485 private ResultTypeConfig defaultResultType;
486
487 public ResultMap(Class actionClass, String actionName, ResultTypeConfig defaultResultType) {
488 this.actionClass = actionClass;
489 this.actionName = actionName;
490 this.defaultResultType = defaultResultType;
491
492
493 while (!actionClass.getName().equals(Object.class.getName())) {
494
495 Results results = (Results) actionClass.getAnnotation(Results.class);
496 if (results != null) {
497
498 for (int i = 0; i < results.value().length; i++) {
499 Result result = results.value()[i];
500 ResultConfig config = createResultConfig(result);
501 if (!containsKey((K)config.getName())) {
502 put((K)config.getName(), (V)config);
503 }
504 }
505 }
506
507
508 Result result = (Result) actionClass.getAnnotation(Result.class);
509 if (result != null) {
510 ResultConfig config = createResultConfig(result);
511 if (!containsKey((K)config.getName())) {
512 put((K)config.getName(), (V)config);
513 }
514 }
515
516 actionClass = actionClass.getSuperclass();
517 }
518 }
519
520 /***
521 * Extracts result name and value and calls {@link #createResultConfig}.
522 *
523 * @param result Result annotation reference representing result type to create
524 * @return New or cached ResultConfig object for result
525 */
526 protected ResultConfig createResultConfig(Result result) {
527 Class<? extends Object> cls = result.type();
528 if (cls == NullResult.class) {
529 cls = null;
530 }
531 return createResultConfig(result.name(), cls, result.value(), createParameterMap(result.params()));
532 }
533
534 protected Map<String, String> createParameterMap(String[] parms) {
535 Map<String, String> map = new HashMap<String, String>();
536 int subtract = parms.length % 2;
537 if(subtract != 0) {
538 LOG.warn("Odd number of result parameters key/values specified. The final one will be ignored.");
539 }
540 for (int i = 0; i < parms.length - subtract; i++) {
541 String key = parms[i++];
542 String value = parms[i];
543 map.put(key, value);
544 if(LOG.isDebugEnabled()) {
545 LOG.debug("Adding parmeter["+key+":"+value+"] to result.");
546 }
547 }
548 return map;
549 }
550
551 /***
552 * Creates a default ResultConfig,
553 * using either the resultClass or the default ResultType for configuration package
554 * associated this ResultMap class.
555 *
556 * @param key The result type name
557 * @param resultClass The class for the result type
558 * @param location Path to the resource represented by this type
559 * @return A ResultConfig for key mapped to location
560 */
561 private ResultConfig createResultConfig(Object key, Class<? extends Object> resultClass,
562 String location,
563 Map<? extends Object,? extends Object > configParams) {
564 if (resultClass == null) {
565 configParams = defaultResultType.getParams();
566 String className = defaultResultType.getClassName();
567 try {
568 resultClass = ClassLoaderUtil.loadClass(className, getClass());
569 } catch (ClassNotFoundException ex) {
570 throw new ConfigurationException("ClasspathPackageProvider: Unable to locate result class "+className, actionClass);
571 }
572 }
573
574 String defaultParam;
575 try {
576 defaultParam = (String) resultClass.getField("DEFAULT_PARAM").get(null);
577 } catch (Exception e) {
578
579 defaultParam = "location";
580 }
581
582 HashMap params = new HashMap();
583 if (configParams != null) {
584 params.putAll(configParams);
585 }
586
587 params.put(defaultParam, location);
588 return new ResultConfig.Builder((String) key, resultClass.getName()).addParams(params).build();
589 }
590 }
591
592 /***
593 * Search classpath for a page.
594 */
595 private final class ServletContextPageLocator implements PageLocator {
596 private final ServletContext context;
597 private ClasspathPageLocator classpathPageLocator = new ClasspathPageLocator();
598
599 private ServletContextPageLocator(ServletContext context) {
600 this.context = context;
601 }
602
603 public URL locate(String path) {
604 URL url = null;
605 try {
606 url = context.getResource(path);
607 if (url == null) {
608 url = classpathPageLocator.locate(path);
609 }
610 } catch (MalformedURLException e) {
611 if (LOG.isDebugEnabled()) {
612 LOG.debug("Unable to resolve path "+path+" against the servlet context");
613 }
614 }
615 return url;
616 }
617 }
618
619 private static class PackageLoader {
620
621 /***
622 * The package configurations for scanned Actions.
623 */
624 private Map<String,PackageConfig.Builder> packageConfigBuilders = new HashMap<String,PackageConfig.Builder>();
625
626 private Map<PackageConfig.Builder,PackageConfig.Builder> childToParent = new HashMap<PackageConfig.Builder,PackageConfig.Builder>();
627
628 public PackageConfig.Builder getPackage(String name) {
629 return packageConfigBuilders.get(name);
630 }
631
632 public void registerChildToParent(PackageConfig.Builder child, PackageConfig.Builder parent) {
633 childToParent.put(child, parent);
634 }
635
636 public void registerPackage(PackageConfig.Builder builder) {
637 packageConfigBuilders.put(builder.getName(), builder);
638 }
639
640 public Collection<PackageConfig> createPackageConfigs() {
641 Map<String, PackageConfig> configs = new HashMap<String, PackageConfig>();
642
643 Set<PackageConfig.Builder> builders;
644 while ((builders = findPackagesWithNoParents()).size() > 0) {
645 for (PackageConfig.Builder parent : builders) {
646 PackageConfig config = parent.build();
647 configs.put(config.getName(), config);
648 packageConfigBuilders.remove(config.getName());
649
650 for (Iterator<Map.Entry<PackageConfig.Builder,PackageConfig.Builder>> i = childToParent.entrySet().iterator(); i.hasNext(); ) {
651 Map.Entry<PackageConfig.Builder,PackageConfig.Builder> entry = i.next();
652 if (entry.getValue() == parent) {
653 entry.getKey().addParent(config);
654 i.remove();
655 }
656 }
657 }
658 }
659 return configs.values();
660 }
661
662 Set<PackageConfig.Builder> findPackagesWithNoParents() {
663 Set<PackageConfig.Builder> builders = new HashSet<PackageConfig.Builder>();
664 for (PackageConfig.Builder child : packageConfigBuilders.values()) {
665 if (!childToParent.containsKey(child)) {
666 builders.add(child);
667 }
668 }
669 return builders;
670 }
671
672 public ResultTypeConfig getDefaultResultType(PackageConfig.Builder pkgConfig) {
673 PackageConfig.Builder parent;
674 PackageConfig.Builder current = pkgConfig;
675
676 while ((parent = childToParent.get(current)) != null) {
677 current = parent;
678 }
679 return current.getResultType(current.getFullDefaultResultType());
680 }
681 }
682 }