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.xsd;
20  
21  import javax.xml.parsers.ParserConfigurationException;
22  
23  import java.io.IOException;
24  import java.io.StringReader;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.List;
28  import java.util.Map;
29  
30  import org.apache.maven.doxia.util.XmlValidator;
31  import org.codehaus.plexus.testing.PlexusTest;
32  import org.junit.jupiter.api.AfterEach;
33  import org.junit.jupiter.api.Test;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  import org.xml.sax.EntityResolver;
37  import org.xml.sax.InputSource;
38  import org.xml.sax.SAXException;
39  import org.xml.sax.SAXNotRecognizedException;
40  import org.xml.sax.SAXNotSupportedException;
41  import org.xml.sax.SAXParseException;
42  import org.xml.sax.XMLReader;
43  import org.xml.sax.helpers.DefaultHandler;
44  
45  import static org.junit.jupiter.api.Assertions.assertFalse;
46  import static org.junit.jupiter.api.Assertions.fail;
47  
48  /**
49   * Abstract class to validate XML files.
50   *
51   * @author ltheussl
52   *
53   * @since 1.2
54   */
55  @PlexusTest
56  public abstract class AbstractXmlValidator {
57  
58      protected static final String EOL = System.getProperty("line.separator");
59  
60      protected final Logger logger = LoggerFactory.getLogger(getClass());
61  
62      /** XMLReader to validate xml file */
63      private XMLReader xmlReader;
64  
65      /** HTML5 does not have a DTD or XSD, include option to disable validation */
66      private boolean validate = true;
67  
68      /**
69       * Filter fail message.
70       *
71       * @param message not null
72       * @return <code>true</code> if the given message will fail the test.
73       * @since 1.1.1
74       */
75      protected boolean isFailErrorMessage(String message) {
76          return !(message.contains("schema_reference.4: Failed to read schema document 'http://www.w3.org/2001/xml.xsd'")
77                  || message.contains("cvc-complex-type.4: Attribute 'alt' must appear on element 'img'.")
78                  || message.contains("cvc-complex-type.2.4.a: Invalid content starting with element")
79                  || message.contains("cvc-complex-type.2.4.a: Invalid content was found starting with element")
80                  || message.contains("cvc-datatype-valid.1.2.1:") // Doxia allow space
81                  || message.contains("cvc-attribute.3:")); // Doxia allow space
82      }
83  
84      @AfterEach
85      protected void tearDown() {
86          xmlReader = null;
87      }
88  
89      /**
90       * Validate the test documents returned by {@link #getTestDocuments()} with DTD or XSD using xerces.
91       *
92       * @throws Exception if any
93       *
94       * @see #addNamespaces(String)
95       * @see #getTestDocuments()
96       */
97      @Test
98      public void testValidateFiles() throws Exception {
99          Map<String, String> testDocuments = getTestDocuments();
100         assertFalse(testDocuments.isEmpty(), "No test documents found");
101         for (Map.Entry<String, String> entry : testDocuments.entrySet()) {
102             if (logger.isDebugEnabled()) {
103                 logger.debug("Validate '" + entry.getKey() + "'");
104             }
105 
106             List<ErrorMessage> errors = parseXML(entry.getValue());
107 
108             for (ErrorMessage error : errors) {
109                 if (isFailErrorMessage(error.getMessage())) {
110                     fail(entry.getKey() + EOL + error.toString());
111                 } else {
112                     if (logger.isDebugEnabled()) {
113                         logger.debug(entry.getKey() + EOL + error.toString());
114                     }
115                 }
116             }
117         }
118     }
119 
120     /**
121      * @param content xml content not null
122      * @return xml content with the wanted Doxia namespace
123      */
124     protected abstract String addNamespaces(String content);
125 
126     /**
127      * @return a Map &lt; filePath, fileContent &gt; of files to validate.
128      * @throws IOException if any
129      */
130     protected abstract Map<String, String> getTestDocuments() throws IOException;
131 
132     /**
133      * Returns the EntityResolver that is used by the XMLReader for validation.
134      *
135      * @return an EntityResolver. Not null.
136      */
137     protected abstract EntityResolver getEntityResolver();
138 
139     /**
140      * Returns whether the XMLReader should validate XML.
141      * @return true if validation should be performed, false otherwise.
142      */
143     protected boolean isValidate() {
144         return validate;
145     }
146 
147     /**
148      * Sets whether the XMLReader should validate XML.
149      * @param validate true if validation should be performed, false otherwise.
150      */
151     protected void setValidate(boolean validate) {
152         this.validate = validate;
153     }
154 
155     // ----------------------------------------------------------------------
156     // Private methods
157     // ----------------------------------------------------------------------
158 
159     private XMLReader getXMLReader() {
160         if (xmlReader == null) {
161             try {
162                 XmlValidator validator = new XmlValidator();
163                 validator.setValidate(validate);
164                 validator.setDefaultHandler(new MessagesErrorHandler());
165                 validator.setEntityResolver(getEntityResolver());
166                 xmlReader = validator.getXmlReader();
167             } catch (SAXNotRecognizedException e) {
168                 fail("SAXNotRecognizedException: " + e.getMessage());
169             } catch (SAXNotSupportedException e) {
170                 fail("SAXNotSupportedException: " + e.getMessage());
171             } catch (SAXException e) {
172                 fail("SAXException: " + e.getMessage());
173             } catch (ParserConfigurationException e) {
174                 fail("ParserConfigurationException: " + e.getMessage());
175             }
176         }
177 
178         ((MessagesErrorHandler) xmlReader.getErrorHandler()).clearMessages();
179 
180         return xmlReader;
181     }
182 
183     /**
184      * @param content
185      * @return a list of ErrorMessage
186      * @throws IOException is any
187      * @throws SAXException if any
188      */
189     private List<ErrorMessage> parseXML(String content) throws IOException, SAXException {
190         String xmlContent = addNamespaces(content);
191 
192         XMLReader xmlReader = getXMLReader();
193 
194         MessagesErrorHandler errorHandler = (MessagesErrorHandler) xmlReader.getErrorHandler();
195 
196         xmlReader.parse(new InputSource(new StringReader(xmlContent)));
197 
198         return errorHandler.getMessages();
199     }
200 
201     private static class ErrorMessage extends DefaultHandler {
202         private final String level;
203         private final String publicID;
204         private final String systemID;
205         private final int lineNumber;
206         private final int columnNumber;
207         private final String message;
208 
209         ErrorMessage(String level, String publicID, String systemID, int lineNumber, int columnNumber, String message) {
210             super();
211             this.level = level;
212             this.publicID = publicID;
213             this.systemID = systemID;
214             this.lineNumber = lineNumber;
215             this.columnNumber = columnNumber;
216             this.message = message;
217         }
218 
219         /**
220          * @return the level
221          */
222         protected String getLevel() {
223             return level;
224         }
225 
226         /**
227          * @return the publicID
228          */
229         protected String getPublicID() {
230             return publicID;
231         }
232         /**
233          * @return the systemID
234          */
235         protected String getSystemID() {
236             return systemID;
237         }
238         /**
239          * @return the lineNumber
240          */
241         protected int getLineNumber() {
242             return lineNumber;
243         }
244         /**
245          * @return the columnNumber
246          */
247         protected int getColumnNumber() {
248             return columnNumber;
249         }
250         /**
251          * @return the message
252          */
253         protected String getMessage() {
254             return message;
255         }
256 
257         /** {@inheritDoc} */
258         @Override
259         public String toString() {
260             StringBuilder sb = new StringBuilder(512);
261 
262             sb.append(level).append(EOL);
263             sb.append("  Public ID: ").append(publicID).append(EOL);
264             sb.append("  System ID: ").append(systemID).append(EOL);
265             sb.append("  Line number: ").append(lineNumber).append(EOL);
266             sb.append("  Column number: ").append(columnNumber).append(EOL);
267             sb.append("  Message: ").append(message).append(EOL);
268 
269             return sb.toString();
270         }
271 
272         /** {@inheritDoc} */
273         @Override
274         public int hashCode() {
275             final int prime = 31;
276             int result = 1;
277             result = prime * result + columnNumber;
278             result = prime * result + ((level == null) ? 0 : level.hashCode());
279             result = prime * result + lineNumber;
280             result = prime * result + ((message == null) ? 0 : message.hashCode());
281             result = prime * result + ((publicID == null) ? 0 : publicID.hashCode());
282             result = prime * result + ((systemID == null) ? 0 : systemID.hashCode());
283             return result;
284         }
285 
286         /** {@inheritDoc} */
287         @Override
288         public boolean equals(Object obj) {
289             if (this == obj) {
290                 return true;
291             }
292             if (obj == null) {
293                 return false;
294             }
295             if (getClass() != obj.getClass()) {
296                 return false;
297             }
298             ErrorMessage other = (ErrorMessage) obj;
299             if (columnNumber != other.getColumnNumber()) {
300                 return false;
301             }
302             if (level == null) {
303                 if (other.getLevel() != null) {
304                     return false;
305                 }
306             } else if (!level.equals(other.getLevel())) {
307                 return false;
308             }
309             if (lineNumber != other.getLineNumber()) {
310                 return false;
311             }
312             if (message == null) {
313                 if (other.getMessage() != null) {
314                     return false;
315                 }
316             } else if (!message.equals(other.getMessage())) {
317                 return false;
318             }
319             if (publicID == null) {
320                 if (other.getPublicID() != null) {
321                     return false;
322                 }
323             } else if (!publicID.equals(other.getPublicID())) {
324                 return false;
325             }
326             if (systemID == null) {
327                 if (other.getSystemID() != null) {
328                     return false;
329                 }
330             } else if (!systemID.equals(other.getSystemID())) {
331                 return false;
332             }
333             return true;
334         }
335     }
336 
337     private static class MessagesErrorHandler extends DefaultHandler {
338         private final List<ErrorMessage> messages;
339 
340         MessagesErrorHandler() {
341             messages = new ArrayList<>(8);
342         }
343 
344         /** {@inheritDoc} */
345         @Override
346         public void warning(SAXParseException e) throws SAXException {
347             addMessage("Warning", e);
348         }
349 
350         /** {@inheritDoc} */
351         @Override
352         public void error(SAXParseException e) throws SAXException {
353             addMessage("Error", e);
354         }
355 
356         /** {@inheritDoc} */
357         @Override
358         public void fatalError(SAXParseException e) throws SAXException {
359             addMessage("Fatal error", e);
360         }
361 
362         private void addMessage(String pre, SAXParseException e) {
363             ErrorMessage error = new ErrorMessage(
364                     pre, e.getPublicId(), e.getSystemId(), e.getLineNumber(), e.getColumnNumber(), e.getMessage());
365 
366             messages.add(error);
367         }
368 
369         protected List<ErrorMessage> getMessages() {
370             return Collections.unmodifiableList(messages);
371         }
372 
373         protected void clearMessages() {
374             messages.clear();
375         }
376     }
377 }