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