1. /*
  2. * Copyright 2000-2004 The Apache Software Foundation
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. *
  16. */
  17. package org.apache.tools.ant.taskdefs.optional;
  18. import java.io.File;
  19. import java.io.FileInputStream;
  20. import java.io.IOException;
  21. import java.net.MalformedURLException;
  22. import java.net.URL;
  23. import java.util.Vector;
  24. import org.apache.tools.ant.AntClassLoader;
  25. import org.apache.tools.ant.BuildException;
  26. import org.apache.tools.ant.DirectoryScanner;
  27. import org.apache.tools.ant.Project;
  28. import org.apache.tools.ant.Task;
  29. import org.apache.tools.ant.types.DTDLocation;
  30. import org.apache.tools.ant.types.FileSet;
  31. import org.apache.tools.ant.types.Path;
  32. import org.apache.tools.ant.types.Reference;
  33. import org.apache.tools.ant.types.XMLCatalog;
  34. import org.apache.tools.ant.util.FileUtils;
  35. import org.apache.tools.ant.util.JAXPUtils;
  36. import org.xml.sax.EntityResolver;
  37. import org.xml.sax.ErrorHandler;
  38. import org.xml.sax.InputSource;
  39. import org.xml.sax.Parser;
  40. import org.xml.sax.SAXException;
  41. import org.xml.sax.SAXNotRecognizedException;
  42. import org.xml.sax.SAXNotSupportedException;
  43. import org.xml.sax.SAXParseException;
  44. import org.xml.sax.XMLReader;
  45. import org.xml.sax.helpers.ParserAdapter;
  46. /**
  47. * Checks XML files are valid (or only well formed). The
  48. * task uses the SAX2 parser implementation provided by JAXP by default
  49. * (probably the one that is used by Ant itself), but one can specify any
  50. * SAX1/2 parser if needed.
  51. *
  52. */
  53. public class XMLValidateTask extends Task {
  54. /**
  55. * helper for path -> URI and URI -> path conversions.
  56. */
  57. private static FileUtils fu = FileUtils.newFileUtils();
  58. protected static final String INIT_FAILED_MSG =
  59. "Could not start xml validation: ";
  60. // ant task properties
  61. // defaults
  62. protected boolean failOnError = true;
  63. protected boolean warn = true;
  64. protected boolean lenient = false;
  65. protected String readerClassName = null;
  66. /** file to be validated */
  67. protected File file = null;
  68. /** sets of file to be validated */
  69. protected Vector filesets = new Vector();
  70. protected Path classpath;
  71. /**
  72. * the parser is viewed as a SAX2 XMLReader. If a SAX1 parser is specified,
  73. * it's wrapped in an adapter that make it behave as a XMLReader.
  74. * a more 'standard' way of doing this would be to use the JAXP1.1 SAXParser
  75. * interface.
  76. */
  77. protected XMLReader xmlReader = null;
  78. // XMLReader used to validation process
  79. protected ValidatorErrorHandler errorHandler = new ValidatorErrorHandler();
  80. // to report sax parsing errors
  81. /** The vector to store all attributes (features) to be set on the parser. **/
  82. private Vector attributeList = new Vector();
  83. /**
  84. * List of properties.
  85. */
  86. private final Vector propertyList = new Vector();
  87. private XMLCatalog xmlCatalog = new XMLCatalog();
  88. /**
  89. * Specify how parser error are to be handled.
  90. * Optional, default is <code>true</code>.
  91. * <p>
  92. * If set to <code>true</code> (default), throw a buildException if the
  93. * parser yields an error.
  94. * @param fail if set to <code>false</code> do not fail on error
  95. */
  96. public void setFailOnError(boolean fail) {
  97. failOnError = fail;
  98. }
  99. /**
  100. * Specify how parser error are to be handled.
  101. * <p>
  102. * If set to <code>true</true> (default), log a warn message for each SAX warn event.
  103. * @param bool if set to <code>false</code> do not send warnings
  104. */
  105. public void setWarn(boolean bool) {
  106. warn = bool;
  107. }
  108. /**
  109. * Specify whether the parser should be validating. Default
  110. * is <code>true</code>.
  111. * <p>
  112. * If set to false, the validation will fail only if the parsed document
  113. * is not well formed XML.
  114. * <p>
  115. * this option is ignored if the specified class
  116. * with {@link #setClassName(String)} is not a SAX2 XMLReader.
  117. * @param bool if set to <code>false</code> only fail on malformed XML
  118. */
  119. public void setLenient(boolean bool) {
  120. lenient = bool;
  121. }
  122. /**
  123. * Specify the class name of the SAX parser to be used. (optional)
  124. * @param className should be an implementation of SAX2
  125. * <code>org.xml.sax.XMLReader</code> or SAX2 <code>org.xml.sax.Parser</code>.
  126. * <p> if className is an implementation of
  127. * <code>org.xml.sax.Parser</code>, {@link #setLenient(boolean)},
  128. * will be ignored.
  129. * <p> if not set, the default will be used.
  130. * @see org.xml.sax.XMLReader
  131. * @see org.xml.sax.Parser
  132. */
  133. public void setClassName(String className) {
  134. readerClassName = className;
  135. }
  136. /**
  137. * Specify the classpath to be searched to load the parser (optional)
  138. * @param classpath the classpath to load the parser
  139. */
  140. public void setClasspath(Path classpath) {
  141. if (this.classpath == null) {
  142. this.classpath = classpath;
  143. } else {
  144. this.classpath.append(classpath);
  145. }
  146. }
  147. /**
  148. * @see #setClasspath
  149. * @return the classpath created
  150. */
  151. public Path createClasspath() {
  152. if (this.classpath == null) {
  153. this.classpath = new Path(getProject());
  154. }
  155. return this.classpath.createPath();
  156. }
  157. /**
  158. * Where to find the parser class; optional.
  159. * @see #setClasspath
  160. * @param r reference to a classpath defined elsewhere
  161. */
  162. public void setClasspathRef(Reference r) {
  163. createClasspath().setRefid(r);
  164. }
  165. /**
  166. * specify the file to be checked; optional.
  167. * @param file the file to be checked
  168. */
  169. public void setFile(File file) {
  170. this.file = file;
  171. }
  172. /**
  173. * add an XMLCatalog as a nested element; optional.
  174. * @param catalog XMLCatalog to use
  175. */
  176. public void addConfiguredXMLCatalog(XMLCatalog catalog) {
  177. xmlCatalog.addConfiguredXMLCatalog(catalog);
  178. }
  179. /**
  180. * specify a set of file to be checked
  181. * @param set the fileset to check
  182. */
  183. public void addFileset(FileSet set) {
  184. filesets.addElement(set);
  185. }
  186. /**
  187. * Add an attribute nested element. This is used for setting arbitrary
  188. * features of the SAX parser.
  189. * Valid attributes
  190. * <a href="http://www.saxproject.org/apidoc/org/xml/sax/package-summary.html#package_description">include</a>
  191. * @return attribute created
  192. * @since ant1.6
  193. */
  194. public Attribute createAttribute() {
  195. final Attribute feature = new Attribute();
  196. attributeList.addElement(feature);
  197. return feature;
  198. }
  199. /**
  200. * Creates a property.
  201. *
  202. * @return a property.
  203. * @since ant 1.6.2
  204. */
  205. public Property createProperty() {
  206. final Property prop = new Property();
  207. propertyList.addElement(prop);
  208. return prop;
  209. }
  210. /**
  211. * Called by the project to let the task initialize properly.
  212. *
  213. * @exception BuildException if something goes wrong with the build
  214. */
  215. public void init() throws BuildException {
  216. super.init();
  217. xmlCatalog.setProject(getProject());
  218. }
  219. /**
  220. * Create a DTD location record; optional.
  221. * This stores the location of a DTD. The DTD is identified
  222. * by its public Id.
  223. * @return created DTD location
  224. */
  225. public DTDLocation createDTD() {
  226. DTDLocation dtdLocation = new DTDLocation();
  227. xmlCatalog.addDTD(dtdLocation);
  228. return dtdLocation;
  229. }
  230. /**
  231. * accessor to the xmlCatalog used in the task
  232. * @return xmlCatalog reference
  233. */
  234. protected EntityResolver getEntityResolver() {
  235. return xmlCatalog;
  236. }
  237. /**
  238. * execute the task
  239. * @throws BuildException if <code>failonerror</code> is true and an error happens
  240. */
  241. public void execute() throws BuildException {
  242. int fileProcessed = 0;
  243. if (file == null && (filesets.size() == 0)) {
  244. throw new BuildException(
  245. "Specify at least one source - " + "a file or a fileset.");
  246. }
  247. initValidator();
  248. if (file != null) {
  249. if (file.exists() && file.canRead() && file.isFile()) {
  250. doValidate(file);
  251. fileProcessed++;
  252. } else {
  253. String errorMsg = "File " + file + " cannot be read";
  254. if (failOnError) {
  255. throw new BuildException(errorMsg);
  256. } else {
  257. log(errorMsg, Project.MSG_ERR);
  258. }
  259. }
  260. }
  261. for (int i = 0; i < filesets.size(); i++) {
  262. FileSet fs = (FileSet) filesets.elementAt(i);
  263. DirectoryScanner ds = fs.getDirectoryScanner(getProject());
  264. String[] files = ds.getIncludedFiles();
  265. for (int j = 0; j < files.length; j++) {
  266. File srcFile = new File(fs.getDir(getProject()), files[j]);
  267. doValidate(srcFile);
  268. fileProcessed++;
  269. }
  270. }
  271. log(fileProcessed + " file(s) have been successfully validated.");
  272. }
  273. /**
  274. * init the parser :
  275. * load the parser class, and set features if necessary
  276. */
  277. private void initValidator() {
  278. Object reader = null;
  279. if (readerClassName == null) {
  280. try {
  281. reader = JAXPUtils.getXMLReader();
  282. } catch (BuildException exc) {
  283. reader = JAXPUtils.getParser();
  284. }
  285. } else {
  286. Class readerClass = null;
  287. try {
  288. // load the parser class
  289. if (classpath != null) {
  290. AntClassLoader loader =
  291. getProject().createClassLoader(classpath);
  292. readerClass = Class.forName(readerClassName, true, loader);
  293. } else {
  294. readerClass = Class.forName(readerClassName);
  295. }
  296. reader = readerClass.newInstance();
  297. } catch (ClassNotFoundException e) {
  298. throw new BuildException(INIT_FAILED_MSG + readerClassName, e);
  299. } catch (InstantiationException e) {
  300. throw new BuildException(INIT_FAILED_MSG + readerClassName, e);
  301. } catch (IllegalAccessException e) {
  302. throw new BuildException(INIT_FAILED_MSG + readerClassName, e);
  303. }
  304. }
  305. // then check it implements XMLReader
  306. if (reader instanceof XMLReader) {
  307. xmlReader = (XMLReader) reader;
  308. log(
  309. "Using SAX2 reader " + reader.getClass().getName(),
  310. Project.MSG_VERBOSE);
  311. } else {
  312. // see if it is a SAX1 Parser
  313. if (reader instanceof Parser) {
  314. xmlReader = new ParserAdapter((Parser) reader);
  315. log(
  316. "Using SAX1 parser " + reader.getClass().getName(),
  317. Project.MSG_VERBOSE);
  318. } else {
  319. throw new BuildException(
  320. INIT_FAILED_MSG
  321. + reader.getClass().getName()
  322. + " implements nor SAX1 Parser nor SAX2 XMLReader.");
  323. }
  324. }
  325. xmlReader.setEntityResolver(getEntityResolver());
  326. xmlReader.setErrorHandler(errorHandler);
  327. if (!(xmlReader instanceof ParserAdapter)) {
  328. // turn validation on
  329. if (!lenient) {
  330. setFeature("http://xml.org/sax/features/validation", true);
  331. }
  332. // set the feature from the attribute list
  333. for (int i = 0; i < attributeList.size(); i++) {
  334. Attribute feature = (Attribute) attributeList.elementAt(i);
  335. setFeature(feature.getName(), feature.getValue());
  336. }
  337. // Sets properties
  338. for (int i = 0; i < propertyList.size(); i++) {
  339. final Property prop = (Property) propertyList.elementAt(i);
  340. setProperty(prop.getName(), prop.getValue());
  341. }
  342. }
  343. }
  344. /**
  345. * Set a feature on the parser.
  346. * @param feature the name of the feature to set
  347. * @param value the value of the feature
  348. */
  349. private void setFeature(String feature, boolean value)
  350. throws BuildException {
  351. try {
  352. xmlReader.setFeature(feature, value);
  353. } catch (SAXNotRecognizedException e) {
  354. throw new BuildException(
  355. "Parser "
  356. + xmlReader.getClass().getName()
  357. + " doesn't recognize feature "
  358. + feature,
  359. e,
  360. getLocation());
  361. } catch (SAXNotSupportedException e) {
  362. throw new BuildException(
  363. "Parser "
  364. + xmlReader.getClass().getName()
  365. + " doesn't support feature "
  366. + feature,
  367. e,
  368. getLocation());
  369. }
  370. }
  371. /**
  372. * Sets a property.
  373. *
  374. * @param name a property name
  375. * @param value a property value.
  376. * @throws BuildException if an error occurs.
  377. */
  378. private void setProperty(String name, String value) throws BuildException {
  379. // Validates property
  380. if (name == null || value == null) {
  381. throw new BuildException("Property name and value must be specified.");
  382. }
  383. try {
  384. xmlReader.setProperty(name, value);
  385. } catch (SAXNotRecognizedException e) {
  386. throw new BuildException(
  387. "Parser "
  388. + xmlReader.getClass().getName()
  389. + " doesn't recognize property "
  390. + name,
  391. e,
  392. getLocation());
  393. } catch (SAXNotSupportedException e) {
  394. throw new BuildException(
  395. "Parser "
  396. + xmlReader.getClass().getName()
  397. + " doesn't support property "
  398. + name,
  399. e,
  400. getLocation());
  401. }
  402. }
  403. /**
  404. * parse the file
  405. */
  406. private void doValidate(File afile) {
  407. try {
  408. log("Validating " + afile.getName() + "... ", Project.MSG_VERBOSE);
  409. errorHandler.init(afile);
  410. InputSource is = new InputSource(new FileInputStream(afile));
  411. String uri = fu.toURI(afile.getAbsolutePath());
  412. is.setSystemId(uri);
  413. xmlReader.parse(is);
  414. } catch (SAXException ex) {
  415. if (failOnError) {
  416. throw new BuildException(
  417. "Could not validate document " + afile);
  418. } else {
  419. log("Could not validate document " + afile + ": " + ex.toString());
  420. }
  421. } catch (IOException ex) {
  422. throw new BuildException(
  423. "Could not validate document " + afile,
  424. ex);
  425. }
  426. if (errorHandler.getFailure()) {
  427. if (failOnError) {
  428. throw new BuildException(
  429. afile + " is not a valid XML document.");
  430. } else {
  431. log(afile + " is not a valid XML document", Project.MSG_ERR);
  432. }
  433. }
  434. }
  435. /**
  436. * ValidatorErrorHandler role :
  437. * <ul>
  438. * <li> log SAX parse exceptions,
  439. * <li> remember if an error occurred
  440. * </ul>
  441. */
  442. protected class ValidatorErrorHandler implements ErrorHandler {
  443. protected File currentFile = null;
  444. protected String lastErrorMessage = null;
  445. protected boolean failed = false;
  446. /**
  447. * initialises the class
  448. * @param file file used
  449. */
  450. public void init(File file) {
  451. currentFile = file;
  452. failed = false;
  453. }
  454. /**
  455. * did an error happen during last parsing ?
  456. * @return did an error happen during last parsing ?
  457. */
  458. public boolean getFailure() {
  459. return failed;
  460. }
  461. /**
  462. * record a fatal error
  463. * @param exception the fatal error
  464. */
  465. public void fatalError(SAXParseException exception) {
  466. failed = true;
  467. doLog(exception, Project.MSG_ERR);
  468. }
  469. /**
  470. * receive notification of a recoverable error
  471. * @param exception the error
  472. */
  473. public void error(SAXParseException exception) {
  474. failed = true;
  475. doLog(exception, Project.MSG_ERR);
  476. }
  477. /**
  478. * receive notification of a warning
  479. * @param exception the warning
  480. */
  481. public void warning(SAXParseException exception) {
  482. // depending on implementation, XMLReader can yield hips of warning,
  483. // only output then if user explicitly asked for it
  484. if (warn) {
  485. doLog(exception, Project.MSG_WARN);
  486. }
  487. }
  488. private void doLog(SAXParseException e, int logLevel) {
  489. log(getMessage(e), logLevel);
  490. }
  491. private String getMessage(SAXParseException e) {
  492. String sysID = e.getSystemId();
  493. if (sysID != null) {
  494. try {
  495. int line = e.getLineNumber();
  496. int col = e.getColumnNumber();
  497. return new URL(sysID).getFile()
  498. + (line == -1
  499. ? ""
  500. : (":" + line + (col == -1 ? "" : (":" + col))))
  501. + ": "
  502. + e.getMessage();
  503. } catch (MalformedURLException mfue) {
  504. // ignore and just return exception message
  505. }
  506. }
  507. return e.getMessage();
  508. }
  509. }
  510. /**
  511. * The class to create to set a feature of the parser.
  512. * @since ant1.6
  513. */
  514. public class Attribute {
  515. /** The name of the attribute to set.
  516. *
  517. * Valid attributes <a href="http://www.saxproject.org/apidoc/org/xml/sax/package-summary.html#package_description">include.</a>
  518. */
  519. private String attributeName = null;
  520. /**
  521. * The value of the feature.
  522. **/
  523. private boolean attributeValue;
  524. /**
  525. * Set the feature name.
  526. * @param name the name to set
  527. */
  528. public void setName(String name) {
  529. attributeName = name;
  530. }
  531. /**
  532. * Set the feature value to true or false.
  533. * @param value feature value
  534. */
  535. public void setValue(boolean value) {
  536. attributeValue = value;
  537. }
  538. /**
  539. * Gets the attribute name.
  540. * @return the feature name
  541. */
  542. public String getName() {
  543. return attributeName;
  544. }
  545. /**
  546. * Gets the attribute value.
  547. * @return the feature value
  548. */
  549. public boolean getValue() {
  550. return attributeValue;
  551. }
  552. }
  553. /**
  554. * A Parser property.
  555. * See <a href="http://xml.apache.org/xerces-j/properties.html">
  556. * XML parser properties</a> for usable properties
  557. * @since ant 1.6.2
  558. */
  559. public final class Property {
  560. private String name;
  561. private String value;
  562. /**
  563. * accessor to the name of the property
  564. * @return name of the property
  565. */
  566. public String getName() {
  567. return name;
  568. }
  569. /**
  570. * setter for the name of the property
  571. * @param name name of the property
  572. */
  573. public void setName(String name) {
  574. this.name = name;
  575. }
  576. /**
  577. * getter for the value of the property
  578. * @return value of the property
  579. */
  580. public String getValue() {
  581. return value;
  582. }
  583. /**
  584. * sets the value of the property
  585. * @param value value of the property
  586. */
  587. public void setValue(String value) {
  588. this.value = value;
  589. }
  590. } // Property
  591. }