View Javadoc

1   /*
2    * $Id: ClasspathPackageProvider.java 651946 2008-04-27 13:41:38Z apetrelli $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
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             // Match Action implementations and classes ending with "Action"
269             public boolean matches(Class type) {
270                 // TODO: should also find annotated classes
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             // Truncate Action suffix if found
327             if (actionName.endsWith(getClassSuffix())) {
328                 actionName = actionName.substring(0, actionName.length() - getClassSuffix().length());
329             }
330 
331             // Force initial letter of action to lowercase, if desired
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         // In case the package changed due to namespace annotation processing
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         // Check for the @Namespace annotation
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             // See if the namespace has been overridden by the @Action annotation    
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                     // we pass null as the namespace in case the parent package hasn't been loaded yet
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         // if the parent package was first created by a child, ensure the namespace is correct
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             // Initialize the classloader scanner with the configured packages
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             // check if any annotations are around
493             while (!actionClass.getName().equals(Object.class.getName())) {
494                 //noinspection unchecked
495                 Results results = (Results) actionClass.getAnnotation(Results.class);
496                 if (results != null) {
497                     // first check here...
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                 // what about a single Result annotation?
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                 // not sure why this happened, but let's just use a sensible choice
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 }