001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.doxia.util;
020
021import javax.xml.parsers.ParserConfigurationException;
022import javax.xml.parsers.SAXParser;
023import javax.xml.parsers.SAXParserFactory;
024
025import java.io.IOException;
026import java.io.StringReader;
027import java.util.Locale;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030
031import org.apache.maven.doxia.markup.XmlMarkup;
032import org.apache.maven.doxia.parser.ParseException;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035import org.xml.sax.EntityResolver;
036import org.xml.sax.InputSource;
037import org.xml.sax.SAXException;
038import org.xml.sax.SAXParseException;
039import org.xml.sax.XMLReader;
040import org.xml.sax.helpers.DefaultHandler;
041
042/**
043 * A class to validate xml documents.
044 *
045 * @since 1.1.3
046 */
047public class XmlValidator {
048    private static final Logger LOGGER = LoggerFactory.getLogger(XmlValidator.class);
049
050    /** lazy xmlReader to validate xml content*/
051    private XMLReader xmlReader;
052
053    private boolean validate = true;
054    private DefaultHandler defaultHandler;
055    private EntityResolver entityResolver;
056
057    public boolean isValidate() {
058        return validate;
059    }
060
061    public void setValidate(boolean validate) {
062        this.validate = validate;
063    }
064
065    public DefaultHandler getDefaultHandler() {
066        return defaultHandler;
067    }
068
069    public void setDefaultHandler(DefaultHandler defaultHandler) {
070        this.defaultHandler = defaultHandler;
071    }
072
073    public EntityResolver getEntityResolver() {
074        return entityResolver;
075    }
076
077    public void setEntityResolver(EntityResolver entityResolver) {
078        this.entityResolver = entityResolver;
079    }
080
081    /**
082     * Validate an XML content with SAX.
083     *
084     * @param content a not null xml content
085     * @throws ParseException if any.
086     */
087    public void validate(String content) throws ParseException {
088        try {
089            getXmlReader().parse(new InputSource(new StringReader(content)));
090        } catch (IOException | SAXException | ParserConfigurationException e) {
091            throw new ParseException("Error validating the model", e);
092        }
093    }
094
095    /**
096     * @return an xmlReader instance.
097     * @throws SAXException if any
098     * @throws ParserConfigurationException
099     */
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        @Override
140        public void warning(SAXParseException e) throws SAXException {
141            processException(TYPE_WARNING, e);
142        }
143
144        @Override
145        public void error(SAXParseException e) throws SAXException {
146            Matcher m = ELEMENT_TYPE_PATTERN.matcher(e.getMessage());
147            if (!m.find()) {
148                processException(TYPE_ERROR, e);
149            }
150        }
151
152        @Override
153        public void fatalError(SAXParseException e) throws SAXException {
154            processException(TYPE_FATAL, e);
155        }
156
157        private void processException(int type, SAXParseException e) throws SAXException {
158            StringBuilder message = new StringBuilder();
159
160            switch (type) {
161                case TYPE_WARNING:
162                    message.append("Warning:");
163                    break;
164
165                case TYPE_ERROR:
166                    message.append("Error:");
167                    break;
168
169                case TYPE_FATAL:
170                    message.append("Fatal error:");
171                    break;
172
173                case TYPE_UNKNOWN:
174                default:
175                    message.append("Unknown:");
176                    break;
177            }
178
179            message.append(EOL);
180            message.append("  Public ID: ").append(e.getPublicId()).append(EOL);
181            message.append("  System ID: ").append(e.getSystemId()).append(EOL);
182            message.append("  Line number: ").append(e.getLineNumber()).append(EOL);
183            message.append("  Column number: ").append(e.getColumnNumber()).append(EOL);
184            message.append("  Message: ").append(e.getMessage()).append(EOL);
185
186            final String logMessage = message.toString();
187
188            switch (type) {
189                case TYPE_WARNING:
190                    LOGGER.warn(logMessage);
191                    break;
192
193                case TYPE_UNKNOWN:
194                case TYPE_ERROR:
195                case TYPE_FATAL:
196                default:
197                    throw new SAXException(logMessage);
198            }
199        }
200    }
201}