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 }