/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.ant.taskdefs.optional; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.Vector; import org.apache.tools.ant.AntClassLoader; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.Project; import org.apache.tools.ant.Task; import org.apache.tools.ant.types.DTDLocation; import org.apache.tools.ant.types.FileSet; import org.apache.tools.ant.types.Path; import org.apache.tools.ant.types.Reference; import org.apache.tools.ant.types.XMLCatalog; import org.apache.tools.ant.util.FileUtils; import org.apache.tools.ant.util.JAXPUtils; import org.apache.tools.ant.util.XmlConstants; import org.xml.sax.EntityResolver; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.Parser; import org.xml.sax.SAXException; import org.xml.sax.SAXNotRecognizedException; import org.xml.sax.SAXNotSupportedException; import org.xml.sax.SAXParseException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.ParserAdapter; /** * Checks XML files are valid (or only well formed). The * task uses the SAX2 parser implementation provided by JAXP by default * (probably the one that is used by Ant itself), but one can specify any * SAX1/2 parser if needed. * */ public class XMLValidateTask extends Task { /** * helper for path -> URI and URI -> path conversions. */ private static final FileUtils FILE_UTILS = FileUtils.getFileUtils(); protected static final String INIT_FAILED_MSG = "Could not start xml validation: "; // ant task properties // defaults // CheckStyle:VisibilityModifier OFF - bc protected boolean failOnError = true; protected boolean warn = true; protected boolean lenient = false; protected String readerClassName = null; /** file to be validated */ protected File file = null; /** sets of file to be validated */ protected Vector filesets = new Vector(); protected Path classpath; /** * the parser is viewed as a SAX2 XMLReader. If a SAX1 parser is specified, * it's wrapped in an adapter that make it behave as a XMLReader. * a more 'standard' way of doing this would be to use the JAXP1.1 SAXParser * interface. */ protected XMLReader xmlReader = null; // XMLReader used to validation process protected ValidatorErrorHandler errorHandler = new ValidatorErrorHandler(); // to report sax parsing errors // CheckStyle:VisibilityModifier ON /** The vector to store all attributes (features) to be set on the parser. **/ private Vector attributeList = new Vector(); /** * List of properties. */ private final Vector propertyList = new Vector(); private XMLCatalog xmlCatalog = new XMLCatalog(); /** Message for successful validation */ public static final String MESSAGE_FILES_VALIDATED = " file(s) have been successfully validated."; private AntClassLoader readerLoader = null; /** * Specify how parser error are to be handled. * Optional, default is <code>true</code>. * <p> * If set to <code>true</code> (default), throw a buildException if the * parser yields an error. * @param fail if set to <code>false</code> do not fail on error */ public void setFailOnError(boolean fail) { failOnError = fail; } /** * Specify how parser error are to be handled. * <p> * If set to <code>true</code> (default), log a warn message for each SAX warn event. * @param bool if set to <code>false</code> do not send warnings */ public void setWarn(boolean bool) { warn = bool; } /** * Specify whether the parser should be validating. Default * is <code>true</code>. * <p> * If set to false, the validation will fail only if the parsed document * is not well formed XML. * <p> * this option is ignored if the specified class * with {@link #setClassName(String)} is not a SAX2 XMLReader. * @param bool if set to <code>false</code> only fail on malformed XML */ public void setLenient(boolean bool) { lenient = bool; } /** * Specify the class name of the SAX parser to be used. (optional) * @param className should be an implementation of SAX2 * <code>org.xml.sax.XMLReader</code> or SAX2 <code>org.xml.sax.Parser</code>. * <p> if className is an implementation of * <code>org.xml.sax.Parser</code>, {@link #setLenient(boolean)}, * will be ignored. * <p> if not set, the default will be used. * @see org.xml.sax.XMLReader * @see org.xml.sax.Parser */ public void setClassName(String className) { readerClassName = className; } /** * Specify the classpath to be searched to load the parser (optional) * @param classpath the classpath to load the parser */ public void setClasspath(Path classpath) { if (this.classpath == null) { this.classpath = classpath; } else { this.classpath.append(classpath); } } /** * @see #setClasspath * @return the classpath created */ public Path createClasspath() { if (this.classpath == null) { this.classpath = new Path(getProject()); } return this.classpath.createPath(); } /** * Where to find the parser class; optional. * @see #setClasspath * @param r reference to a classpath defined elsewhere */ public void setClasspathRef(Reference r) { createClasspath().setRefid(r); } /** * specify the file to be checked; optional. * @param file the file to be checked */ public void setFile(File file) { this.file = file; } /** * add an XMLCatalog as a nested element; optional. * @param catalog XMLCatalog to use */ public void addConfiguredXMLCatalog(XMLCatalog catalog) { xmlCatalog.addConfiguredXMLCatalog(catalog); } /** * specify a set of file to be checked * @param set the fileset to check */ public void addFileset(FileSet set) { filesets.addElement(set); } /** * Add an attribute nested element. This is used for setting arbitrary * features of the SAX parser. * Valid attributes * <a href= * "http://www.saxproject.org/apidoc/org/xml/sax/package-summary.html#package_description" * >include</a> * @return attribute created * @since ant1.6 */ public Attribute createAttribute() { final Attribute feature = new Attribute(); attributeList.addElement(feature); return feature; } /** * Creates a property. * * @return a property. * @since ant 1.6.2 */ public Property createProperty() { final Property prop = new Property(); propertyList.addElement(prop); return prop; } /** * Called by the project to let the task initialize properly. * * @exception BuildException if something goes wrong with the build */ public void init() throws BuildException { super.init(); xmlCatalog.setProject(getProject()); } /** * Create a DTD location record; optional. * This stores the location of a DTD. The DTD is identified * by its public Id. * @return created DTD location */ public DTDLocation createDTD() { DTDLocation dtdLocation = new DTDLocation(); xmlCatalog.addDTD(dtdLocation); return dtdLocation; } /** * accessor to the xmlCatalog used in the task * @return xmlCatalog reference */ protected EntityResolver getEntityResolver() { return xmlCatalog; } /** * get the XML reader. Non-null only after {@link #initValidator()}. * If the reader is an instance of {@link ParserAdapter} then * the parser is a SAX1 parser, and you cannot call * {@link #setFeature(String, boolean)} or {@link #setProperty(String, String)} * on it. * @return the XML reader or null. */ protected XMLReader getXmlReader() { return xmlReader; } /** * execute the task * @throws BuildException if <code>failonerror</code> is true and an error happens */ public void execute() throws BuildException { try { int fileProcessed = 0; if (file == null && (filesets.size() == 0)) { throw new BuildException( "Specify at least one source - " + "a file or a fileset."); } if (file != null) { if (file.exists() && file.canRead() && file.isFile()) { doValidate(file); fileProcessed++; } else { String errorMsg = "File " + file + " cannot be read"; if (failOnError) { throw new BuildException(errorMsg); } else { log(errorMsg, Project.MSG_ERR); } } } final int size = filesets.size(); for (int i = 0; i < size; i++) { FileSet fs = (FileSet) filesets.elementAt(i); DirectoryScanner ds = fs.getDirectoryScanner(getProject()); String[] files = ds.getIncludedFiles(); for (int j = 0; j < files.length; j++) { File srcFile = new File(fs.getDir(getProject()), files[j]); doValidate(srcFile); fileProcessed++; } } onSuccessfulValidation(fileProcessed); } finally { cleanup(); } } /** * handler called on successful file validation. * @param fileProcessed number of files processed. */ protected void onSuccessfulValidation(int fileProcessed) { log(fileProcessed + MESSAGE_FILES_VALIDATED); } /** * init the parser : * load the parser class, and set features if necessary * It is only after this that the reader is valid * @throws BuildException if something went wrong */ protected void initValidator() { xmlReader = createXmlReader(); xmlReader.setEntityResolver(getEntityResolver()); xmlReader.setErrorHandler(errorHandler); if (!isSax1Parser()) { // turn validation on if (!lenient) { setFeature(XmlConstants.FEATURE_VALIDATION, true); } // set the feature from the attribute list final int attSize = attributeList.size(); for (int i = 0; i < attSize; i++) { Attribute feature = (Attribute) attributeList.elementAt(i); setFeature(feature.getName(), feature.getValue()); } // Sets properties final int propSize = propertyList.size(); for (int i = 0; i < propSize; i++) { final Property prop = (Property) propertyList.elementAt(i); setProperty(prop.getName(), prop.getValue()); } } } /** * test that returns true if we are using a SAX1 parser. * @return true when a SAX1 parser is in use */ protected boolean isSax1Parser() { return (xmlReader instanceof ParserAdapter); } /** * create the XML reader. * This is one by instantiating anything specified by {@link #readerClassName}, * falling back to a default reader if not. * If the returned reader is an instance of {@link ParserAdapter} then * we have created and wrapped a SAX1 parser. * @return the new XMLReader. */ protected XMLReader createXmlReader() { Object reader = null; if (readerClassName == null) { reader = createDefaultReaderOrParser(); } else { Class readerClass = null; try { // load the parser class if (classpath != null) { readerLoader = getProject().createClassLoader(classpath); readerClass = Class.forName(readerClassName, true, readerLoader); } else { readerClass = Class.forName(readerClassName); } reader = readerClass.newInstance(); } catch (ClassNotFoundException e) { throw new BuildException(INIT_FAILED_MSG + readerClassName, e); } catch (InstantiationException e) { throw new BuildException(INIT_FAILED_MSG + readerClassName, e); } catch (IllegalAccessException e) { throw new BuildException(INIT_FAILED_MSG + readerClassName, e); } } // then check it implements XMLReader XMLReader newReader; if (reader instanceof XMLReader) { newReader = (XMLReader) reader; log( "Using SAX2 reader " + reader.getClass().getName(), Project.MSG_VERBOSE); } else { // see if it is a SAX1 Parser if (reader instanceof Parser) { newReader = new ParserAdapter((Parser) reader); log( "Using SAX1 parser " + reader.getClass().getName(), Project.MSG_VERBOSE); } else { throw new BuildException( INIT_FAILED_MSG + reader.getClass().getName() + " implements nor SAX1 Parser nor SAX2 XMLReader."); } } return newReader; } /** * Cleans up resources. * * @since Ant 1.8.0 */ protected void cleanup() { if (readerLoader != null) { readerLoader.cleanup(); readerLoader = null; } } /** * Returns a SAX-based XMLReader or a SAX-based Parser. * @return reader or parser */ private Object createDefaultReaderOrParser() { Object reader; try { reader = createDefaultReader(); } catch (BuildException exc) { reader = JAXPUtils.getParser(); } return reader; } /** * Create a reader if the use of the class did not specify another one. * If a BuildException is thrown, the caller may revert to an alternate * reader. * @return a new reader. * @throws BuildException if something went wrong */ protected XMLReader createDefaultReader() { return JAXPUtils.getXMLReader(); } /** * Set a feature on the parser. * @param feature the name of the feature to set * @param value the value of the feature * @throws BuildException if the feature was not supported */ protected void setFeature(String feature, boolean value) throws BuildException { log("Setting feature " + feature + "=" + value, Project.MSG_DEBUG); try { xmlReader.setFeature(feature, value); } catch (SAXNotRecognizedException e) { throw new BuildException( "Parser " + xmlReader.getClass().getName() + " doesn't recognize feature " + feature, e, getLocation()); } catch (SAXNotSupportedException e) { throw new BuildException( "Parser " + xmlReader.getClass().getName() + " doesn't support feature " + feature, e, getLocation()); } } /** * Sets a property. * * @param name a property name * @param value a property value. * @throws BuildException if an error occurs. * @throws BuildException if the property was not supported */ protected void setProperty(String name, String value) throws BuildException { // Validates property if (name == null || value == null) { throw new BuildException("Property name and value must be specified."); } try { xmlReader.setProperty(name, value); } catch (SAXNotRecognizedException e) { throw new BuildException( "Parser " + xmlReader.getClass().getName() + " doesn't recognize property " + name, e, getLocation()); } catch (SAXNotSupportedException e) { throw new BuildException( "Parser " + xmlReader.getClass().getName() + " doesn't support property " + name, e, getLocation()); } } /** * parse the file * @param afile the file to validate. * @return true if the file validates. */ protected boolean doValidate(File afile) { //for every file, we have a new instance of the validator initValidator(); boolean result = true; try { log("Validating " + afile.getName() + "... ", Project.MSG_VERBOSE); errorHandler.init(afile); InputSource is = new InputSource(Files.newInputStream(afile.toPath())); String uri = FILE_UTILS.toURI(afile.getAbsolutePath()); is.setSystemId(uri); xmlReader.parse(is); } catch (SAXException ex) { log("Caught when validating: " + ex.toString(), Project.MSG_DEBUG); if (failOnError) { throw new BuildException( "Could not validate document " + afile); } log("Could not validate document " + afile + ": " + ex.toString()); result = false; } catch (IOException ex) { throw new BuildException( "Could not validate document " + afile, ex); } if (errorHandler.getFailure()) { if (failOnError) { throw new BuildException( afile + " is not a valid XML document."); } result = false; log(afile + " is not a valid XML document", Project.MSG_ERR); } return result; } /** * ValidatorErrorHandler role : * <ul> * <li> log SAX parse exceptions, * <li> remember if an error occurred * </ul> */ protected class ValidatorErrorHandler implements ErrorHandler { // CheckStyle:VisibilityModifier OFF - bc protected File currentFile = null; protected String lastErrorMessage = null; protected boolean failed = false; // CheckStyle:VisibilityModifier ON /** * initialises the class * @param file file used */ public void init(File file) { currentFile = file; failed = false; } /** * did an error happen during last parsing ? * @return did an error happen during last parsing ? */ public boolean getFailure() { return failed; } /** * record a fatal error * @param exception the fatal error */ public void fatalError(SAXParseException exception) { failed = true; doLog(exception, Project.MSG_ERR); } /** * receive notification of a recoverable error * @param exception the error */ public void error(SAXParseException exception) { failed = true; doLog(exception, Project.MSG_ERR); } /** * receive notification of a warning * @param exception the warning */ public void warning(SAXParseException exception) { // depending on implementation, XMLReader can yield hips of warning, // only output then if user explicitly asked for it if (warn) { doLog(exception, Project.MSG_WARN); } } private void doLog(SAXParseException e, int logLevel) { log(getMessage(e), logLevel); } private String getMessage(SAXParseException e) { String sysID = e.getSystemId(); if (sysID != null) { String name = sysID; if (sysID.startsWith("file:")) { try { name = FILE_UTILS.fromURI(sysID); } catch (Exception ex) { // if this is not a valid file: just use the uri } } int line = e.getLineNumber(); int col = e.getColumnNumber(); return name + (line == -1 ? "" : (":" + line + (col == -1 ? "" : (":" + col)))) + ": " + e.getMessage(); } return e.getMessage(); } } /** * The class to create to set a feature of the parser. * @since ant1.6 */ public static class Attribute { /** The name of the attribute to set. * * Valid attributes <a href= * "http://www.saxproject.org/apidoc/org/xml/sax/package-summary.html#package_description" * >include.</a> */ private String attributeName = null; /** * The value of the feature. **/ private boolean attributeValue; /** * Set the feature name. * @param name the name to set */ public void setName(String name) { attributeName = name; } /** * Set the feature value to true or false. * @param value feature value */ public void setValue(boolean value) { attributeValue = value; } /** * Gets the attribute name. * @return the feature name */ public String getName() { return attributeName; } /** * Gets the attribute value. * @return the feature value */ public boolean getValue() { return attributeValue; } } /** * A Parser property. * See <a href="http://xml.apache.org/xerces-j/properties.html"> * XML parser properties</a> for usable properties * @since ant 1.6.2 */ public static final class Property { private String name; private String value; /** * accessor to the name of the property * @return name of the property */ public String getName() { return name; } /** * setter for the name of the property * @param name name of the property */ public void setName(String name) { this.name = name; } /** * getter for the value of the property * @return value of the property */ public String getValue() { return value; } /** * sets the value of the property * @param value value of the property */ public void setValue(String value) { this.value = value; } } // Property }