View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  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,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.tobago.internal.config;
21  
22  import org.apache.myfaces.tobago.context.ThemeImpl;
23  import org.apache.myfaces.tobago.context.ThemeScript;
24  import org.apache.myfaces.tobago.context.ThemeStyle;
25  import org.apache.myfaces.tobago.exception.TobagoConfigurationException;
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  import org.xml.sax.Attributes;
29  import org.xml.sax.SAXException;
30  import org.xml.sax.SAXParseException;
31  
32  import javax.xml.XMLConstants;
33  import javax.xml.parsers.ParserConfigurationException;
34  import javax.xml.parsers.SAXParser;
35  import javax.xml.parsers.SAXParserFactory;
36  import javax.xml.transform.Source;
37  import javax.xml.transform.stream.StreamSource;
38  import javax.xml.validation.Schema;
39  import javax.xml.validation.SchemaFactory;
40  import javax.xml.validation.Validator;
41  import java.io.IOException;
42  import java.io.InputStream;
43  import java.lang.invoke.MethodHandles;
44  import java.net.URISyntaxException;
45  import java.net.URL;
46  import java.util.Properties;
47  import java.util.Stack;
48  
49  public class TobagoConfigParser extends TobagoConfigEntityResolver {
50  
51    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
52  
53    private static final int TOBAGO_CONFIG = -1498874611;
54    private static final int NAME = 3373707;
55    private static final int ORDERING = 1234314708;
56    private static final int BEFORE = -1392885889;
57    private static final int AFTER = 92734940;
58    private static final int THEME_CONFIG = 1930630086;
59    private static final int DEFAULT_THEME = -114431171;
60    private static final int SUPPORTED_THEME = -822303766;
61    private static final int CREATE_SESSION_SECRET = 413906616;
62    private static final int CHECK_SESSION_SECRET = 275994924;
63    private static final int PREVENT_FRAME_ATTACKS = 270456726;
64    private static final int SET_NOSNIFF_HEADER = -1238451304;
65    private static final int CONTENT_SECURITY_POLICY = 1207440139;
66    private static final int SECURITY_ANNOTATION = 1744426972;
67    private static final int DIRECTIVE = -962590641;
68    /**
69     * @deprecated since 4.0.0
70     */
71    @Deprecated
72    private static final int RENDERERS = 1839650832;
73    /**
74     * @deprecated since 4.0.0
75     */
76    @Deprecated
77    private static final int RENDERER = -494845757;
78    /**
79     * @deprecated since 4.0.0
80     */
81    @Deprecated
82    private static final int SUPPORTED_MARKUP = 71904295;
83    /**
84     * @deprecated since 4.0.0
85     */
86    @Deprecated
87    private static final int MARKUP = -1081305560;
88    private static final int THEME_DEFINITIONS = -255617156;
89    private static final int THEME_DEFINITION = 1515774935;
90    private static final int DISPLAY_NAME = 1568910518;
91    private static final int FALLBACK = 761243362;
92    /**
93     * @deprecated since 5.0.0
94     */
95    @Deprecated
96    private static final int VERSIONED = -1407102089;
97    private static final int VERSION = 351608024;
98    private static final int RESOURCES = -1983070683;
99    private static final int INCLUDES = 90259659;
100   private static final int EXCLUDES = 1994055129;
101   private static final int SANITIZER = 1807639849;
102   private static final int SANITIZER_CLASS = -974266412;
103   private static final int DECODE_LINE_FEED = -1764519240;
104   private static final int SCRIPT = -907685685;
105   private static final int STYLE = 109780401;
106   private static final int PROPERTIES = -926053069;
107   private static final int ENTRY = 96667762;
108   private static final int MIME_TYPES = 1081186720;
109   private static final int MIME_TYPE = -242217677;
110   private static final int EXTENSION = -612557761;
111   private static final int TYPE = 3575610;
112 
113   private static final String ATTR_MODE = "mode";
114   private static final String ATTR_PRODUCTION = "production";
115   private static final String ATTR_NAME = "name";
116   private static final String ATTR_KEY = "key";
117   private static final String ATTR_PRIORITY = "priority";
118   private static final String ATTR_TYPE = "type";
119 
120   private static final int MAX_PRIORITY = 65536;
121 
122   private TobagoConfigFragment tobagoConfig;
123   private ThemeImpl currentTheme;
124   private Boolean production;
125   private boolean exclude;
126   private StringBuilder buffer;
127   private Properties properties;
128   private String entryKey;
129   private String directiveName;
130   private String extension;
131   private String type;
132 
133   private Stack<String> stack;
134 
135   public TobagoConfigParser() {
136   }
137 
138   public TobagoConfigFragment parse(final URL url)
139       throws IOException, SAXException, ParserConfigurationException, URISyntaxException {
140 
141     if (LOG.isInfoEnabled()) {
142       LOG.info("Parsing configuration file: '{}'", url);
143     }
144 
145     final TobagoConfigVersion version = new TobagoConfigVersion(url);
146 
147     // todo: Is there a solution that validate with both, DTD and XSD?
148 
149     if (version.isSchema()) {
150       validate(url, version);
151     }
152 
153     try (final InputStream inputStream = url.openStream()) {
154       final SAXParserFactory factory = SAXParserFactory.newInstance();
155       if (!version.isSchema()) {
156         factory.setValidating(true);
157       }
158       final SAXParser saxParser = factory.newSAXParser();
159       saxParser.parse(inputStream, this);
160     }
161     return tobagoConfig;
162   }
163 
164   @Override
165   public void ignorableWhitespace(final char[] ch, final int start, final int length) throws SAXException {
166     super.ignorableWhitespace(ch, start, length);
167   }
168 
169   @Override
170   public void startDocument() throws SAXException {
171 
172     buffer = new StringBuilder();
173     stack = new Stack<>();
174   }
175 
176   @Override
177   public void endDocument() throws SAXException {
178     assert stack.empty();
179     stack = null;
180   }
181 
182   @Override
183   public void startElement(final String uri, final String localName, final String qName, final Attributes attributes)
184       throws SAXException {
185 
186     // No unused content should be collected, specially text mixed with tags.
187     assert buffer.toString().trim().length() == 0;
188 
189     buffer.setLength(0);
190     stack.add(qName);
191 
192     switch (qName.hashCode()) {
193 
194       case TOBAGO_CONFIG:
195         tobagoConfig = new TobagoConfigFragment();
196         break;
197 
198       case CONTENT_SECURITY_POLICY:
199         final String mode = attributes.getValue(ATTR_MODE);
200         tobagoConfig.setContentSecurityPolicy(new ContentSecurityPolicy(mode));
201         break;
202 
203       case THEME_DEFINITION:
204         currentTheme = new ThemeImpl();
205         tobagoConfig.addThemeDefinition(currentTheme);
206         break;
207 
208       case RESOURCES:
209         production = Boolean.parseBoolean(attributes.getValue(ATTR_PRODUCTION));
210         break;
211 
212       case EXCLUDES:
213         exclude = true;
214         break;
215 
216       case SCRIPT:
217         final ThemeScript script = new ThemeScript();
218         script.setName(attributes.getValue(ATTR_NAME));
219         script.setType(attributes.getValue(ATTR_TYPE));
220         final String scriptPriority = attributes.getValue(ATTR_PRIORITY);
221         script.setPriority(scriptPriority != null ? Integer.parseUnsignedInt(scriptPriority) : MAX_PRIORITY);
222         if (production) {
223           if (exclude) {
224             currentTheme.getProductionResources().addExcludeScript(script);
225           } else {
226             currentTheme.getProductionResources().addIncludeScript(script);
227           }
228         } else {
229           if (exclude) {
230             currentTheme.getDevelopmentResources().addExcludeScript(script);
231           } else {
232             currentTheme.getDevelopmentResources().addIncludeScript(script);
233           }
234         }
235         break;
236 
237       case STYLE:
238         final ThemeStyle style = new ThemeStyle();
239         style.setName(attributes.getValue(ATTR_NAME));
240         final String stylePriority = attributes.getValue(ATTR_PRIORITY);
241         style.setPriority(stylePriority != null ? Integer.parseUnsignedInt(stylePriority) : MAX_PRIORITY);
242         if (production) {
243           if (exclude) {
244             currentTheme.getProductionResources().addExcludeStyle(style);
245           } else {
246             currentTheme.getProductionResources().addIncludeStyle(style);
247           }
248         } else {
249           if (exclude) {
250             currentTheme.getDevelopmentResources().addExcludeStyle(style);
251           } else {
252             currentTheme.getDevelopmentResources().addIncludeStyle(style);
253           }
254         }
255         break;
256 
257       case PROPERTIES:
258         properties = new Properties();
259         break;
260 
261       case ENTRY:
262         entryKey = attributes.getValue(ATTR_KEY);
263         break;
264 
265       case DIRECTIVE:
266         directiveName = attributes.getValue(ATTR_NAME);
267         break;
268 
269       case NAME:
270       case ORDERING:
271       case BEFORE:
272       case AFTER:
273       case THEME_CONFIG:
274       case DEFAULT_THEME:
275       case SUPPORTED_THEME:
276       case SUPPORTED_MARKUP:
277       case MARKUP:
278       case CREATE_SESSION_SECRET:
279       case CHECK_SESSION_SECRET:
280       case SECURITY_ANNOTATION:
281       case PREVENT_FRAME_ATTACKS:
282       case SET_NOSNIFF_HEADER:
283       case THEME_DEFINITIONS:
284       case DISPLAY_NAME:
285       case VERSION:
286       case VERSIONED:
287       case FALLBACK:
288       case SANITIZER:
289       case SANITIZER_CLASS:
290       case MIME_TYPES:
291       case MIME_TYPE:
292       case EXTENSION:
293       case TYPE:
294       case RENDERERS:
295       case RENDERER:
296       case INCLUDES:
297         // nothing to do
298         break;
299 
300       default:
301         LOG.warn("Ignoring unknown start tag <" + qName + "> with hashCode=" + qName.hashCode());
302     }
303   }
304 
305   @Override
306   public void characters(final char[] ch, final int start, final int length) throws SAXException {
307     buffer.append(ch, start, length);
308   }
309 
310   @Override
311   public void endElement(final String uri, final String localName, final String qName) throws SAXException {
312     assert qName.equals(stack.peek());
313 
314     final String text = buffer.toString().trim();
315     buffer.setLength(0);
316 
317     switch (qName.hashCode()) {
318 
319       case NAME:
320         final String parent = stack.get(stack.size() - 2);
321         switch (parent.hashCode()) {
322 
323           case TOBAGO_CONFIG:
324             tobagoConfig.setName(text);
325             break;
326 
327           case BEFORE:
328             tobagoConfig.addBefore(text);
329             break;
330 
331           case AFTER:
332             tobagoConfig.addAfter(text);
333             break;
334 
335           case THEME_DEFINITION:
336             currentTheme.setName(text);
337             break;
338 
339           case RENDERER:
340             // nothing to do
341             break;
342 
343           default:
344             LOG.warn("Ignoring unknown parent <" + parent + "> of tag <name>");
345         }
346         break;
347 
348       case DEFAULT_THEME:
349         tobagoConfig.setDefaultThemeName(text);
350         break;
351 
352       case SUPPORTED_THEME:
353         tobagoConfig.addSupportedThemeName(text);
354         break;
355 
356       case CREATE_SESSION_SECRET:
357         tobagoConfig.setCreateSessionSecret(text);
358         break;
359 
360       case CHECK_SESSION_SECRET:
361         tobagoConfig.setCheckSessionSecret(text);
362         break;
363 
364       case PREVENT_FRAME_ATTACKS:
365         tobagoConfig.setPreventFrameAttacks(Boolean.parseBoolean(text));
366         break;
367 
368       case SET_NOSNIFF_HEADER:
369         tobagoConfig.setSetNosniffHeader(Boolean.parseBoolean(text));
370         break;
371 
372       case SECURITY_ANNOTATION:
373         tobagoConfig.setSecurityAnnotation(SecurityAnnotation.valueOf(text));
374         break;
375 
376       case DIRECTIVE:
377         if (directiveName == null) { // before Tobago 4.0
378           final int i = text.indexOf(' ');
379           if (i < 1) {
380             throw new TobagoConfigurationException("CSP directive can't be parsed!");
381           }
382           tobagoConfig.getContentSecurityPolicy().addDirective(text.substring(0, i), text.substring(i + 1));
383         } else {
384           tobagoConfig.getContentSecurityPolicy().addDirective(directiveName, text);
385         }
386         directiveName = null;
387         break;
388 
389       case DISPLAY_NAME:
390         currentTheme.setDisplayName(text);
391         break;
392 
393       case FALLBACK:
394         currentTheme.setFallbackName(text);
395         break;
396 
397       case THEME_DEFINITION:
398         currentTheme = null;
399         break;
400 
401       case VERSION:
402         currentTheme.setVersion(text);
403         break;
404 
405       case RESOURCES:
406         production = null;
407         break;
408 
409       case EXCLUDES:
410         exclude = false;
411         break;
412 
413       case SANITIZER_CLASS:
414         tobagoConfig.setSanitizerClass(text);
415         break;
416 
417       case SANITIZER:
418         if (properties != null) {
419           tobagoConfig.setSanitizerProperties(properties);
420         }
421         properties = null;
422         break;
423 
424       case DECODE_LINE_FEED:
425         tobagoConfig.setDecodeLineFeed(Boolean.parseBoolean(text));
426         break;
427 
428       case ENTRY:
429         properties.setProperty(entryKey, text);
430         entryKey = null;
431         break;
432 
433       case EXTENSION:
434         extension = text;
435         break;
436 
437       case TYPE:
438         type = text;
439         break;
440 
441       case MIME_TYPE:
442         tobagoConfig.addMimeType(extension, type);
443         break;
444 
445       case TOBAGO_CONFIG:
446       case THEME_CONFIG:
447       case ORDERING:
448       case BEFORE:
449       case AFTER:
450       case SUPPORTED_MARKUP:
451       case CONTENT_SECURITY_POLICY:
452       case THEME_DEFINITIONS:
453       case RENDERERS:
454       case RENDERER:
455       case SCRIPT:
456       case STYLE:
457       case PROPERTIES:
458       case MIME_TYPES:
459       case MARKUP:
460       case INCLUDES:
461       case VERSIONED:
462         // nothing to do
463         break;
464 
465       default:
466         LOG.warn("Ignoring unknown end tag <" + qName + ">");
467     }
468 
469     stack.pop();
470   }
471 
472   @Override
473   public void warning(final SAXParseException e) throws SAXException {
474     throw e;
475   }
476 
477   @Override
478   public void error(final SAXParseException e) throws SAXException {
479     throw e;
480   }
481 
482   @Override
483   public void fatalError(final SAXParseException e) throws SAXException {
484     throw e;
485   }
486 
487   private void validate(final URL url, final TobagoConfigVersion version)
488       throws URISyntaxException, SAXException, IOException {
489 
490     final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
491     final Schema schema;
492     if ("5.0".equals(version.getVersion())) {
493       schema = schemaFactory.newSchema(getClass().getResource(TOBAGO_CONFIG_XSD_5_0));
494     } else if ("4.0".equals(version.getVersion())) {
495       schema = schemaFactory.newSchema(getClass().getResource(TOBAGO_CONFIG_XSD_4_0));
496     } else if ("3.0".equals(version.getVersion())) {
497       schema = schemaFactory.newSchema(getClass().getResource(TOBAGO_CONFIG_XSD_3_0));
498     } else if ("2.0.6".equals(version.getVersion())) {
499       schema = schemaFactory.newSchema(getClass().getResource(TOBAGO_CONFIG_XSD_2_0_6));
500     } else if ("2.0".equals(version.getVersion())) {
501       schema = schemaFactory.newSchema(getClass().getResource(TOBAGO_CONFIG_XSD_2_0));
502     } else if ("1.6".equals(version.getVersion())) {
503       LOG.warn("Using deprecated schema with version attribute 1.6 in file: '" + url + "'");
504       schema = schemaFactory.newSchema(getClass().getResource(TOBAGO_CONFIG_XSD_1_6));
505     } else if ("1.5".equals(version.getVersion())) {
506       schema = schemaFactory.newSchema(getClass().getResource(TOBAGO_CONFIG_XSD_1_5));
507     } else {
508       throw new SAXException("Using unknown version attribute '" + version.getVersion() + "' in file: '" + url + "'");
509     }
510     final Validator validator = schema.newValidator();
511     final Source source = new StreamSource(url.openStream());
512 
513     validator.validate(source);
514   }
515 
516 }