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        /** {@inheritDoc} */
140        @Override
141        public void warning(SAXParseException e) throws SAXException {
142            processException(TYPE_WARNING, e);
143        }
144
145        /** {@inheritDoc} */
146        @Override
147        public void error(SAXParseException e) throws SAXException {
148            Matcher m = ELEMENT_TYPE_PATTERN.matcher(e.getMessage());
149            if (!m.find()) {
150                processException(TYPE_ERROR, e);
151            }
152        }
153
154        /** {@inheritDoc} */
155        @Override
156        public void fatalError(SAXParseException e) throws SAXException {
157            processException(TYPE_FATAL, e);
158        }
159
160        private void processException(int type, SAXParseException e) throws SAXException {
161            StringBuilder message = new StringBuilder();
162
163            switch (type) {
164                case TYPE_WARNING:
165                    message.append("Warning:");
166                    break;
167
168                case TYPE_ERROR:
169                    message.append("Error:");
170                    break;
171
172                case TYPE_FATAL:
173                    message.append("Fatal error:");
174                    break;
175
176                case TYPE_UNKNOWN:
177                default:
178                    message.append("Unknown:");
179                    break;
180            }
181
182            message.append(EOL);
183            message.append("  Public ID: ").append(e.getPublicId()).append(EOL);
184            message.append("  System ID: ").append(e.getSystemId()).append(EOL);
185            message.append("  Line number: ").append(e.getLineNumber()).append(EOL);
186            message.append("  Column number: ").append(e.getColumnNumber()).append(EOL);
187            message.append("  Message: ").append(e.getMessage()).append(EOL);
188
189            final String logMessage = message.toString();
190
191            switch (type) {
192                case TYPE_WARNING:
193                    LOGGER.warn(logMessage);
194                    break;
195
196                case TYPE_UNKNOWN:
197                case TYPE_ERROR:
198                case TYPE_FATAL:
199                default:
200                    throw new SAXException(logMessage);
201            }
202        }
203    }
204}