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  package org.apache.maven.doxia.util;
20  
21  import javax.xml.parsers.ParserConfigurationException;
22  import javax.xml.parsers.SAXParser;
23  import javax.xml.parsers.SAXParserFactory;
24  
25  import java.io.IOException;
26  import java.io.StringReader;
27  import java.util.Locale;
28  import java.util.regex.Matcher;
29  import java.util.regex.Pattern;
30  
31  import org.apache.maven.doxia.markup.XmlMarkup;
32  import org.apache.maven.doxia.parser.ParseException;
33  import org.slf4j.Logger;
34  import org.slf4j.LoggerFactory;
35  import org.xml.sax.EntityResolver;
36  import org.xml.sax.InputSource;
37  import org.xml.sax.SAXException;
38  import org.xml.sax.SAXParseException;
39  import org.xml.sax.XMLReader;
40  import org.xml.sax.helpers.DefaultHandler;
41  
42  /**
43   * A class to validate xml documents.
44   *
45   * @since 1.1.3
46   */
47  public class XmlValidator {
48      private static final Logger LOGGER = LoggerFactory.getLogger(XmlValidator.class);
49  
50      /** lazy xmlReader to validate xml content*/
51      private XMLReader xmlReader;
52  
53      private boolean validate = true;
54      private DefaultHandler defaultHandler;
55      private EntityResolver entityResolver;
56  
57      public boolean isValidate() {
58          return validate;
59      }
60  
61      public void setValidate(boolean validate) {
62          this.validate = validate;
63      }
64  
65      public DefaultHandler getDefaultHandler() {
66          return defaultHandler;
67      }
68  
69      public void setDefaultHandler(DefaultHandler defaultHandler) {
70          this.defaultHandler = defaultHandler;
71      }
72  
73      public EntityResolver getEntityResolver() {
74          return entityResolver;
75      }
76  
77      public void setEntityResolver(EntityResolver entityResolver) {
78          this.entityResolver = entityResolver;
79      }
80  
81      /**
82       * Validate an XML content with SAX.
83       *
84       * @param content a not null xml content
85       * @throws ParseException if any.
86       */
87      public void validate(String content) throws ParseException {
88          try {
89              getXmlReader().parse(new InputSource(new StringReader(content)));
90          } catch (IOException | SAXException | ParserConfigurationException e) {
91              throw new ParseException("Error validating the model", e);
92          }
93      }
94  
95      /**
96       * @return an xmlReader instance.
97       * @throws SAXException if any
98       * @throws ParserConfigurationException
99       */
100     public XMLReader getXmlReader() throws SAXException, ParserConfigurationException {
101         if (xmlReader == null) {
102             SAXParserFactory parserFactory = SAXParserFactory.newInstance();
103             parserFactory.setNamespaceAware(true);
104             SAXParser parser = parserFactory.newSAXParser();
105             // If both DTD and XSD are provided, force XSD
106             parser.setProperty(
107                     "http://java.sun.com/xml/jaxp/properties/schemaLanguage", "http://www.w3.org/2001/XMLSchema");
108             // Always force language-neutral exception messages for MessagesErrorHandler
109             parser.setProperty("http://apache.org/xml/properties/locale", Locale.ROOT);
110             xmlReader = parser.getXMLReader();
111             xmlReader.setFeature("http://xml.org/sax/features/validation", isValidate());
112             xmlReader.setFeature("http://apache.org/xml/features/validation/dynamic", isValidate());
113             xmlReader.setFeature("http://apache.org/xml/features/validation/schema", isValidate());
114             xmlReader.setErrorHandler(getDefaultHandler());
115             xmlReader.setEntityResolver(getEntityResolver());
116         }
117 
118         return xmlReader;
119     }
120 
121     /**
122      * Convenience class to beautify <code>SAXParseException</code> messages.
123      */
124     public static class MessagesErrorHandler extends DefaultHandler {
125         private static final int TYPE_UNKNOWN = 0;
126 
127         private static final int TYPE_WARNING = 1;
128 
129         private static final int TYPE_ERROR = 2;
130 
131         private static final int TYPE_FATAL = 3;
132 
133         private static final String EOL = XmlMarkup.EOL;
134 
135         /** @see org/apache/xerces/impl/msg/XMLMessages.properties#MSG_ELEMENT_NOT_DECLARED */
136         private static final Pattern ELEMENT_TYPE_PATTERN =
137                 Pattern.compile("Element type \".*\" must be declared.", Pattern.DOTALL);
138 
139         /** {@inheritDoc} */
140         @Override
141         public void warning(SAXParseException e) throws SAXException {
142             processException(TYPE_WARNING, e);
143         }
144 
145         /** {@inheritDoc} */
146         @Override
147         public void error(SAXParseException e) throws SAXException {
148             Matcher m = ELEMENT_TYPE_PATTERN.matcher(e.getMessage());
149             if (!m.find()) {
150                 processException(TYPE_ERROR, e);
151             }
152         }
153 
154         /** {@inheritDoc} */
155         @Override
156         public void fatalError(SAXParseException e) throws SAXException {
157             processException(TYPE_FATAL, e);
158         }
159 
160         private void processException(int type, SAXParseException e) throws SAXException {
161             StringBuilder message = new StringBuilder();
162 
163             switch (type) {
164                 case TYPE_WARNING:
165                     message.append("Warning:");
166                     break;
167 
168                 case TYPE_ERROR:
169                     message.append("Error:");
170                     break;
171 
172                 case TYPE_FATAL:
173                     message.append("Fatal error:");
174                     break;
175 
176                 case TYPE_UNKNOWN:
177                 default:
178                     message.append("Unknown:");
179                     break;
180             }
181 
182             message.append(EOL);
183             message.append("  Public ID: ").append(e.getPublicId()).append(EOL);
184             message.append("  System ID: ").append(e.getSystemId()).append(EOL);
185             message.append("  Line number: ").append(e.getLineNumber()).append(EOL);
186             message.append("  Column number: ").append(e.getColumnNumber()).append(EOL);
187             message.append("  Message: ").append(e.getMessage()).append(EOL);
188 
189             final String logMessage = message.toString();
190 
191             switch (type) {
192                 case TYPE_WARNING:
193                     LOGGER.warn(logMessage);
194                     break;
195 
196                 case TYPE_UNKNOWN:
197                 case TYPE_ERROR:
198                 case TYPE_FATAL:
199                 default:
200                     throw new SAXException(logMessage);
201             }
202         }
203     }
204 }