| /* |
| * 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()); |
| } |
| } |
| } |