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 java.io.File;
021: import java.io.FileInputStream;
022: import java.io.IOException;
023: import java.util.Vector;
024:
025: import org.apache.tools.ant.AntClassLoader;
026: import org.apache.tools.ant.BuildException;
027: import org.apache.tools.ant.DirectoryScanner;
028: import org.apache.tools.ant.Project;
029: import org.apache.tools.ant.Task;
030: import org.apache.tools.ant.types.DTDLocation;
031: import org.apache.tools.ant.types.FileSet;
032: import org.apache.tools.ant.types.Path;
033: import org.apache.tools.ant.types.Reference;
034: import org.apache.tools.ant.types.XMLCatalog;
035: import org.apache.tools.ant.util.FileUtils;
036: import org.apache.tools.ant.util.JAXPUtils;
037: import org.apache.tools.ant.util.XmlConstants;
038:
039: import org.xml.sax.EntityResolver;
040: import org.xml.sax.ErrorHandler;
041: import org.xml.sax.InputSource;
042: import org.xml.sax.Parser;
043: import org.xml.sax.SAXException;
044: import org.xml.sax.SAXNotRecognizedException;
045: import org.xml.sax.SAXNotSupportedException;
046: import org.xml.sax.SAXParseException;
047: import org.xml.sax.XMLReader;
048: import org.xml.sax.helpers.ParserAdapter;
049:
050: /**
051: * Checks XML files are valid (or only well formed). The
052: * task uses the SAX2 parser implementation provided by JAXP by default
053: * (probably the one that is used by Ant itself), but one can specify any
054: * SAX1/2 parser if needed.
055: *
056: */
057: public class XMLValidateTask extends Task {
058:
059: /**
060: * helper for path -> URI and URI -> path conversions.
061: */
062: private static final FileUtils FILE_UTILS = FileUtils
063: .getFileUtils();
064:
065: protected static final String INIT_FAILED_MSG = "Could not start xml validation: ";
066:
067: // ant task properties
068: // defaults
069: // CheckStyle:VisibilityModifier OFF - bc
070: protected boolean failOnError = true;
071: protected boolean warn = true;
072: protected boolean lenient = false;
073: protected String readerClassName = null;
074:
075: /** file to be validated */
076: protected File file = null;
077: /** sets of file to be validated */
078: protected Vector filesets = new Vector();
079: protected Path classpath;
080:
081: /**
082: * the parser is viewed as a SAX2 XMLReader. If a SAX1 parser is specified,
083: * it's wrapped in an adapter that make it behave as a XMLReader.
084: * a more 'standard' way of doing this would be to use the JAXP1.1 SAXParser
085: * interface.
086: */
087: protected XMLReader xmlReader = null;
088: // XMLReader used to validation process
089: protected ValidatorErrorHandler errorHandler = new ValidatorErrorHandler();
090: // to report sax parsing errors
091: // CheckStyle:VisibilityModifier ON
092:
093: /** The vector to store all attributes (features) to be set on the parser. **/
094: private Vector attributeList = new Vector();
095:
096: /**
097: * List of properties.
098: */
099: private final Vector propertyList = new Vector();
100:
101: private XMLCatalog xmlCatalog = new XMLCatalog();
102: /** Message for sucessfull validation */
103: public static final String MESSAGE_FILES_VALIDATED = " file(s) have been successfully validated.";
104:
105: /**
106: * Specify how parser error are to be handled.
107: * Optional, default is <code>true</code>.
108: * <p>
109: * If set to <code>true</code> (default), throw a buildException if the
110: * parser yields an error.
111: * @param fail if set to <code>false</code> do not fail on error
112: */
113: public void setFailOnError(boolean fail) {
114: failOnError = fail;
115: }
116:
117: /**
118: * Specify how parser error are to be handled.
119: * <p>
120: * If set to <code>true</true> (default), log a warn message for each SAX warn event.
121: * @param bool if set to <code>false</code> do not send warnings
122: */
123: public void setWarn(boolean bool) {
124: warn = bool;
125: }
126:
127: /**
128: * Specify whether the parser should be validating. Default
129: * is <code>true</code>.
130: * <p>
131: * If set to false, the validation will fail only if the parsed document
132: * is not well formed XML.
133: * <p>
134: * this option is ignored if the specified class
135: * with {@link #setClassName(String)} is not a SAX2 XMLReader.
136: * @param bool if set to <code>false</code> only fail on malformed XML
137: */
138: public void setLenient(boolean bool) {
139: lenient = bool;
140: }
141:
142: /**
143: * Specify the class name of the SAX parser to be used. (optional)
144: * @param className should be an implementation of SAX2
145: * <code>org.xml.sax.XMLReader</code> or SAX2 <code>org.xml.sax.Parser</code>.
146: * <p> if className is an implementation of
147: * <code>org.xml.sax.Parser</code>, {@link #setLenient(boolean)},
148: * will be ignored.
149: * <p> if not set, the default will be used.
150: * @see org.xml.sax.XMLReader
151: * @see org.xml.sax.Parser
152: */
153: public void setClassName(String className) {
154: readerClassName = className;
155: }
156:
157: /**
158: * Specify the classpath to be searched to load the parser (optional)
159: * @param classpath the classpath to load the parser
160: */
161: public void setClasspath(Path classpath) {
162: if (this .classpath == null) {
163: this .classpath = classpath;
164: } else {
165: this .classpath.append(classpath);
166: }
167: }
168:
169: /**
170: * @see #setClasspath
171: * @return the classpath created
172: */
173: public Path createClasspath() {
174: if (this .classpath == null) {
175: this .classpath = new Path(getProject());
176: }
177: return this .classpath.createPath();
178: }
179:
180: /**
181: * Where to find the parser class; optional.
182: * @see #setClasspath
183: * @param r reference to a classpath defined elsewhere
184: */
185: public void setClasspathRef(Reference r) {
186: createClasspath().setRefid(r);
187: }
188:
189: /**
190: * specify the file to be checked; optional.
191: * @param file the file to be checked
192: */
193: public void setFile(File file) {
194: this .file = file;
195: }
196:
197: /**
198: * add an XMLCatalog as a nested element; optional.
199: * @param catalog XMLCatalog to use
200: */
201: public void addConfiguredXMLCatalog(XMLCatalog catalog) {
202: xmlCatalog.addConfiguredXMLCatalog(catalog);
203: }
204:
205: /**
206: * specify a set of file to be checked
207: * @param set the fileset to check
208: */
209: public void addFileset(FileSet set) {
210: filesets.addElement(set);
211: }
212:
213: /**
214: * Add an attribute nested element. This is used for setting arbitrary
215: * features of the SAX parser.
216: * Valid attributes
217: * <a href=
218: * "http://www.saxproject.org/apidoc/org/xml/sax/package-summary.html#package_description"
219: * >include</a>
220: * @return attribute created
221: * @since ant1.6
222: */
223: public Attribute createAttribute() {
224: final Attribute feature = new Attribute();
225: attributeList.addElement(feature);
226: return feature;
227: }
228:
229: /**
230: * Creates a property.
231: *
232: * @return a property.
233: * @since ant 1.6.2
234: */
235: public Property createProperty() {
236: final Property prop = new Property();
237: propertyList.addElement(prop);
238: return prop;
239: }
240:
241: /**
242: * Called by the project to let the task initialize properly.
243: *
244: * @exception BuildException if something goes wrong with the build
245: */
246: public void init() throws BuildException {
247: super .init();
248: xmlCatalog.setProject(getProject());
249: }
250:
251: /**
252: * Create a DTD location record; optional.
253: * This stores the location of a DTD. The DTD is identified
254: * by its public Id.
255: * @return created DTD location
256: */
257: public DTDLocation createDTD() {
258: DTDLocation dtdLocation = new DTDLocation();
259: xmlCatalog.addDTD(dtdLocation);
260: return dtdLocation;
261: }
262:
263: /**
264: * accessor to the xmlCatalog used in the task
265: * @return xmlCatalog reference
266: */
267: protected EntityResolver getEntityResolver() {
268: return xmlCatalog;
269: }
270:
271: /**
272: * get the XML reader. Non-null only after {@link #initValidator()}.
273: * If the reader is an instance of {@link ParserAdapter} then
274: * the parser is a SAX1 parser, and you cannot call
275: * {@link #setFeature(String, boolean)} or {@link #setProperty(String, String)}
276: * on it.
277: * @return the XML reader or null.
278: */
279: protected XMLReader getXmlReader() {
280: return xmlReader;
281: }
282:
283: /**
284: * execute the task
285: * @throws BuildException if <code>failonerror</code> is true and an error happens
286: */
287: public void execute() throws BuildException {
288:
289: int fileProcessed = 0;
290: if (file == null && (filesets.size() == 0)) {
291: throw new BuildException("Specify at least one source - "
292: + "a file or a fileset.");
293: }
294:
295: if (file != null) {
296: if (file.exists() && file.canRead() && file.isFile()) {
297: doValidate(file);
298: fileProcessed++;
299: } else {
300: String errorMsg = "File " + file + " cannot be read";
301: if (failOnError) {
302: throw new BuildException(errorMsg);
303: } else {
304: log(errorMsg, Project.MSG_ERR);
305: }
306: }
307: }
308:
309: for (int i = 0; i < filesets.size(); i++) {
310:
311: FileSet fs = (FileSet) filesets.elementAt(i);
312: DirectoryScanner ds = fs.getDirectoryScanner(getProject());
313: String[] files = ds.getIncludedFiles();
314:
315: for (int j = 0; j < files.length; j++) {
316: File srcFile = new File(fs.getDir(getProject()),
317: files[j]);
318: doValidate(srcFile);
319: fileProcessed++;
320: }
321: }
322: onSuccessfulValidation(fileProcessed);
323: }
324:
325: /**
326: * handler called on successful file validation.
327: * @param fileProcessed number of files processed.
328: */
329: protected void onSuccessfulValidation(int fileProcessed) {
330: log(fileProcessed + MESSAGE_FILES_VALIDATED);
331: }
332:
333: /**
334: * init the parser :
335: * load the parser class, and set features if necessary
336: * It is only after this that the reader is valid
337: * @throws BuildException if something went wrong
338: */
339: protected void initValidator() {
340:
341: xmlReader = createXmlReader();
342:
343: xmlReader.setEntityResolver(getEntityResolver());
344: xmlReader.setErrorHandler(errorHandler);
345:
346: if (!isSax1Parser()) {
347: // turn validation on
348: if (!lenient) {
349: setFeature(XmlConstants.FEATURE_VALIDATION, true);
350: }
351: // set the feature from the attribute list
352: for (int i = 0; i < attributeList.size(); i++) {
353: Attribute feature = (Attribute) attributeList
354: .elementAt(i);
355: setFeature(feature.getName(), feature.getValue());
356:
357: }
358: // Sets properties
359: for (int i = 0; i < propertyList.size(); i++) {
360: final Property prop = (Property) propertyList
361: .elementAt(i);
362: setProperty(prop.getName(), prop.getValue());
363: }
364: }
365: }
366:
367: /**
368: * test that returns true if we are using a SAX1 parser.
369: * @return true when a SAX1 parser is in use
370: */
371: protected boolean isSax1Parser() {
372: return (xmlReader instanceof ParserAdapter);
373: }
374:
375: /**
376: * create the XML reader.
377: * This is one by instantiating anything specified by {@link #readerClassName},
378: * falling back to a default reader if not.
379: * If the returned reader is an instance of {@link ParserAdapter} then
380: * we have created and wrapped a SAX1 parser.
381: * @return the new XMLReader.
382: */
383: protected XMLReader createXmlReader() {
384: Object reader = null;
385: if (readerClassName == null) {
386: reader = createDefaultReaderOrParser();
387: } else {
388:
389: Class readerClass = null;
390: try {
391: // load the parser class
392: if (classpath != null) {
393: AntClassLoader loader = getProject()
394: .createClassLoader(classpath);
395: readerClass = Class.forName(readerClassName, true,
396: loader);
397: } else {
398: readerClass = Class.forName(readerClassName);
399: }
400:
401: reader = readerClass.newInstance();
402: } catch (ClassNotFoundException e) {
403: throw new BuildException(INIT_FAILED_MSG
404: + readerClassName, e);
405: } catch (InstantiationException e) {
406: throw new BuildException(INIT_FAILED_MSG
407: + readerClassName, e);
408: } catch (IllegalAccessException e) {
409: throw new BuildException(INIT_FAILED_MSG
410: + readerClassName, e);
411: }
412: }
413:
414: // then check it implements XMLReader
415: XMLReader newReader;
416: if (reader instanceof XMLReader) {
417: newReader = (XMLReader) reader;
418: log("Using SAX2 reader " + reader.getClass().getName(),
419: Project.MSG_VERBOSE);
420: } else {
421:
422: // see if it is a SAX1 Parser
423: if (reader instanceof Parser) {
424: newReader = new ParserAdapter((Parser) reader);
425: log("Using SAX1 parser " + reader.getClass().getName(),
426: Project.MSG_VERBOSE);
427: } else {
428: throw new BuildException(
429: INIT_FAILED_MSG
430: + reader.getClass().getName()
431: + " implements nor SAX1 Parser nor SAX2 XMLReader.");
432: }
433: }
434: return newReader;
435: }
436:
437: /**
438: *
439: * @return
440: */
441: private Object createDefaultReaderOrParser() {
442: Object reader;
443: try {
444: reader = createDefaultReader();
445: } catch (BuildException exc) {
446: reader = JAXPUtils.getParser();
447: }
448: return reader;
449: }
450:
451: /**
452: * create a reader if the use of the class did not specify another one.
453: * If a BuildException is thrown, the caller may revert to an alternate
454: * reader.
455: * @return a new reader.
456: * @throws BuildException if something went wrong
457: */
458: protected XMLReader createDefaultReader() {
459: return JAXPUtils.getXMLReader();
460: }
461:
462: /**
463: * Set a feature on the parser.
464: * @param feature the name of the feature to set
465: * @param value the value of the feature
466: * @throws BuildException if the feature was not supported
467: */
468: protected void setFeature(String feature, boolean value)
469: throws BuildException {
470: log("Setting feature " + feature + "=" + value,
471: Project.MSG_DEBUG);
472: try {
473: xmlReader.setFeature(feature, value);
474: } catch (SAXNotRecognizedException e) {
475: throw new BuildException("Parser "
476: + xmlReader.getClass().getName()
477: + " doesn't recognize feature " + feature, e,
478: getLocation());
479: } catch (SAXNotSupportedException e) {
480: throw new BuildException("Parser "
481: + xmlReader.getClass().getName()
482: + " doesn't support feature " + feature, e,
483: getLocation());
484: }
485: }
486:
487: /**
488: * Sets a property.
489: *
490: * @param name a property name
491: * @param value a property value.
492: * @throws BuildException if an error occurs.
493: * @throws BuildException if the property was not supported
494: */
495: protected void setProperty(String name, String value)
496: throws BuildException {
497: // Validates property
498: if (name == null || value == null) {
499: throw new BuildException(
500: "Property name and value must be specified.");
501: }
502:
503: try {
504: xmlReader.setProperty(name, value);
505: } catch (SAXNotRecognizedException e) {
506: throw new BuildException("Parser "
507: + xmlReader.getClass().getName()
508: + " doesn't recognize property " + name, e,
509: getLocation());
510: } catch (SAXNotSupportedException e) {
511: throw new BuildException("Parser "
512: + xmlReader.getClass().getName()
513: + " doesn't support property " + name, e,
514: getLocation());
515: }
516: }
517:
518: /**
519: * parse the file
520: * @param afile the file to validate.
521: * @return true if the file validates.
522: */
523: protected boolean doValidate(File afile) {
524: //for every file, we have a new instance of the validator
525: initValidator();
526: boolean result = true;
527: try {
528: log("Validating " + afile.getName() + "... ",
529: Project.MSG_VERBOSE);
530: errorHandler.init(afile);
531: InputSource is = new InputSource(new FileInputStream(afile));
532: String uri = FILE_UTILS.toURI(afile.getAbsolutePath());
533: is.setSystemId(uri);
534: xmlReader.parse(is);
535: } catch (SAXException ex) {
536: log("Caught when validating: " + ex.toString(),
537: Project.MSG_DEBUG);
538: if (failOnError) {
539: throw new BuildException("Could not validate document "
540: + afile);
541: }
542: log("Could not validate document " + afile + ": "
543: + ex.toString());
544: result = false;
545: } catch (IOException ex) {
546: throw new BuildException("Could not validate document "
547: + afile, ex);
548: }
549: if (errorHandler.getFailure()) {
550: if (failOnError) {
551: throw new BuildException(afile
552: + " is not a valid XML document.");
553: }
554: result = false;
555: log(afile + " is not a valid XML document", Project.MSG_ERR);
556: }
557: return result;
558: }
559:
560: /**
561: * ValidatorErrorHandler role :
562: * <ul>
563: * <li> log SAX parse exceptions,
564: * <li> remember if an error occurred
565: * </ul>
566: */
567: protected class ValidatorErrorHandler implements ErrorHandler {
568:
569: // CheckStyle:VisibilityModifier OFF - bc
570: protected File currentFile = null;
571: protected String lastErrorMessage = null;
572: protected boolean failed = false;
573:
574: // CheckStyle:VisibilityModifier ON
575: /**
576: * initialises the class
577: * @param file file used
578: */
579: public void init(File file) {
580: currentFile = file;
581: failed = false;
582: }
583:
584: /**
585: * did an error happen during last parsing ?
586: * @return did an error happen during last parsing ?
587: */
588: public boolean getFailure() {
589: return failed;
590: }
591:
592: /**
593: * record a fatal error
594: * @param exception the fatal error
595: */
596: public void fatalError(SAXParseException exception) {
597: failed = true;
598: doLog(exception, Project.MSG_ERR);
599: }
600:
601: /**
602: * receive notification of a recoverable error
603: * @param exception the error
604: */
605: public void error(SAXParseException exception) {
606: failed = true;
607: doLog(exception, Project.MSG_ERR);
608: }
609:
610: /**
611: * receive notification of a warning
612: * @param exception the warning
613: */
614: public void warning(SAXParseException exception) {
615: // depending on implementation, XMLReader can yield hips of warning,
616: // only output then if user explicitly asked for it
617: if (warn) {
618: doLog(exception, Project.MSG_WARN);
619: }
620: }
621:
622: private void doLog(SAXParseException e, int logLevel) {
623:
624: log(getMessage(e), logLevel);
625: }
626:
627: private String getMessage(SAXParseException e) {
628: String sysID = e.getSystemId();
629: if (sysID != null) {
630: String name = sysID;
631: if (sysID.startsWith("file:")) {
632: try {
633: name = FILE_UTILS.fromURI(sysID);
634: } catch (Exception ex) {
635: // if this is not a valid file: just use the uri
636: }
637: }
638: int line = e.getLineNumber();
639: int col = e.getColumnNumber();
640: return name
641: + (line == -1 ? ""
642: : (":" + line + (col == -1 ? ""
643: : (":" + col)))) + ": "
644: + e.getMessage();
645: }
646: return e.getMessage();
647: }
648: }
649:
650: /**
651: * The class to create to set a feature of the parser.
652: * @since ant1.6
653: */
654: public static class Attribute {
655: /** The name of the attribute to set.
656: *
657: * Valid attributes <a href=
658: * "http://www.saxproject.org/apidoc/org/xml/sax/package-summary.html#package_description"
659: * >include.</a>
660: */
661: private String attributeName = null;
662:
663: /**
664: * The value of the feature.
665: **/
666: private boolean attributeValue;
667:
668: /**
669: * Set the feature name.
670: * @param name the name to set
671: */
672: public void setName(String name) {
673: attributeName = name;
674: }
675:
676: /**
677: * Set the feature value to true or false.
678: * @param value feature value
679: */
680: public void setValue(boolean value) {
681: attributeValue = value;
682: }
683:
684: /**
685: * Gets the attribute name.
686: * @return the feature name
687: */
688: public String getName() {
689: return attributeName;
690: }
691:
692: /**
693: * Gets the attribute value.
694: * @return the feature value
695: */
696: public boolean getValue() {
697: return attributeValue;
698: }
699: }
700:
701: /**
702: * A Parser property.
703: * See <a href="http://xml.apache.org/xerces-j/properties.html">
704: * XML parser properties</a> for usable properties
705: * @since ant 1.6.2
706: */
707: public static final class Property {
708:
709: private String name;
710: private String value;
711:
712: /**
713: * accessor to the name of the property
714: * @return name of the property
715: */
716: public String getName() {
717: return name;
718: }
719:
720: /**
721: * setter for the name of the property
722: * @param name name of the property
723: */
724: public void setName(String name) {
725: this .name = name;
726: }
727:
728: /**
729: * getter for the value of the property
730: * @return value of the property
731: */
732: public String getValue() {
733: return value;
734: }
735:
736: /**
737: * sets the value of the property
738: * @param value value of the property
739: */
740: public void setValue(String value) {
741: this .value = value;
742: }
743:
744: } // Property
745:
746: }
|