View Javadoc

    1   /*
    2    * $Id: I18nFactorySet.java 471754 2006-11-06 14:55:09Z husted $
    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.struts.tiles.xmlDefinition;
    23
    24  import java.io.FileNotFoundException;
    25  import java.io.IOException;
    26  import java.io.InputStream;
    27  import java.util.ArrayList;
    28  import java.util.HashMap;
    29  import java.util.Iterator;
    30  import java.util.List;
    31  import java.util.Locale;
    32  import java.util.Map;
    33  import java.util.StringTokenizer;
    34
    35  import javax.servlet.ServletContext;
    36  import javax.servlet.ServletRequest;
    37  import javax.servlet.http.HttpServletRequest;
    38  import javax.servlet.http.HttpSession;
    39
    40  import org.apache.commons.logging.Log;
    41  import org.apache.commons.logging.LogFactory;
    42  import org.apache.struts.tiles.taglib.ComponentConstants;
    43  import org.apache.struts.tiles.DefinitionsFactoryException;
    44  import org.apache.struts.tiles.FactoryNotFoundException;
    45  import org.xml.sax.SAXException;
    46
    47  /**
    48   * Definitions factory.
    49   * This implementation allows to have a set of definition factories.
    50   * There is a main factory and one factory for each file associated to a Locale.
    51   *
    52   * To retrieve a definition, we first search for the appropriate factory using
    53   * the Locale found in session context. If no factory is found, use the
    54   * default one. Then we ask the factory for the definition.
    55   *
    56   * A definition factory file is loaded using main filename extended with locale code
    57   * (ex : <code>templateDefinitions_fr.xml</code>). If no file is found under this name, use default file.
    58   */
    59  public class I18nFactorySet extends FactorySet {
    60
    61      /**
    62       * Commons Logging instance.
    63       */
    64      protected static Log log = LogFactory.getLog(I18nFactorySet.class);
    65
    66      /**
    67       * Config file parameter name.
    68       */
    69      public static final String DEFINITIONS_CONFIG_PARAMETER_NAME =
    70          "definitions-config";
    71
    72      /**
    73       * Config file parameter name.
    74       */
    75      public static final String PARSER_DETAILS_PARAMETER_NAME =
    76          "definitions-parser-details";
    77
    78      /**
    79       * Config file parameter name.
    80       */
    81      public static final String PARSER_VALIDATE_PARAMETER_NAME =
    82          "definitions-parser-validate";
    83
    84      /**
    85       * Possible definition filenames.
    86       */
    87      public static final String DEFAULT_DEFINITION_FILENAMES[] =
    88          {
    89              "/WEB-INF/tileDefinitions.xml",
    90              "/WEB-INF/componentDefinitions.xml",
    91              "/WEB-INF/instanceDefinitions.xml" };
    92
    93      /**
    94       * Default filenames extension.
    95       */
    96      public static final String FILENAME_EXTENSION = ".xml";
    97
    98      /**
    99       * Default factory.
    100      */
    101     protected DefinitionsFactory defaultFactory = null;
    102
    103     /**
    104      * XML parser used.
    105      * Attribute is transient to allow serialization. In this implementaiton,
    106      * xmlParser is created each time we need it ;-(.
    107      */
    108     protected transient XmlParser xmlParser;
    109
    110     /**
    111      * Do we want validating parser. Default is <code>false</code>.
    112      * Can be set from servlet config file.
    113      */
    114     protected boolean isValidatingParser = false;
    115
    116     /**
    117      * Parser detail level. Default is 0.
    118      * Can be set from servlet config file.
    119      */
    120     protected int parserDetailLevel = 0;
    121
    122     /**
    123      * Names of files containing instances descriptions.
    124      */
    125     private List filenames = null;
    126
    127     /**
    128      * Collection of already loaded definitions set, referenced by their suffix.
    129      */
    130     private Map loaded = null;
    131
    132     /**
    133      * Parameterless Constructor.
    134      * Method {@link #initFactory} must be called prior to any use of created factory.
    135      */
    136     public I18nFactorySet() {
    137         super();
    138     }
    139
    140     /**
    141      * Constructor.
    142      * Init the factory by reading appropriate configuration file.
    143      * @param servletContext Servlet context.
    144      * @param properties Map containing all properties.
    145      * @throws FactoryNotFoundException Can't find factory configuration file.
    146      */
    147     public I18nFactorySet(ServletContext servletContext, Map properties)
    148         throws DefinitionsFactoryException {
    149
    150         initFactory(servletContext, properties);
    151     }
    152
    153     /**
    154      * Initialization method.
    155      * Init the factory by reading appropriate configuration file.
    156      * This method is called exactly once immediately after factory creation in
    157      * case of internal creation (by DefinitionUtil).
    158      * @param servletContext Servlet Context passed to newly created factory.
    159      * @param properties Map of name/property passed to newly created factory. Map can contains
    160      * more properties than requested.
    161      * @throws DefinitionsFactoryException An error occur during initialization.
    162      */
    163     public void initFactory(ServletContext servletContext, Map properties)
    164         throws DefinitionsFactoryException {
    165
    166         // Set some property values
    167         String value = (String) properties.get(PARSER_VALIDATE_PARAMETER_NAME);
    168         if (value != null) {
    169             isValidatingParser = Boolean.valueOf(value).booleanValue();
    170         }
    171
    172         value = (String) properties.get(PARSER_DETAILS_PARAMETER_NAME);
    173         if (value != null) {
    174             try {
    175                 parserDetailLevel = Integer.valueOf(value).intValue();
    176
    177             } catch (NumberFormatException ex) {
    178                 log.error(
    179                     "Bad format for parameter '"
    180                         + PARSER_DETAILS_PARAMETER_NAME
    181                         + "'. Integer expected.");
    182             }
    183         }
    184
    185         // init factory withappropriate configuration file
    186         // Try to use provided filename, if any.
    187         // If no filename are provided, try to use default ones.
    188         String filename = (String) properties.get(DEFINITIONS_CONFIG_PARAMETER_NAME);
    189         if (filename != null) { // Use provided filename
    190             try {
    191                 initFactory(servletContext, filename);
    192                 if (log.isDebugEnabled()) {
    193                     log.debug("Factory initialized from file '" + filename + "'.");
    194                 }
    195
    196             } catch (FileNotFoundException ex) { // A filename is specified, throw appropriate error.
    197                 log.error(ex.getMessage() + " : Can't find file '" + filename + "'");
    198                 throw new FactoryNotFoundException(
    199                     ex.getMessage() + " : Can't find file '" + filename + "'");
    200             }
    201
    202         } else { // try each default file names
    203             for (int i = 0; i < DEFAULT_DEFINITION_FILENAMES.length; i++) {
    204                 filename = DEFAULT_DEFINITION_FILENAMES[i];
    205                 try {
    206                     initFactory(servletContext, filename);
    207                     if (log.isInfoEnabled()) {
    208                         log.info(
    209                             "Factory initialized from file '" + filename + "'.");
    210                     }
    211                 } catch (FileNotFoundException ex) {
    212                     // Do nothing
    213                 }
    214             }
    215         }
    216
    217     }
    218
    219     /**
    220      * Initialization method.
    221      * Init the factory by reading appropriate configuration file.
    222      * This method is called exactly once immediately after factory creation in
    223      * case of internal creation (by DefinitionUtil).
    224      * @param servletContext Servlet Context passed to newly created factory.
    225      * @param proposedFilename File names, comma separated, to use as  base file names.
    226      * @throws DefinitionsFactoryException An error occur during initialization.
    227      */
    228     protected void initFactory(
    229         ServletContext servletContext,
    230         String proposedFilename)
    231         throws DefinitionsFactoryException, FileNotFoundException {
    232
    233         // Init list of filenames
    234         StringTokenizer tokenizer = new StringTokenizer(proposedFilename, ",");
    235         this.filenames = new ArrayList(tokenizer.countTokens());
    236         while (tokenizer.hasMoreTokens()) {
    237             this.filenames.add(tokenizer.nextToken().trim());
    238         }
    239
    240         loaded = new HashMap();
    241         defaultFactory = createDefaultFactory(servletContext);
    242         if (log.isDebugEnabled())
    243             log.debug("default factory:" + defaultFactory);
    244     }
    245
    246     /**
    247      * Get default factory.
    248      * @return Default factory
    249      */
    250     protected DefinitionsFactory getDefaultFactory() {
    251         return defaultFactory;
    252     }
    253
    254     /**
    255      * Create default factory .
    256      * Create InstancesMapper for specified Locale.
    257      * If creation failes, use default mapper and log error message.
    258      * @param servletContext Current servlet context. Used to open file.
    259      * @return Created default definition factory.
    260      * @throws DefinitionsFactoryException If an error occur while creating factory.
    261      * @throws FileNotFoundException if factory can't be loaded from filenames.
    262      */
    263     protected DefinitionsFactory createDefaultFactory(ServletContext servletContext)
    264         throws DefinitionsFactoryException, FileNotFoundException {
    265
    266         XmlDefinitionsSet rootXmlConfig = parseXmlFiles(servletContext, "", null);
    267         if (rootXmlConfig == null) {
    268             throw new FileNotFoundException();
    269         }
    270
    271         rootXmlConfig.resolveInheritances();
    272
    273         if (log.isDebugEnabled()) {
    274             log.debug(rootXmlConfig);
    275         }
    276
    277         DefinitionsFactory factory = new DefinitionsFactory(rootXmlConfig);
    278         if (log.isDebugEnabled()) {
    279             log.debug("factory loaded : " + factory);
    280         }
    281
    282         return factory;
    283     }
    284
    285     /**
    286      * Extract key that will be used to get the sub factory.
    287      * @param name Name of requested definition
    288      * @param request Current servlet request.
    289      * @param servletContext Current servlet context.
    290      * @return the key or <code>null</code> if not found.
    291      */
    292     protected Object getDefinitionsFactoryKey(
    293         String name,
    294         ServletRequest request,
    295         ServletContext servletContext) {
    296
    297         Locale locale = null;
    298         try {
    299             HttpSession session = ((HttpServletRequest) request).getSession(false);
    300             if (session != null) {
    301                 locale = (Locale) session.getAttribute(ComponentConstants.LOCALE_KEY);
    302             }
    303
    304         } catch (ClassCastException ex) {
    305             log.error("I18nFactorySet.getDefinitionsFactoryKey");
    306             ex.printStackTrace();
    307         }
    308
    309         return locale;
    310     }
    311
    312     /**
    313      * Create a factory for specified key.
    314     * If creation failes, return default factory and log an error message.
    315     * @param key The key.
    316     * @param request Servlet request.
    317     * @param servletContext Servlet context.
    318     * @return Definition factory for specified key.
    319     * @throws DefinitionsFactoryException If an error occur while creating factory.
    320      */
    321     protected DefinitionsFactory createFactory(
    322         Object key,
    323         ServletRequest request,
    324         ServletContext servletContext)
    325         throws DefinitionsFactoryException {
    326
    327         if (key == null) {
    328             return getDefaultFactory();
    329         }
    330
    331         // Build possible postfixes
    332         List possiblePostfixes = calculateSuffixes((Locale) key);
    333
    334         // Search last postix corresponding to a config file to load.
    335         // First check if something is loaded for this postfix.
    336         // If not, try to load its config.
    337         XmlDefinitionsSet lastXmlFile = null;
    338         DefinitionsFactory factory = null;
    339         String curPostfix = null;
    340         int i = 0;
    341
    342         for (i = possiblePostfixes.size() - 1; i >= 0; i--) {
    343             curPostfix = (String) possiblePostfixes.get(i);
    344
    345             // Already loaded ?
    346             factory = (DefinitionsFactory) loaded.get(curPostfix);
    347             if (factory != null) { // yes, stop search
    348                 return factory;
    349             }
    350
    351             // Try to load it. If success, stop search
    352             lastXmlFile = parseXmlFiles(servletContext, curPostfix, null);
    353             if (lastXmlFile != null) {
    354                 break;
    355             }
    356         }
    357
    358         // Have we found a description file ?
    359         // If no, return default one
    360         if (lastXmlFile == null) {
    361             return getDefaultFactory();
    362         }
    363
    364         // We found something. Need to load base and intermediate files
    365         String lastPostfix = curPostfix;
    366         XmlDefinitionsSet rootXmlConfig = parseXmlFiles(servletContext, "", null);
    367         for (int j = 0; j < i; j++) {
    368             curPostfix = (String) possiblePostfixes.get(j);
    369             parseXmlFiles(servletContext, curPostfix, rootXmlConfig);
    370         }
    371
    372         rootXmlConfig.extend(lastXmlFile);
    373         rootXmlConfig.resolveInheritances();
    374
    375         factory = new DefinitionsFactory(rootXmlConfig);
    376         loaded.put(lastPostfix, factory);
    377
    378         if (log.isDebugEnabled()) {
    379             log.debug("factory loaded : " + factory);
    380         }
    381
    382         // return last available found !
    383         return factory;
    384     }
    385
    386     /**
    387      * Calculate the suffixes based on the locale.
    388      * @param locale the locale
    389      */
    390     private List calculateSuffixes(Locale locale) {
    391
    392         List suffixes = new ArrayList(3);
    393         String language = locale.getLanguage();
    394         String country  = locale.getCountry();
    395         String variant  = locale.getVariant();
    396
    397         StringBuffer suffix = new StringBuffer();
    398         suffix.append('_');
    399         suffix.append(language);
    400         if (language.length() > 0) {
    401             suffixes.add(suffix.toString());
    402         }
    403
    404         suffix.append('_');
    405         suffix.append(country);
    406         if (country.length() > 0) {
    407             suffixes.add(suffix.toString());
    408         }
    409
    410         suffix.append('_');
    411         suffix.append(variant);
    412         if (variant.length() > 0) {
    413             suffixes.add(suffix.toString());
    414         }
    415
    416         return suffixes;
    417
    418     }
    419
    420     /**
    421      * Parse files associated to postix if they exist.
    422      * For each name in filenames, append postfix before file extension,
    423      * then try to load the corresponding file.
    424      * If file doesn't exist, try next one. Each file description is added to
    425      * the XmlDefinitionsSet description.
    426      * The XmlDefinitionsSet description is created only if there is a definition file.
    427      * Inheritance is not resolved in the returned XmlDefinitionsSet.
    428      * If no description file can be opened and no definiion set is provided, return <code>null</code>.
    429      * @param postfix Postfix to add to each description file.
    430      * @param xmlDefinitions Definitions set to which definitions will be added. If <code>null</code>, a definitions
    431      * set is created on request.
    432      * @return XmlDefinitionsSet The definitions set created or passed as parameter.
    433      * @throws DefinitionsFactoryException On errors parsing file.
    434      */
    435     protected XmlDefinitionsSet parseXmlFiles(
    436         ServletContext servletContext,
    437         String postfix,
    438         XmlDefinitionsSet xmlDefinitions)
    439         throws DefinitionsFactoryException {
    440
    441         if (postfix != null && postfix.length() == 0) {
    442             postfix = null;
    443         }
    444
    445         // Iterate throw each file name in list
    446         Iterator i = filenames.iterator();
    447         while (i.hasNext()) {
    448             String filename = concatPostfix((String) i.next(), postfix);
    449             xmlDefinitions = parseXmlFile(servletContext, filename, xmlDefinitions);
    450         }
    451
    452         return xmlDefinitions;
    453     }
    454
    455     /**
    456      * Parse specified xml file and add definition to specified definitions set.
    457      * This method is used to load several description files in one instances list.
    458      * If filename exists and definition set is <code>null</code>, create a new set. Otherwise, return
    459      * passed definition set (can be <code>null</code>).
    460      * @param servletContext Current servlet context. Used to open file.
    461      * @param filename Name of file to parse.
    462      * @param xmlDefinitions Definitions set to which definitions will be added. If null, a definitions
    463      * set is created on request.
    464      * @return XmlDefinitionsSet The definitions set created or passed as parameter.
    465      * @throws DefinitionsFactoryException On errors parsing file.
    466      */
    467     protected XmlDefinitionsSet parseXmlFile(
    468         ServletContext servletContext,
    469         String filename,
    470         XmlDefinitionsSet xmlDefinitions)
    471         throws DefinitionsFactoryException {
    472
    473         try {
    474             InputStream input = servletContext.getResourceAsStream(filename);
    475             // Try to load using real path.
    476             // This allow to load config file under websphere 3.5.x
    477             // Patch proposed Houston, Stephen (LIT) on 5 Apr 2002
    478             if (null == input) {
    479                 try {
    480                     input =
    481                         new java.io.FileInputStream(
    482                             servletContext.getRealPath(filename));
    483                 } catch (Exception e) {
    484                 }
    485             }
    486
    487             // If the config isn't in the servlet context, try the class loader
    488             // which allows the config files to be stored in a jar
    489             if (input == null) {
    490                 input = getClass().getResourceAsStream(filename);
    491             }
    492
    493             // If still nothing found, this mean no config file is associated
    494             if (input == null) {
    495                 if (log.isDebugEnabled()) {
    496                     log.debug("Can't open file '" + filename + "'");
    497                 }
    498                 return xmlDefinitions;
    499             }
    500
    501             // Check if parser already exist.
    502             // Doesn't seem to work yet.
    503             //if( xmlParser == null )
    504             if (true) {
    505                 xmlParser = new XmlParser();
    506                 xmlParser.setValidating(isValidatingParser);
    507             }
    508
    509             // Check if definition set already exist.
    510             if (xmlDefinitions == null) {
    511                 xmlDefinitions = new XmlDefinitionsSet();
    512             }
    513
    514             xmlParser.parse(input, xmlDefinitions);
    515
    516         } catch (SAXException ex) {
    517             if (log.isDebugEnabled()) {
    518                 log.debug("Error while parsing file '" + filename + "'.");
    519                 ex.printStackTrace();
    520             }
    521             throw new DefinitionsFactoryException(
    522                 "Error while parsing file '" + filename + "'. " + ex.getMessage(),
    523                 ex);
    524
    525         } catch (IOException ex) {
    526             throw new DefinitionsFactoryException(
    527                 "IO Error while parsing file '" + filename + "'. " + ex.getMessage(),
    528                 ex);
    529         }
    530
    531         return xmlDefinitions;
    532     }
    533
    534     /**
    535      * Concat postfix to the name. Take care of existing filename extension.
    536      * Transform the given name "name.ext" to have "name" + "postfix" + "ext".
    537      * If there is no ext, return "name" + "postfix".
    538      * @param name Filename.
    539      * @param postfix Postfix to add.
    540      * @return Concatenated filename.
    541      */
    542     private String concatPostfix(String name, String postfix) {
    543         if (postfix == null) {
    544             return name;
    545         }
    546
    547         // Search file name extension.
    548         // take care of Unix files starting with .
    549         int dotIndex = name.lastIndexOf(".");
    550         int lastNameStart = name.lastIndexOf(java.io.File.pathSeparator);
    551         if (dotIndex < 1 || dotIndex < lastNameStart) {
    552             return name + postfix;
    553         }
    554
    555         String ext = name.substring(dotIndex);
    556         name = name.substring(0, dotIndex);
    557         return name + postfix + ext;
    558     }
    559
    560     /**
    561      * Return String representation.
    562      * @return String representation.
    563      */
    564     public String toString() {
    565         StringBuffer buff = new StringBuffer("I18nFactorySet : \n");
    566         buff.append("--- default factory ---\n");
    567         buff.append(defaultFactory.toString());
    568         buff.append("\n--- other factories ---\n");
    569         Iterator i = factories.values().iterator();
    570         while (i.hasNext()) {
    571             buff.append(i.next().toString()).append("---------- \n");
    572         }
    573         return buff.toString();
    574     }
    575
    576 }