blob: 962ba75249045625d2739aa6dff32b4db3e5a2a9 [file] [log] [blame]
/*
* 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.FileInputStream;
import java.io.IOException;
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 sucessfull validation */
public static final String MESSAGE_FILES_VALIDATED
= " file(s) have been successfully validated.";
/**
* 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</true> (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 {
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);
}
}
}
for (int i = 0; i < filesets.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);
}
/**
* 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 (int i = 0; i < attributeList.size(); i++) {
Attribute feature = (Attribute) attributeList.elementAt(i);
setFeature(feature.getName(), feature.getValue());
}
// Sets properties
for (int i = 0; i < propertyList.size(); 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) {
AntClassLoader loader =
getProject().createClassLoader(classpath);
readerClass = Class.forName(readerClassName, true, loader);
} 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;
}
/**
*
* @return
*/
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(new FileInputStream(afile));
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
}