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 }