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