001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018: package org.apache.tools.ant.taskdefs.optional;
019:
020: import org.apache.tools.ant.BuildException;
021: import org.apache.tools.ant.Project;
022: import org.apache.tools.ant.util.FileUtils;
023: import org.apache.tools.ant.util.XmlConstants;
024: import org.xml.sax.XMLReader;
025: import org.xml.sax.SAXNotRecognizedException;
026: import org.xml.sax.SAXNotSupportedException;
027: import org.xml.sax.SAXException;
028:
029: import javax.xml.parsers.SAXParserFactory;
030: import javax.xml.parsers.SAXParser;
031: import javax.xml.parsers.ParserConfigurationException;
032: import java.util.Iterator;
033: import java.util.HashMap;
034: import java.io.File;
035: import java.net.MalformedURLException;
036:
037: /**
038: * Validate XML Schema documents.
039: * This task validates XML schema documents. It requires an XML parser
040: * that handles the relevant SAx, Xerces or JAXP options.
041: *
042: * To resolve remote referencies, Ant may need its proxy set up, using the
043: * setproxy task.
044: *
045: * Hands off most of the work to its parent, {@link XMLValidateTask}
046: * @since Ant1.7
047: */
048:
049: public class SchemaValidate extends XMLValidateTask {
050:
051: /** map of all declared schemas; we catch and complain about redefinitions */
052: private HashMap schemaLocations = new HashMap();
053:
054: /** full checking of a schema */
055: private boolean fullChecking = true;
056:
057: /**
058: * flag to disable DTD support. Best left enabled.
059: */
060: private boolean disableDTD = false;
061:
062: /**
063: * default URL for nonamespace schemas
064: */
065: private SchemaLocation anonymousSchema;
066:
067: // Error strings
068: /** SAX1 not supported */
069: public static final String ERROR_SAX_1 = "SAX1 parsers are not supported";
070:
071: /** schema features not supported */
072: public static final String ERROR_NO_XSD_SUPPORT = "Parser does not support Xerces or JAXP schema features";
073:
074: /** too many default schemas */
075: public static final String ERROR_TOO_MANY_DEFAULT_SCHEMAS = "Only one of defaultSchemaFile and defaultSchemaURL allowed";
076:
077: /** unable to create parser */
078: public static final String ERROR_PARSER_CREATION_FAILURE = "Could not create parser";
079:
080: /** adding schema */
081: public static final String MESSAGE_ADDING_SCHEMA = "Adding schema ";
082:
083: /** Duplicate declaration of schema */
084: public static final String ERROR_DUPLICATE_SCHEMA = "Duplicate declaration of schema ";
085:
086: /**
087: * Called by the project to let the task initialize properly. The default
088: * implementation is a no-op.
089: *
090: * @throws BuildException if something goes wrong with the build
091: */
092: public void init() throws BuildException {
093: super .init();
094: //validating
095: setLenient(false);
096: }
097:
098: /**
099: * Turn on XSD support in Xerces.
100: * @return true on success, false on failure
101: */
102: public boolean enableXercesSchemaValidation() {
103: try {
104: setFeature(XmlConstants.FEATURE_XSD, true);
105: //set the schema source for the doc
106: setNoNamespaceSchemaProperty(XmlConstants.PROPERTY_NO_NAMESPACE_SCHEMA_LOCATION);
107: } catch (BuildException e) {
108: log(e.toString(), Project.MSG_VERBOSE);
109: return false;
110: }
111: return true;
112: }
113:
114: /**
115: * set nonamespace handling up for xerces or other parsers
116: * @param property name of the property to set
117: */
118: private void setNoNamespaceSchemaProperty(String property) {
119: String anonSchema = getNoNamespaceSchemaURL();
120: if (anonSchema != null) {
121: setProperty(property, anonSchema);
122: }
123: }
124:
125: /**
126: * Set schema attributes in a JAXP 1.2 engine.
127: * @see <A href="http://java.sun.com/xml/jaxp/change-requests-11.html">
128: * JAXP 1.2 Approved CHANGES</A>
129: * @return true on success, false on failure
130: */
131: public boolean enableJAXP12SchemaValidation() {
132: try {
133: //enable XSD
134: setProperty(XmlConstants.FEATURE_JAXP12_SCHEMA_LANGUAGE,
135: XmlConstants.URI_XSD);
136: //set the schema source for the doc
137: setNoNamespaceSchemaProperty(XmlConstants.FEATURE_JAXP12_SCHEMA_SOURCE);
138: } catch (BuildException e) {
139: log(e.toString(), Project.MSG_VERBOSE);
140: return false;
141: }
142: return true;
143: }
144:
145: /**
146: * add the schema
147: * @param location the schema location.
148: * @throws BuildException if there is no namespace, or if there already
149: * is a declaration of this schema with a different value
150: */
151: public void addConfiguredSchema(SchemaLocation location) {
152: log("adding schema " + location, Project.MSG_DEBUG);
153: location.validateNamespace();
154: SchemaLocation old = (SchemaLocation) schemaLocations
155: .get(location.getNamespace());
156: if (old != null && !old.equals(location)) {
157: throw new BuildException(ERROR_DUPLICATE_SCHEMA + location);
158: }
159: schemaLocations.put(location.getNamespace(), location);
160: }
161:
162: /**
163: * enable full schema checking. Slower but better.
164: * @param fullChecking a <code>boolean</code> value.
165: */
166: public void setFullChecking(boolean fullChecking) {
167: this .fullChecking = fullChecking;
168: }
169:
170: /**
171: * create a schema location to hold the anonymous
172: * schema
173: */
174: protected void createAnonymousSchema() {
175: if (anonymousSchema == null) {
176: anonymousSchema = new SchemaLocation();
177: }
178: anonymousSchema.setNamespace("(no namespace)");
179: }
180:
181: /**
182: * identify the URL of the default schema
183: * @param defaultSchemaURL the URL of the default schema.
184: */
185: public void setNoNamespaceURL(String defaultSchemaURL) {
186: createAnonymousSchema();
187: this .anonymousSchema.setUrl(defaultSchemaURL);
188: }
189:
190: /**
191: * identify a file containing the default schema
192: * @param defaultSchemaFile the location of the default schema.
193: */
194: public void setNoNamespaceFile(File defaultSchemaFile) {
195: createAnonymousSchema();
196: this .anonymousSchema.setFile(defaultSchemaFile);
197: }
198:
199: /**
200: * flag to disable DTD support.
201: * @param disableDTD a <code>boolean</code> value.
202: */
203: public void setDisableDTD(boolean disableDTD) {
204: this .disableDTD = disableDTD;
205: }
206:
207: /**
208: * init the parser : load the parser class, and set features if necessary It
209: * is only after this that the reader is valid
210: *
211: * @throws BuildException if something went wrong
212: */
213: protected void initValidator() {
214: super .initValidator();
215: //validate the parser type
216: if (isSax1Parser()) {
217: throw new BuildException(ERROR_SAX_1);
218: }
219:
220: //enable schema
221: //setFeature(XmlConstants.FEATURE_VALIDATION, false);
222: setFeature(XmlConstants.FEATURE_NAMESPACES, true);
223: if (!enableXercesSchemaValidation()
224: && !enableJAXP12SchemaValidation()) {
225: //couldnt use the xerces or jaxp calls
226: throw new BuildException(ERROR_NO_XSD_SUPPORT);
227: }
228:
229: //enable schema checking
230: setFeature(XmlConstants.FEATURE_XSD_FULL_VALIDATION,
231: fullChecking);
232:
233: //turn off DTDs if desired
234: setFeatureIfSupported(XmlConstants.FEATURE_DISALLOW_DTD,
235: disableDTD);
236:
237: //schema declarations go in next
238: addSchemaLocations();
239: }
240:
241: /**
242: * Create a reader if the use of the class did not specify another one.
243: * The reason to not use {@link JAXPUtils#getXMLReader()} was to
244: * create our own factory with our own options.
245: * @return a default XML parser
246: */
247: protected XMLReader createDefaultReader() {
248: SAXParserFactory factory = SAXParserFactory.newInstance();
249: factory.setValidating(true);
250: factory.setNamespaceAware(true);
251: XMLReader reader = null;
252: try {
253: SAXParser saxParser = factory.newSAXParser();
254: reader = saxParser.getXMLReader();
255: } catch (ParserConfigurationException e) {
256: throw new BuildException(ERROR_PARSER_CREATION_FAILURE, e);
257: } catch (SAXException e) {
258: throw new BuildException(ERROR_PARSER_CREATION_FAILURE, e);
259: }
260: return reader;
261: }
262:
263: /**
264: * build a string list of all schema locations, then set the relevant
265: * property.
266: */
267: protected void addSchemaLocations() {
268: Iterator it = schemaLocations.values().iterator();
269: StringBuffer buffer = new StringBuffer();
270: int count = 0;
271: while (it.hasNext()) {
272: if (count > 0) {
273: buffer.append(' ');
274: }
275: SchemaLocation schemaLocation = (SchemaLocation) it.next();
276: String tuple = schemaLocation.getURIandLocation();
277: buffer.append(tuple);
278: log("Adding schema " + tuple, Project.MSG_VERBOSE);
279: count++;
280: }
281: if (count > 0) {
282: setProperty(XmlConstants.PROPERTY_SCHEMA_LOCATION, buffer
283: .toString());
284: }
285:
286: }
287:
288: /**
289: * get the URL of the no namespace schema
290: * @return the schema URL
291: */
292: protected String getNoNamespaceSchemaURL() {
293: if (anonymousSchema == null) {
294: return null;
295: } else {
296: return anonymousSchema.getSchemaLocationURL();
297: }
298: }
299:
300: /**
301: * set a feature if it is supported, log at verbose level if
302: * not
303: * @param feature the feature.
304: * @param value a <code>boolean</code> value.
305: */
306: protected void setFeatureIfSupported(String feature, boolean value) {
307: try {
308: getXmlReader().setFeature(feature, value);
309: } catch (SAXNotRecognizedException e) {
310: log("Not recognizied: " + feature, Project.MSG_VERBOSE);
311: } catch (SAXNotSupportedException e) {
312: log("Not supported: " + feature, Project.MSG_VERBOSE);
313: }
314: }
315:
316: /**
317: * handler called on successful file validation.
318: *
319: * @param fileProcessed number of files processed.
320: */
321: protected void onSuccessfulValidation(int fileProcessed) {
322: log(fileProcessed + MESSAGE_FILES_VALIDATED,
323: Project.MSG_VERBOSE);
324: }
325:
326: /**
327: * representation of a schema location. This is a URI plus either a file or
328: * a url
329: */
330: public static class SchemaLocation {
331: private String namespace;
332:
333: private File file;
334:
335: private String url;
336:
337: /** No namespace URI */
338: public static final String ERROR_NO_URI = "No namespace URI";
339:
340: /** Both URL and File were given for schema */
341: public static final String ERROR_TWO_LOCATIONS = "Both URL and File were given for schema ";
342:
343: /** File not found */
344: public static final String ERROR_NO_FILE = "File not found: ";
345:
346: /** Cannot make URL */
347: public static final String ERROR_NO_URL_REPRESENTATION = "Cannot make a URL of ";
348:
349: /** No location provided */
350: public static final String ERROR_NO_LOCATION = "No file or URL supplied for the schema ";
351:
352: /** No arg constructor */
353: public SchemaLocation() {
354: }
355:
356: /**
357: * Get the namespace.
358: * @return the namespace.
359: */
360: public String getNamespace() {
361: return namespace;
362: }
363:
364: /**
365: * set the namespace of this schema. Any URI
366: * @param namespace the namespace to use.
367: */
368: public void setNamespace(String namespace) {
369: this .namespace = namespace;
370: }
371:
372: /**
373: * Get the file.
374: * @return the file containing the schema.
375: */
376: public File getFile() {
377: return file;
378: }
379:
380: /**
381: * identify a file that contains this namespace's schema.
382: * The file must exist.
383: * @param file the file contains the schema.
384: */
385: public void setFile(File file) {
386: this .file = file;
387: }
388:
389: /**
390: * The URL containing the schema.
391: * @return the URL string.
392: */
393: public String getUrl() {
394: return url;
395: }
396:
397: /**
398: * identify a URL that hosts the schema.
399: * @param url the URL string.
400: */
401: public void setUrl(String url) {
402: this .url = url;
403: }
404:
405: /**
406: * get the URL of the schema
407: * @return a URL to the schema
408: * @throws BuildException if not
409: */
410: public String getSchemaLocationURL() {
411: boolean hasFile = file != null;
412: boolean hasURL = isSet(url);
413: //error if both are empty, or both are set
414: if (!hasFile && !hasURL) {
415: throw new BuildException(ERROR_NO_LOCATION + namespace);
416: }
417: if (hasFile && hasURL) {
418: throw new BuildException(ERROR_TWO_LOCATIONS
419: + namespace);
420: }
421: String schema = url;
422: if (hasFile) {
423: if (!file.exists()) {
424: throw new BuildException(ERROR_NO_FILE + file);
425: }
426:
427: try {
428: schema = FileUtils.getFileUtils().getFileURL(file)
429: .toString();
430: } catch (MalformedURLException e) {
431: //this is almost implausible, but required handling
432: throw new BuildException(
433: ERROR_NO_URL_REPRESENTATION + file, e);
434: }
435: }
436: return schema;
437: }
438:
439: /**
440: * validate the fields then create a "uri location" string
441: *
442: * @return string of uri and location
443: * @throws BuildException if there is an error.
444: */
445: public String getURIandLocation() throws BuildException {
446: validateNamespace();
447: StringBuffer buffer = new StringBuffer();
448: buffer.append(namespace);
449: buffer.append(' ');
450: buffer.append(getSchemaLocationURL());
451: return new String(buffer);
452: }
453:
454: /**
455: * assert that a namespace is valid
456: * @throws BuildException if not
457: */
458: public void validateNamespace() {
459: if (!isSet(getNamespace())) {
460: throw new BuildException(ERROR_NO_URI);
461: }
462: }
463:
464: /**
465: * check that a property is set
466: * @param property string to check
467: * @return true if it is not null or empty
468: */
469: private boolean isSet(String property) {
470: return property != null && property.length() != 0;
471: }
472:
473: /**
474: * equality test checks namespace, location and filename. All must match,
475: * @param o object to compare against
476: * @return true iff the objects are considered equal in value
477: */
478:
479: public boolean equals(Object o) {
480: if (this == o) {
481: return true;
482: }
483: if (!(o instanceof SchemaLocation)) {
484: return false;
485: }
486:
487: final SchemaLocation schemaLocation = (SchemaLocation) o;
488:
489: if (file != null ? !file.equals(schemaLocation.file)
490: : schemaLocation.file != null) {
491: return false;
492: }
493: if (namespace != null ? !namespace
494: .equals(schemaLocation.namespace)
495: : schemaLocation.namespace != null) {
496: return false;
497: }
498: if (url != null ? !url.equals(schemaLocation.url)
499: : schemaLocation.url != null) {
500: return false;
501: }
502:
503: return true;
504: }
505:
506: /**
507: * Generate a hashcode depending on the namespace, url and file name.
508: * @return the hashcode.
509: */
510: public int hashCode() {
511: int result;
512: result = (namespace != null ? namespace.hashCode() : 0);
513: result = 29 * result + (file != null ? file.hashCode() : 0);
514: result = 29 * result + (url != null ? url.hashCode() : 0);
515: return result;
516: }
517:
518: /**
519: * Returns a string representation of the object for error messages
520: * and the like
521: * @return a string representation of the object.
522: */
523: public String toString() {
524: StringBuffer buffer = new StringBuffer();
525: buffer
526: .append(namespace != null ? namespace
527: : "(anonymous)");
528: buffer.append(' ');
529: buffer.append(url != null ? (url + " ") : "");
530: buffer.append(file != null ? file.getAbsolutePath() : "");
531: return buffer.toString();
532: }
533: } //SchemaLocation
534: }
|