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.net.URISyntaxException;
44  import java.net.URL;
45  import java.util.Properties;
46  import java.util.Stack;
47  
48  public class TobagoConfigParser extends TobagoConfigEntityResolver {
49  
50    private static final Logger LOG = LoggerFactory.getLogger(TobagoConfigParser.class);
51  
52    private static final int TOBAGO_CONFIG = -1498874611;
53    private static final int NAME = 3373707;
54    private static final int ORDERING = 1234314708;
55    private static final int BEFORE = -1392885889;
56    private static final int AFTER = 92734940;
57    private static final int THEME_CONFIG = 1930630086;
58    private static final int DEFAULT_THEME = -114431171;
59    private static final int SUPPORTED_THEME = -822303766;
60    private static final int CREATE_SESSION_SECRET = 413906616;
61    private static final int CHECK_SESSION_SECRET = 275994924;
62    private static final int PREVENT_FRAME_ATTACKS = 270456726;
63    private static final int SET_NOSNIFF_HEADER = -1238451304;
64    private static final int CONTENT_SECURITY_POLICY = 1207440139;
65    private static final int SECURITY_ANNOTATION = 1744426972;
66    private static final int DIRECTIVE = -962590641;
67    /**
68     * @deprecated since 4.0.0
69     */
70    @Deprecated
71    private static final int RENDERERS = 1839650832;
72    /**
73     * @deprecated since 4.0.0
74     */
75    @Deprecated
76    private static final int RENDERER = -494845757;
77    /**
78     * @deprecated since 4.0.0
79     */
80    @Deprecated
81    private static final int SUPPORTED_MARKUP = 71904295;
82    /**
83     * @deprecated since 4.0.0
84     */
85    @Deprecated
86    private static final int MARKUP = -1081305560;
87    private static final int THEME_DEFINITIONS = -255617156;
88    private static final int THEME_DEFINITION = 1515774935;
89    private static final int DISPLAY_NAME = 1568910518;
90    private static final int FALLBACK = 761243362;
91    private static final int VERSIONED = -1407102089;
92    private static final int RESOURCES = -1983070683;
93    private static final int EXCLUDES = 1994055129;
94    private static final int SANITIZER = 1807639849;
95    private static final int SANITIZER_CLASS = -974266412;
96    private static final int DECODE_LINE_FEED = -1764519240;
97    private static final int SCRIPT = -907685685;
98    private static final int STYLE = 109780401;
99    private static final int PROPERTIES = -926053069;
100   private static final int ENTRY = 96667762;
101   private static final int MIME_TYPES = 1081186720;
102   private static final int MIME_TYPE = -242217677;
103   private static final int EXTENSION = -612557761;
104   private static final int TYPE = 3575610;
105 
106   private TobagoConfigFragment tobagoConfig;
107   private ThemeImpl currentTheme;
108   private Boolean production;
109   private boolean exclude;
110   private StringBuilder buffer;
111   private Properties properties;
112   private String entryKey;
113   private String directiveName;
114   private String extension;
115   private String type;
116 
117   private Stack<String> stack;
118 
119   public TobagoConfigParser() {
120   }
121 
122   public TobagoConfigFragment parse(final URL url)
123       throws IOException, SAXException, ParserConfigurationException, URISyntaxException {
124 
125     if (LOG.isInfoEnabled()) {
126       LOG.info("Parsing configuration file: '{}'", url);
127     }
128 
129     final TobagoConfigVersion version = new TobagoConfigVersion(url);
130 
131     // todo: Is there a solution that validate with both, DTD and XSD?
132 
133     if (version.isSchema()) {
134       validate(url, version);
135     }
136 
137     try (final InputStream inputStream = url.openStream()) {
138       final SAXParserFactory factory = SAXParserFactory.newInstance();
139       if (!version.isSchema()) {
140         factory.setValidating(true);
141       }
142       final SAXParser saxParser = factory.newSAXParser();
143       saxParser.parse(inputStream, this);
144     }
145     return tobagoConfig;
146   }
147 
148   @Override
149   public void ignorableWhitespace(final char[] ch, final int start, final int length) throws SAXException {
150     super.ignorableWhitespace(ch, start, length);
151   }
152 
153   @Override
154   public void startDocument() throws SAXException {
155 
156     buffer = new StringBuilder();
157     stack = new Stack<>();
158   }
159 
160   @Override
161   public void endDocument() throws SAXException {
162     assert stack.empty();
163     stack = null;
164   }
165 
166   @Override
167   public void startElement(final String uri, final String localName, final String qName, final Attributes attributes)
168       throws SAXException {
169 
170     // No unused content should be collected, specially text mixed with tags.
171     assert buffer.toString().trim().length() == 0;
172 
173     buffer.setLength(0);
174     stack.add(qName);
175 
176     switch (qName.hashCode()) {
177 
178       case TOBAGO_CONFIG:
179         tobagoConfig = new TobagoConfigFragment();
180         break;
181 
182       case CONTENT_SECURITY_POLICY:
183         final String mode = attributes.getValue("mode");
184         tobagoConfig.setContentSecurityPolicy(new ContentSecurityPolicy(mode));
185         break;
186 
187       case THEME_DEFINITION:
188         currentTheme = new ThemeImpl();
189         tobagoConfig.addThemeDefinition(currentTheme);
190         break;
191 
192       case RESOURCES:
193         production = Boolean.parseBoolean(attributes.getValue("production"));
194         break;
195 
196       case EXCLUDES:
197         exclude = true;
198         break;
199 
200       case SCRIPT:
201         final ThemeScript script = new ThemeScript();
202         script.setName(attributes.getValue("name"));
203         if (production) {
204           currentTheme.getProductionResources().addScript(script, exclude);
205         } else {
206           currentTheme.getResources().addScript(script, exclude);
207         }
208         break;
209 
210       case STYLE:
211         final ThemeStyle style = new ThemeStyle();
212         style.setName(attributes.getValue("name"));
213         if (production) {
214           currentTheme.getProductionResources().addStyle(style, exclude);
215         } else {
216           currentTheme.getResources().addStyle(style, exclude);
217         }
218         break;
219 
220       case PROPERTIES:
221         properties = new Properties();
222         break;
223 
224       case ENTRY:
225         entryKey = attributes.getValue("key");
226         break;
227 
228       case DIRECTIVE:
229         directiveName = attributes.getValue("name");
230         break;
231 
232       case NAME:
233       case ORDERING:
234       case BEFORE:
235       case AFTER:
236       case THEME_CONFIG:
237       case DEFAULT_THEME:
238       case SUPPORTED_THEME:
239       case SUPPORTED_MARKUP:
240       case MARKUP:
241       case CREATE_SESSION_SECRET:
242       case CHECK_SESSION_SECRET:
243       case SECURITY_ANNOTATION:
244       case PREVENT_FRAME_ATTACKS:
245       case SET_NOSNIFF_HEADER:
246       case THEME_DEFINITIONS:
247       case DISPLAY_NAME:
248       case VERSIONED:
249       case FALLBACK:
250       case SANITIZER:
251       case SANITIZER_CLASS:
252       case MIME_TYPES:
253       case MIME_TYPE:
254       case EXTENSION:
255       case TYPE:
256       case RENDERERS:
257       case RENDERER:
258         // nothing to do
259         break;
260 
261       default:
262         LOG.warn("Ignoring unknown start tag <" + qName + "> with hashCode=" + qName.hashCode());
263     }
264   }
265 
266   @Override
267   public void characters(final char[] ch, final int start, final int length) throws SAXException {
268     buffer.append(ch, start, length);
269   }
270 
271   @Override
272   public void endElement(final String uri, final String localName, final String qName) throws SAXException {
273     assert qName.equals(stack.peek());
274 
275     final String text = buffer.toString().trim();
276     buffer.setLength(0);
277 
278     switch (qName.hashCode()) {
279 
280       case NAME:
281         final String parent = stack.get(stack.size() - 2);
282         switch (parent.hashCode()) {
283 
284           case TOBAGO_CONFIG:
285             tobagoConfig.setName(text);
286             break;
287 
288           case BEFORE:
289             tobagoConfig.addBefore(text);
290             break;
291 
292           case AFTER:
293             tobagoConfig.addAfter(text);
294             break;
295 
296           case THEME_DEFINITION:
297             currentTheme.setName(text);
298             break;
299 
300           case RENDERER:
301             // nothing to do
302             break;
303 
304           default:
305             LOG.warn("Ignoring unknown parent <" + parent + "> of tag <name>");
306         }
307         break;
308 
309       case DEFAULT_THEME:
310         tobagoConfig.setDefaultThemeName(text);
311         break;
312 
313       case SUPPORTED_THEME:
314         tobagoConfig.addSupportedThemeName(text);
315         break;
316 
317       case CREATE_SESSION_SECRET:
318         tobagoConfig.setCreateSessionSecret(text);
319         break;
320 
321       case CHECK_SESSION_SECRET:
322         tobagoConfig.setCheckSessionSecret(text);
323         break;
324 
325       case PREVENT_FRAME_ATTACKS:
326         tobagoConfig.setPreventFrameAttacks(Boolean.parseBoolean(text));
327         break;
328 
329       case SET_NOSNIFF_HEADER:
330         tobagoConfig.setSetNosniffHeader(Boolean.parseBoolean(text));
331         break;
332 
333       case SECURITY_ANNOTATION:
334         tobagoConfig.setSecurityAnnotation(SecurityAnnotation.valueOf(text));
335         break;
336 
337       case DIRECTIVE:
338         if (directiveName == null) { // before Tobago 4.0
339           final int i = text.indexOf(' ');
340           if (i < 1) {
341             throw new TobagoConfigurationException("CSP directive can't be parsed!");
342           }
343           tobagoConfig.getContentSecurityPolicy().addDirective(text.substring(0, i), text.substring(i + 1));
344         } else {
345           tobagoConfig.getContentSecurityPolicy().addDirective(directiveName, text);
346         }
347         directiveName = null;
348         break;
349 
350       case DISPLAY_NAME:
351         currentTheme.setDisplayName(text);
352         break;
353 
354       case FALLBACK:
355         currentTheme.setFallbackName(text);
356         break;
357 
358       case THEME_DEFINITION:
359         currentTheme = null;
360         break;
361 
362       case VERSIONED:
363         currentTheme.setVersioned(Boolean.parseBoolean(text));
364         break;
365 
366       case RESOURCES:
367         production = null;
368         break;
369 
370       case EXCLUDES:
371         exclude = false;
372         break;
373 
374       case SANITIZER_CLASS:
375         tobagoConfig.setSanitizerClass(text);
376         break;
377 
378       case SANITIZER:
379         if (properties != null) {
380           tobagoConfig.setSanitizerProperties(properties);
381         }
382         properties = null;
383         break;
384 
385       case DECODE_LINE_FEED:
386         tobagoConfig.setDecodeLineFeed(Boolean.parseBoolean(text));
387         break;
388 
389       case ENTRY:
390         properties.setProperty(entryKey, text);
391         entryKey = null;
392         break;
393 
394       case EXTENSION:
395         extension = text;
396         break;
397 
398       case TYPE:
399         type = text;
400         break;
401 
402       case MIME_TYPE:
403         tobagoConfig.addMimeType(extension, type);
404         break;
405 
406       case TOBAGO_CONFIG:
407       case THEME_CONFIG:
408       case ORDERING:
409       case BEFORE:
410       case AFTER:
411       case SUPPORTED_MARKUP:
412       case CONTENT_SECURITY_POLICY:
413       case THEME_DEFINITIONS:
414       case RENDERERS:
415       case RENDERER:
416       case SCRIPT:
417       case STYLE:
418       case PROPERTIES:
419       case MIME_TYPES:
420       case MARKUP:
421         // nothing to do
422         break;
423 
424       default:
425         LOG.warn("Ignoring unknown end tag <" + qName + ">");
426     }
427 
428     stack.pop();
429   }
430 
431   @Override
432   public void warning(final SAXParseException e) throws SAXException {
433     throw e;
434   }
435 
436   @Override
437   public void error(final SAXParseException e) throws SAXException {
438     throw e;
439   }
440 
441   @Override
442   public void fatalError(final SAXParseException e) throws SAXException {
443     throw e;
444   }
445 
446   private void validate(final URL url, final TobagoConfigVersion version)
447       throws URISyntaxException, SAXException, IOException {
448 
449     final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
450     final Schema schema;
451     if ("4.0".equals(version.getVersion())) {
452       schema = schemaFactory.newSchema(getClass().getResource(TOBAGO_CONFIG_XSD_4_0));
453     } else if ("3.0".equals(version.getVersion())) {
454       schema = schemaFactory.newSchema(getClass().getResource(TOBAGO_CONFIG_XSD_3_0));
455     } else if ("2.0.6".equals(version.getVersion())) {
456       schema = schemaFactory.newSchema(getClass().getResource(TOBAGO_CONFIG_XSD_2_0_6));
457     } else if ("2.0".equals(version.getVersion())) {
458       schema = schemaFactory.newSchema(getClass().getResource(TOBAGO_CONFIG_XSD_2_0));
459     } else if ("1.6".equals(version.getVersion())) {
460       LOG.warn("Using deprecated schema with version attribute 1.6 in file: '" + url + "'");
461       schema = schemaFactory.newSchema(getClass().getResource(TOBAGO_CONFIG_XSD_1_6));
462     } else if ("1.5".equals(version.getVersion())) {
463       schema = schemaFactory.newSchema(getClass().getResource(TOBAGO_CONFIG_XSD_1_5));
464     } else {
465       throw new SAXException("Using unknown version attribute '" + version.getVersion() + "' in file: '" + url + "'");
466     }
467     final Validator validator = schema.newValidator();
468     final Source source = new StreamSource(url.openStream());
469 
470     validator.validate(source);
471   }
472 
473 }