blob: 354dcabb868a2e319f8e144e6fe8819cecfae8a5 [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
*
* 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.net.MalformedURLException;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.XmlConstants;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.XMLReader;
/**
* Validate XML Schema documents.
* This task validates XML schema documents. It requires an XML parser
* that handles the relevant SAX, Xerces or JAXP options.
*
* To resolve remote references, Ant may need its proxy set up, using the
* setproxy task.
*
* Hands off most of the work to its parent, {@link XMLValidateTask}
* @since Ant1.7
*/
public class SchemaValidate extends XMLValidateTask {
// Error strings
/** SAX1 not supported */
public static final String ERROR_SAX_1 = "SAX1 parsers are not supported";
/** schema features not supported */
public static final String ERROR_NO_XSD_SUPPORT
= "Parser does not support Xerces or JAXP schema features";
/** too many default schemas */
public static final String ERROR_TOO_MANY_DEFAULT_SCHEMAS
= "Only one of defaultSchemaFile and defaultSchemaURL allowed";
/** unable to create parser */
public static final String ERROR_PARSER_CREATION_FAILURE
= "Could not create parser";
/** adding schema */
public static final String MESSAGE_ADDING_SCHEMA = "Adding schema ";
/** Duplicate declaration of schema */
public static final String ERROR_DUPLICATE_SCHEMA
= "Duplicate declaration of schema ";
/** map of all declared schemas; we catch and complain about redefinitions */
private Map<String, SchemaLocation> schemaLocations = new HashMap<>();
/** full checking of a schema */
private boolean fullChecking = true;
/**
* flag to disable DTD support. Best left enabled.
*/
private boolean disableDTD = false;
/**
* default URL for nonamespace schemas
*/
private SchemaLocation anonymousSchema;
/**
* Called by the project to let the task initialize properly. The default
* implementation is a no-op.
*
* @throws BuildException if something goes wrong with the build
*/
@Override
public void init() throws BuildException {
super.init();
//validating
setLenient(false);
}
/**
* Turn on XSD support in Xerces.
* @return true on success, false on failure
*/
public boolean enableXercesSchemaValidation() {
try {
setFeature(XmlConstants.FEATURE_XSD, true);
//set the schema source for the doc
setNoNamespaceSchemaProperty(XmlConstants.PROPERTY_NO_NAMESPACE_SCHEMA_LOCATION);
} catch (BuildException e) {
log(e.toString(), Project.MSG_VERBOSE);
return false;
}
return true;
}
/**
* set nonamespace handling up for xerces or other parsers
* @param property name of the property to set
*/
private void setNoNamespaceSchemaProperty(String property) {
String anonSchema = getNoNamespaceSchemaURL();
if (anonSchema != null) {
setProperty(property, anonSchema);
}
}
/**
* Set schema attributes in a JAXP 1.2 engine.
* @see <A href="https://java.sun.com/xml/jaxp/change-requests-11.html">
* JAXP 1.2 Approved CHANGES</A>
* @return true on success, false on failure
*/
public boolean enableJAXP12SchemaValidation() {
try {
//enable XSD
setProperty(XmlConstants.FEATURE_JAXP12_SCHEMA_LANGUAGE, XmlConstants.URI_XSD);
//set the schema source for the doc
setNoNamespaceSchemaProperty(XmlConstants.FEATURE_JAXP12_SCHEMA_SOURCE);
} catch (BuildException e) {
log(e.toString(), Project.MSG_VERBOSE);
return false;
}
return true;
}
/**
* add the schema
* @param location the schema location.
* @throws BuildException if there is no namespace, or if there already
* is a declaration of this schema with a different value
*/
public void addConfiguredSchema(SchemaLocation location) {
log("adding schema " + location, Project.MSG_DEBUG);
location.validateNamespace();
SchemaLocation old = schemaLocations.get(location.getNamespace());
if (old != null && !old.equals(location)) {
throw new BuildException(ERROR_DUPLICATE_SCHEMA + location);
}
schemaLocations.put(location.getNamespace(), location);
}
/**
* enable full schema checking. Slower but better.
* @param fullChecking a <code>boolean</code> value.
*/
public void setFullChecking(boolean fullChecking) {
this.fullChecking = fullChecking;
}
/**
* create a schema location to hold the anonymous
* schema
*/
protected void createAnonymousSchema() {
if (anonymousSchema == null) {
anonymousSchema = new SchemaLocation();
}
anonymousSchema.setNamespace("(no namespace)");
}
/**
* identify the URL of the default schema
* @param defaultSchemaURL the URL of the default schema.
*/
public void setNoNamespaceURL(String defaultSchemaURL) {
createAnonymousSchema();
this.anonymousSchema.setUrl(defaultSchemaURL);
}
/**
* identify a file containing the default schema
* @param defaultSchemaFile the location of the default schema.
*/
public void setNoNamespaceFile(File defaultSchemaFile) {
createAnonymousSchema();
this.anonymousSchema.setFile(defaultSchemaFile);
}
/**
* flag to disable DTD support.
* @param disableDTD a <code>boolean</code> value.
*/
public void setDisableDTD(boolean disableDTD) {
this.disableDTD = disableDTD;
}
/**
* 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
*/
@Override
protected void initValidator() {
super.initValidator();
//validate the parser type
if (isSax1Parser()) {
throw new BuildException(ERROR_SAX_1);
}
//enable schema
setFeature(XmlConstants.FEATURE_NAMESPACES, true);
if (!enableXercesSchemaValidation() && !enableJAXP12SchemaValidation()) {
//could not use xerces or jaxp calls
throw new BuildException(ERROR_NO_XSD_SUPPORT);
}
//enable schema checking
setFeature(XmlConstants.FEATURE_XSD_FULL_VALIDATION, fullChecking);
//turn off DTDs if desired
setFeatureIfSupported(XmlConstants.FEATURE_DISALLOW_DTD, disableDTD);
//schema declarations go in next
addSchemaLocations();
}
/**
* Create a reader if the use of the class did not specify another one.
* The reason to not use {@link org.apache.tools.ant.util.JAXPUtils#getXMLReader()} was to
* create our own factory with our own options.
* @return a default XML parser
*/
@Override
protected XMLReader createDefaultReader() {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setValidating(true);
factory.setNamespaceAware(true);
XMLReader reader = null;
try {
SAXParser saxParser = factory.newSAXParser();
reader = saxParser.getXMLReader();
} catch (ParserConfigurationException | SAXException e) {
throw new BuildException(ERROR_PARSER_CREATION_FAILURE, e);
}
return reader;
}
/**
* build a string list of all schema locations, then set the relevant
* property.
*/
protected void addSchemaLocations() {
if (!schemaLocations.isEmpty()) {
String joinedValue = schemaLocations.values().stream()
.map(SchemaLocation::getURIandLocation)
.peek(tuple -> log("Adding schema " + tuple, Project.MSG_VERBOSE))
.collect(Collectors.joining(" "));
setProperty(XmlConstants.PROPERTY_SCHEMA_LOCATION, joinedValue);
}
}
/**
* get the URL of the no namespace schema
* @return the schema URL
*/
protected String getNoNamespaceSchemaURL() {
return anonymousSchema == null ? null
: anonymousSchema.getSchemaLocationURL();
}
/**
* set a feature if it is supported, log at verbose level if
* not
* @param feature the feature.
* @param value a <code>boolean</code> value.
*/
protected void setFeatureIfSupported(String feature, boolean value) {
try {
getXmlReader().setFeature(feature, value);
} catch (SAXNotRecognizedException e) {
log("Not recognized: " + feature, Project.MSG_VERBOSE);
} catch (SAXNotSupportedException e) {
log("Not supported: " + feature, Project.MSG_VERBOSE);
}
}
/**
* handler called on successful file validation.
*
* @param fileProcessed number of files processed.
*/
@Override
protected void onSuccessfulValidation(int fileProcessed) {
log(fileProcessed + MESSAGE_FILES_VALIDATED, Project.MSG_VERBOSE);
}
/**
* representation of a schema location. This is a URI plus either a file or
* a url
*/
public static class SchemaLocation {
private String namespace;
private File file;
private String url;
/** No namespace URI */
public static final String ERROR_NO_URI = "No namespace URI";
/** Both URL and File were given for schema */
public static final String ERROR_TWO_LOCATIONS
= "Both URL and File were given for schema ";
/** File not found */
public static final String ERROR_NO_FILE = "File not found: ";
/** Cannot make URL */
public static final String ERROR_NO_URL_REPRESENTATION
= "Cannot make a URL of ";
/** No location provided */
public static final String ERROR_NO_LOCATION
= "No file or URL supplied for the schema ";
/**
* Get the namespace.
* @return the namespace.
*/
public String getNamespace() {
return namespace;
}
/**
* set the namespace of this schema. Any URI
* @param namespace the namespace to use.
*/
public void setNamespace(String namespace) {
this.namespace = namespace;
}
/**
* Get the file.
* @return the file containing the schema.
*/
public File getFile() {
return file;
}
/**
* identify a file that contains this namespace's schema.
* The file must exist.
* @param file the file contains the schema.
*/
public void setFile(File file) {
this.file = file;
}
/**
* The URL containing the schema.
* @return the URL string.
*/
public String getUrl() {
return url;
}
/**
* identify a URL that hosts the schema.
* @param url the URL string.
*/
public void setUrl(String url) {
this.url = url;
}
/**
* get the URL of the schema
* @return a URL to the schema
* @throws BuildException if not
*/
public String getSchemaLocationURL() {
boolean hasFile = file != null;
boolean hasURL = isSet(url);
//error if both are empty, or both are set
if (!hasFile && !hasURL) {
throw new BuildException(ERROR_NO_LOCATION + namespace);
}
if (hasFile && hasURL) {
throw new BuildException(ERROR_TWO_LOCATIONS + namespace);
}
String schema = url;
if (hasFile) {
if (!file.exists()) {
throw new BuildException(ERROR_NO_FILE + file);
}
try {
schema = FileUtils.getFileUtils().getFileURL(file).toString();
} catch (MalformedURLException e) {
//this is almost implausible, but required handling
throw new BuildException(ERROR_NO_URL_REPRESENTATION + file, e);
}
}
return schema;
}
/**
* validate the fields then create a "uri location" string
*
* @return string of uri and location
* @throws BuildException if there is an error.
*/
public String getURIandLocation() throws BuildException {
validateNamespace();
return namespace + ' ' + getSchemaLocationURL();
}
/**
* assert that a namespace is valid
* @throws BuildException if not
*/
public void validateNamespace() {
if (!isSet(getNamespace())) {
throw new BuildException(ERROR_NO_URI);
}
}
/**
* check that a property is set
* @param property string to check
* @return true if it is not null or empty
*/
private boolean isSet(String property) {
return property != null && !property.isEmpty();
}
/**
* equality test checks namespace, location and filename. All must match,
* @param o object to compare against
* @return true iff the objects are considered equal in value
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof SchemaLocation)) {
return false;
}
final SchemaLocation schemaLocation = (SchemaLocation) o;
return (file == null ? schemaLocation.file == null : file.equals(schemaLocation.file))
&& (namespace == null ? schemaLocation.namespace == null : namespace.equals(schemaLocation.namespace))
&& (url == null ? schemaLocation.url == null : url.equals(schemaLocation.url));
}
/**
* Generate a hashcode depending on the namespace, url and file name.
* @return the hashcode.
*/
@Override
public int hashCode() {
int result;
// CheckStyle:MagicNumber OFF
result = (namespace == null ? 0 : namespace.hashCode());
result = 29 * result + (file == null ? 0 : file.hashCode());
result = 29 * result + (url == null ? 0 : url.hashCode());
// CheckStyle:MagicNumber OFF
return result;
}
/**
* Returns a string representation of the object for error messages
* and the like
* @return a string representation of the object.
*/
@Override
public String toString() {
return (namespace == null ? "(anonymous)" : namespace)
+ (url == null ? "" : " " + url)
+ (file == null ? "" : " " + file.getAbsolutePath());
}
}
}