View Javadoc

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