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