View Javadoc

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