001package org.apache.maven.doxia.xsd; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.io.IOException; 023import java.io.StringReader; 024import java.util.ArrayList; 025import java.util.Collections; 026import java.util.Iterator; 027import java.util.List; 028import java.util.Map; 029 030import junit.framework.AssertionFailedError; 031 032import org.apache.maven.doxia.parser.Parser; 033 034import org.codehaus.plexus.PlexusTestCase; 035import org.codehaus.plexus.logging.Logger; 036 037import org.xml.sax.EntityResolver; 038import org.xml.sax.InputSource; 039import org.xml.sax.SAXException; 040import org.xml.sax.SAXNotRecognizedException; 041import org.xml.sax.SAXNotSupportedException; 042import org.xml.sax.SAXParseException; 043import org.xml.sax.XMLReader; 044import org.xml.sax.helpers.DefaultHandler; 045import org.xml.sax.helpers.XMLReaderFactory; 046 047/** 048 * Abstract class to validate XML files. 049 * 050 * @author ltheussl 051 * 052 * @since 1.2 053 */ 054public abstract class AbstractXmlValidator 055 extends PlexusTestCase 056{ 057 protected static final String EOL = System.getProperty( "line.separator" ); 058 059 /** XMLReader to validate xml file */ 060 private XMLReader xmlReader; 061 062 /** 063 * Filter fail message. 064 * 065 * @param message not null 066 * @return <code>true</code> if the given message will fail the test. 067 * @since 1.1.1 068 */ 069 protected boolean isFailErrorMessage( String message ) 070 { 071 return !( message.contains( "schema_reference.4: Failed to read schema document 'http://www.w3.org/2001/xml.xsd'" ) 072 || message.contains( "cvc-complex-type.4: Attribute 'alt' must appear on element 'img'." ) 073 || message.contains( "cvc-complex-type.2.4.a: Invalid content starting with element" ) 074 || message.contains( "cvc-complex-type.2.4.a: Invalid content was found starting with element" ) 075 || message.contains( "cvc-datatype-valid.1.2.1:" ) // Doxia allow space 076 || message.contains( "cvc-attribute.3:" ) ); // Doxia allow space 077 } 078 079 @Override 080 protected void tearDown() 081 throws Exception 082 { 083 super.tearDown(); 084 085 xmlReader = null; 086 } 087 088 /** 089 * Validate the test documents returned by {@link #getTestDocuments()} with DTD or XSD using xerces. 090 * 091 * @throws Exception if any 092 * 093 * @see #addNamespaces(String) 094 * @see #getTestDocuments() 095 */ 096 public void testValidateFiles() 097 throws Exception 098 { 099 final Logger logger = getContainer().getLoggerManager().getLoggerForComponent( Parser.ROLE ); 100 101 for ( Iterator<Map.Entry<String, String>> it = getTestDocuments().entrySet().iterator(); it.hasNext(); ) 102 { 103 Map.Entry<String, String> entry = it.next(); 104 105 if ( logger.isDebugEnabled() ) 106 { 107 logger.debug( "Validate '" + entry.getKey() + "'" ); 108 } 109 110 List<ErrorMessage> errors = parseXML( entry.getValue().toString() ); 111 112 for ( Iterator<ErrorMessage> it2 = errors.iterator(); it2.hasNext(); ) 113 { 114 ErrorMessage error = it2.next(); 115 116 if ( isFailErrorMessage( error.getMessage() ) ) 117 { 118 fail( entry.getKey() + EOL + error.toString() ); 119 } 120 else 121 { 122 if ( logger.isDebugEnabled() ) 123 { 124 logger.debug( entry.getKey() + EOL + error.toString() ); 125 } 126 } 127 } 128 } 129 } 130 131 /** 132 * @param content xml content not null 133 * @return xml content with the wanted Doxia namespace 134 */ 135 protected abstract String addNamespaces( String content ); 136 137 /** 138 * @return a Map < filePath, fileContent > of files to validate. 139 * @throws IOException if any 140 */ 141 protected abstract Map<String,String> getTestDocuments() 142 throws IOException; 143 144 /** 145 * Returns the EntityResolver that is used by the XMLReader for validation. 146 * 147 * @return an EntityResolver. Not null. 148 */ 149 protected abstract EntityResolver getEntityResolver(); 150 151 // ---------------------------------------------------------------------- 152 // Private methods 153 // ---------------------------------------------------------------------- 154 155 private XMLReader getXMLReader() 156 { 157 if ( xmlReader == null ) 158 { 159 try 160 { 161 xmlReader = XMLReaderFactory.createXMLReader( "org.apache.xerces.parsers.SAXParser" ); 162 xmlReader.setFeature( "http://xml.org/sax/features/validation", true ); 163 xmlReader.setFeature( "http://apache.org/xml/features/validation/schema", true ); 164 xmlReader.setErrorHandler( new MessagesErrorHandler() ); 165 xmlReader.setEntityResolver( getEntityResolver() ); 166 } 167 catch ( SAXNotRecognizedException e ) 168 { 169 throw new AssertionFailedError( "SAXNotRecognizedException: " + e.getMessage() ); 170 } 171 catch ( SAXNotSupportedException e ) 172 { 173 throw new AssertionFailedError( "SAXNotSupportedException: " + e.getMessage() ); 174 } 175 catch ( SAXException e ) 176 { 177 throw new AssertionFailedError( "SAXException: " + e.getMessage() ); 178 } 179 } 180 181 ( (MessagesErrorHandler) xmlReader.getErrorHandler() ).clearMessages(); 182 183 return xmlReader; 184 } 185 186 /** 187 * @param content 188 * @return a list of ErrorMessage 189 * @throws IOException is any 190 * @throws SAXException if any 191 */ 192 private List<ErrorMessage> parseXML( String content ) 193 throws IOException, SAXException 194 { 195 String xmlContent = addNamespaces( content ); 196 197 MessagesErrorHandler errorHandler = (MessagesErrorHandler) getXMLReader().getErrorHandler(); 198 199 getXMLReader().parse( new InputSource( new StringReader( xmlContent ) ) ); 200 201 return errorHandler.getMessages(); 202 } 203 204 private static class ErrorMessage 205 extends DefaultHandler 206 { 207 private final String level; 208 private final String publicID; 209 private final String systemID; 210 private final int lineNumber; 211 private final int columnNumber; 212 private final String message; 213 214 ErrorMessage( String level, String publicID, String systemID, int lineNumber, int columnNumber, 215 String message ) 216 { 217 super(); 218 this.level = level; 219 this.publicID = publicID; 220 this.systemID = systemID; 221 this.lineNumber = lineNumber; 222 this.columnNumber = columnNumber; 223 this.message = message; 224 } 225 226 /** 227 * @return the level 228 */ 229 protected String getLevel() 230 { 231 return level; 232 } 233 234 /** 235 * @return the publicID 236 */ 237 protected String getPublicID() 238 { 239 return publicID; 240 } 241 /** 242 * @return the systemID 243 */ 244 protected String getSystemID() 245 { 246 return systemID; 247 } 248 /** 249 * @return the lineNumber 250 */ 251 protected int getLineNumber() 252 { 253 return lineNumber; 254 } 255 /** 256 * @return the columnNumber 257 */ 258 protected int getColumnNumber() 259 { 260 return columnNumber; 261 } 262 /** 263 * @return the message 264 */ 265 protected String getMessage() 266 { 267 return message; 268 } 269 270 /** {@inheritDoc} */ 271 @Override 272 public String toString() 273 { 274 StringBuilder sb = new StringBuilder( 512 ); 275 276 sb.append( level ).append( EOL ); 277 sb.append( " Public ID: " ).append( publicID ).append( EOL ); 278 sb.append( " System ID: " ).append( systemID ).append( EOL ); 279 sb.append( " Line number: " ).append( lineNumber ).append( EOL ); 280 sb.append( " Column number: " ).append( columnNumber ).append( EOL ); 281 sb.append( " Message: " ).append( message ).append( EOL ); 282 283 return sb.toString(); 284 } 285 286 /** {@inheritDoc} */ 287 @Override 288 public int hashCode() 289 { 290 final int prime = 31; 291 int result = 1; 292 result = prime * result + columnNumber; 293 result = prime * result + ( ( level == null ) ? 0 : level.hashCode() ); 294 result = prime * result + lineNumber; 295 result = prime * result + ( ( message == null ) ? 0 : message.hashCode() ); 296 result = prime * result + ( ( publicID == null ) ? 0 : publicID.hashCode() ); 297 result = prime * result + ( ( systemID == null ) ? 0 : systemID.hashCode() ); 298 return result; 299 } 300 301 /** {@inheritDoc} */ 302 @Override 303 public boolean equals( Object obj ) 304 { 305 if ( this == obj ) 306 { 307 return true; 308 } 309 if ( obj == null ) 310 { 311 return false; 312 } 313 if ( getClass() != obj.getClass() ) 314 { 315 return false; 316 } 317 ErrorMessage other = (ErrorMessage) obj; 318 if ( columnNumber != other.getColumnNumber() ) 319 { 320 return false; 321 } 322 if ( level == null ) 323 { 324 if ( other.getLevel() != null ) 325 { 326 return false; 327 } 328 } 329 else if ( !level.equals( other.getLevel() ) ) 330 { 331 return false; 332 } 333 if ( lineNumber != other.getLineNumber() ) 334 { 335 return false; 336 } 337 if ( message == null ) 338 { 339 if ( other.getMessage() != null ) 340 { 341 return false; 342 } 343 } 344 else if ( !message.equals( other.getMessage() ) ) 345 { 346 return false; 347 } 348 if ( publicID == null ) 349 { 350 if ( other.getPublicID() != null ) 351 { 352 return false; 353 } 354 } 355 else if ( !publicID.equals( other.getPublicID() ) ) 356 { 357 return false; 358 } 359 if ( systemID == null ) 360 { 361 if ( other.getSystemID() != null ) 362 { 363 return false; 364 } 365 } 366 else if ( !systemID.equals( other.getSystemID() ) ) 367 { 368 return false; 369 } 370 return true; 371 } 372 } 373 374 private static class MessagesErrorHandler 375 extends DefaultHandler 376 { 377 private final List<ErrorMessage> messages; 378 379 MessagesErrorHandler() 380 { 381 messages = new ArrayList<ErrorMessage>( 8 ); 382 } 383 384 /** {@inheritDoc} */ 385 @Override 386 public void warning( SAXParseException e ) 387 throws SAXException 388 { 389 addMessage( "Warning", e ); 390 } 391 392 /** {@inheritDoc} */ 393 @Override 394 public void error( SAXParseException e ) 395 throws SAXException 396 { 397 addMessage( "Error", e ); 398 } 399 400 /** {@inheritDoc} */ 401 @Override 402 public void fatalError( SAXParseException e ) 403 throws SAXException 404 { 405 addMessage( "Fatal error", e ); 406 } 407 408 private void addMessage( String pre, SAXParseException e ) 409 { 410 ErrorMessage error = 411 new ErrorMessage( pre, e.getPublicId(), e.getSystemId(), e.getLineNumber(), e.getColumnNumber(), 412 e.getMessage() ); 413 414 messages.add( error ); 415 } 416 417 protected List<ErrorMessage> getMessages() 418 { 419 return Collections.unmodifiableList( messages ); 420 } 421 422 protected void clearMessages() 423 { 424 messages.clear(); 425 } 426 } 427}