| /* |
| * 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 |
| * |
| * https://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<FileSet> 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<Attribute> attributeList = new Vector<>(); |
| |
| /** |
| * List of properties. |
| */ |
| private final Vector<Property> 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> |
| * <p>If not set, the default will be used.</p> |
| * @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.isEmpty()) { |
| 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); |
| } |
| } |
| } |
| |
| for (final FileSet fs : filesets) { |
| DirectoryScanner ds = fs.getDirectoryScanner(getProject()); |
| for (String fileName : ds.getIncludedFiles()) { |
| File srcFile = new File(fs.getDir(getProject()), fileName); |
| 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 |
| for (final Attribute feature : attributeList) { |
| setFeature(feature.getName(), feature.getValue()); |
| |
| } |
| // Sets properties |
| for (final Property prop : propertyList) { |
| 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 | IllegalAccessException | InstantiationException 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="https://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 |
| |
| |
| |
| } |