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