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 &lt; filePath, fileContent &gt; 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}