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}