1. /*
  2. * Copyright 2002-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;
  18. import java.io.File;
  19. import java.io.IOException;
  20. import java.util.Hashtable;
  21. import javax.xml.parsers.DocumentBuilderFactory;
  22. import javax.xml.parsers.ParserConfigurationException;
  23. import org.apache.tools.ant.BuildException;
  24. import org.apache.tools.ant.Project;
  25. import org.apache.tools.ant.types.Path;
  26. import org.apache.tools.ant.util.FileUtils;
  27. import org.w3c.dom.Document;
  28. import org.w3c.dom.Element;
  29. import org.w3c.dom.NamedNodeMap;
  30. import org.w3c.dom.Node;
  31. import org.w3c.dom.NodeList;
  32. import org.xml.sax.SAXException;
  33. /**
  34. * Loads property values from a valid XML file, generating the
  35. * property names from the file's element and attribute names.
  36. *
  37. * <p>Example:</p>
  38. * <pre>
  39. * <root-tag myattr="true">
  40. * <inner-tag someattr="val">Text</inner-tag>
  41. * <a2><a3><a4>false</a4></a3></a2>
  42. * <x>x1</x>
  43. * <x>x2</x>
  44. * </root-tag>
  45. *</pre>
  46. *
  47. * <p>this generates the following properties:</p>
  48. *
  49. * <pre>
  50. * root-tag(myattr)=true
  51. * root-tag.inner-tag=Text
  52. * root-tag.inner-tag(someattr)=val
  53. * root-tag.a2.a3.a4=false
  54. * root-tag.x=x1,x2
  55. * </pre>
  56. *
  57. * <p>The <i>collapseAttributes</i> property of this task can be set
  58. * to true (the default is false) which will instead result in the
  59. * following properties (note the difference in names of properties
  60. * corresponding to XML attributes):</p>
  61. *
  62. * <pre>
  63. * root-tag.myattr=true
  64. * root-tag.inner-tag=Text
  65. * root-tag.inner-tag.someattr=val
  66. * root-tag.a2.a3.a4=false
  67. * root-tag.x=x1,x2
  68. * </pre>
  69. *
  70. * <p>Optionally, to more closely mirror the abilities of the Property
  71. * task, a selected set of attributes can be treated specially. To
  72. * enable this behavior, the "semanticAttributes" property of this task
  73. * must be set to true (it defaults to false). If this attribute is
  74. * specified, the following attributes take on special meaning
  75. * (setting this to true implicitly sets collapseAttributes to true as
  76. * well):</p>
  77. *
  78. * <ul>
  79. * <li><b>value</b>: Identifies a text value for a property.</li>
  80. * <li><b>location</b>: Identifies a file location for a property.</li>
  81. * <li><b>id</b>: Sets an id for a property</li>
  82. * <li><b>refid</b>: Sets a property to the value of another property
  83. * based upon the provided id</li>
  84. * <li><b>pathid</b>: Defines a path rather than a property with
  85. * the given id.</li>
  86. * </ul>
  87. *
  88. * <p>For example, with keepRoot = false, the following properties file:</p>
  89. *
  90. * <pre>
  91. * <root-tag>
  92. * <build>
  93. * <build folder="build">
  94. * <classes id="build.classes" location="${build.folder}/classes"/>
  95. * <reference refid="build.classes"/>
  96. * </build>
  97. * <compile>
  98. * <classpath pathid="compile.classpath">
  99. * <pathelement location="${build.classes}"/>
  100. * </classpath>
  101. * </compile>
  102. * <run-time>
  103. * <jars>*.jar</jars>
  104. * <classpath pathid="run-time.classpath">
  105. * <path refid="compile.classpath"/>
  106. * <pathelement path="${run-time.jars}"/>
  107. * </classpath>
  108. * </run-time>
  109. * </root-tag>
  110. * </pre>
  111. *
  112. * <p>is equivalent to the following entries in a build file:</p>
  113. *
  114. * <pre>
  115. * <property name="build" location="build"/>
  116. * <property name="build.classes" location="${build.location}/classes"/>
  117. * <property name="build.reference" refid="build.classes"/>
  118. *
  119. * <property name="run-time.jars" value="*.jar/>
  120. *
  121. * <classpath id="compile.classpath">
  122. * <pathelement location="${build.classes}"/>
  123. * </classpath>
  124. *
  125. * <classpath id="run-time.classpath">
  126. * <path refid="compile.classpath"/>
  127. * <pathelement path="${run-time.jars}"/>
  128. * </classpath>
  129. * </pre>
  130. *
  131. * <p> This task <i>requires</i> the following attributes:</p>
  132. *
  133. * <ul>
  134. * <li><b>file</b>: The name of the file to load.</li>
  135. * </ul>
  136. *
  137. * <p>This task supports the following attributes:</p>
  138. *
  139. * <ul>
  140. * <li><b>prefix</b>: Optionally specify a prefix applied to
  141. * all properties loaded. Defaults to an empty string.</li>
  142. * <li><b>keepRoot</b>: Indicate whether the root xml element
  143. * is kept as part of property name. Defaults to true.</li>
  144. * <li><b>validate</b>: Indicate whether the xml file is validated.
  145. * Defaults to false.</li>
  146. * <li><b>collapseAttributes</b>: Indicate whether attributes are
  147. * stored in property names with parens or with period
  148. * delimiters. Defaults to false, meaning properties
  149. * are stored with parens (i.e., foo(attr)).</li>
  150. * <li><b>semanticAttributes</b>: Indicate whether attributes
  151. * named "location", "value", "refid" and "path"
  152. * are interpreted as ant properties. Defaults
  153. * to false.</li>
  154. * <li><b>rootDirectory</b>: Indicate the directory to use
  155. * as the root directory for resolving location
  156. * properties. Defaults to the directory
  157. * of the project using the task.</li>
  158. * <li><b>includeSemanticAttribute</b>: Indicate whether to include
  159. * the semantic attribute ("location" or "value") as
  160. * part of the property name. Defaults to false.</li>
  161. * </ul>
  162. *
  163. * @ant.task name="xmlproperty" category="xml"
  164. */
  165. public class XmlProperty extends org.apache.tools.ant.Task {
  166. private File src;
  167. private String prefix = "";
  168. private boolean keepRoot = true;
  169. private boolean validate = false;
  170. private boolean collapseAttributes = false;
  171. private boolean semanticAttributes = false;
  172. private boolean includeSemanticAttribute = false;
  173. private File rootDirectory = null;
  174. private FileUtils fileUtils = FileUtils.newFileUtils();
  175. private Hashtable addedAttributes = new Hashtable();
  176. private static final String ID = "id";
  177. private static final String REF_ID = "refid";
  178. private static final String LOCATION = "location";
  179. private static final String VALUE = "value";
  180. private static final String PATH = "path";
  181. private static final String PATHID = "pathid";
  182. private static final String[] ATTRIBUTES = new String[] {
  183. ID, REF_ID, LOCATION, VALUE, PATH, PATHID
  184. };
  185. /**
  186. * Constructor.
  187. */
  188. public XmlProperty() {
  189. super();
  190. }
  191. /**
  192. * Initializes the task.
  193. */
  194. public void init() {
  195. super.init();
  196. }
  197. /**
  198. * Run the task.
  199. * @throws BuildException The exception raised during task execution.
  200. * @todo validate the source file is valid before opening, print a better error message
  201. * @todo add a verbose level log message listing the name of the file being loaded
  202. */
  203. public void execute()
  204. throws BuildException {
  205. if (getFile() == null) {
  206. String msg = "XmlProperty task requires a file attribute";
  207. throw new BuildException(msg);
  208. }
  209. try {
  210. log("Loading " + src.getAbsolutePath(), Project.MSG_VERBOSE);
  211. if (src.exists()) {
  212. DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
  213. factory.setValidating(validate);
  214. factory.setNamespaceAware(false);
  215. Document document = factory.newDocumentBuilder().parse(src);
  216. Element topElement = document.getDocumentElement();
  217. // Keep a hashtable of attributes added by this task.
  218. // This task is allow to override its own properties
  219. // but not other properties. So we need to keep track
  220. // of which properties we've added.
  221. addedAttributes = new Hashtable();
  222. if (keepRoot) {
  223. addNodeRecursively(topElement, prefix, null);
  224. } else {
  225. NodeList topChildren = topElement.getChildNodes();
  226. int numChildren = topChildren.getLength();
  227. for (int i = 0; i < numChildren; i++) {
  228. addNodeRecursively(topChildren.item(i), prefix, null);
  229. }
  230. }
  231. } else {
  232. log("Unable to find property file: " + src.getAbsolutePath(),
  233. Project.MSG_VERBOSE);
  234. }
  235. } catch (SAXException sxe) {
  236. // Error generated during parsing
  237. Exception x = sxe;
  238. if (sxe.getException() != null) {
  239. x = sxe.getException();
  240. }
  241. throw new BuildException(x);
  242. } catch (ParserConfigurationException pce) {
  243. // Parser with specified options can't be built
  244. throw new BuildException(pce);
  245. } catch (IOException ioe) {
  246. // I/O error
  247. throw new BuildException(ioe);
  248. }
  249. }
  250. /** Iterate through all nodes in the tree. */
  251. private void addNodeRecursively(Node node, String prefix,
  252. Object container) {
  253. // Set the prefix for this node to include its tag name.
  254. String nodePrefix = prefix;
  255. if (node.getNodeType() != Node.TEXT_NODE) {
  256. if (prefix.trim().length() > 0) {
  257. nodePrefix += ".";
  258. }
  259. nodePrefix += node.getNodeName();
  260. }
  261. // Pass the container to the processing of this node,
  262. Object nodeObject = processNode(node, nodePrefix, container);
  263. // now, iterate through children.
  264. if (node.hasChildNodes()) {
  265. NodeList nodeChildren = node.getChildNodes();
  266. int numChildren = nodeChildren.getLength();
  267. for (int i = 0; i < numChildren; i++) {
  268. // For each child, pass the object added by
  269. // processNode to its children -- in other word, each
  270. // object can pass information along to its children.
  271. addNodeRecursively(nodeChildren.item(i), nodePrefix,
  272. nodeObject);
  273. }
  274. }
  275. }
  276. void addNodeRecursively(org.w3c.dom.Node node, String prefix) {
  277. addNodeRecursively(node, prefix, null);
  278. }
  279. /**
  280. * Process the given node, adding any required attributes from
  281. * this child node alone -- but <em>not</em> processing any
  282. * children.
  283. *
  284. * @param node the XML Node to parse
  285. * @param prefix A string to prepend to any properties that get
  286. * added by this node.
  287. * @param container Optionally, an object that a parent node
  288. * generated that this node might belong to. For example, this
  289. * node could be within a node that generated a Path.
  290. * @return the Object created by this node. Generally, this is
  291. * either a String if this node resulted in setting an attribute,
  292. * or a Path.
  293. */
  294. public Object processNode (Node node, String prefix, Object container) {
  295. // Parse the attribute(s) and text of this node, adding
  296. // properties for each.
  297. // if the "path" attribute is specified, then return the created path
  298. // which will be passed to the children of this node.
  299. Object addedPath = null;
  300. // The value of an id attribute of this node.
  301. String id = null;
  302. if (node.hasAttributes()) {
  303. NamedNodeMap nodeAttributes = node.getAttributes();
  304. // Is there an id attribute?
  305. Node idNode = nodeAttributes.getNamedItem(ID);
  306. id = (semanticAttributes && idNode != null
  307. ? idNode.getNodeValue() : null);
  308. // Now, iterate through the attributes adding them.
  309. for (int i = 0; i < nodeAttributes.getLength(); i++) {
  310. Node attributeNode = nodeAttributes.item(i);
  311. if (!semanticAttributes) {
  312. String attributeName = getAttributeName(attributeNode);
  313. String attributeValue = getAttributeValue(attributeNode);
  314. addProperty(prefix + attributeName, attributeValue, null);
  315. } else {
  316. String nodeName = attributeNode.getNodeName();
  317. String attributeValue = getAttributeValue(attributeNode);
  318. Path containingPath = (container != null
  319. && container instanceof Path ? (Path) container : null);
  320. /*
  321. * The main conditional logic -- if the attribute
  322. * is somehow "special" (i.e., it has known
  323. * semantic meaning) then deal with it
  324. * appropriately.
  325. */
  326. if (nodeName.equals(ID)) {
  327. // ID has already been found above.
  328. continue;
  329. } else if (containingPath != null
  330. && nodeName.equals(PATH)) {
  331. // A "path" attribute for a node within a Path object.
  332. containingPath.setPath(attributeValue);
  333. } else if (container instanceof Path
  334. && nodeName.equals(REF_ID)) {
  335. // A "refid" attribute for a node within a Path object.
  336. containingPath.setPath(attributeValue);
  337. } else if (container instanceof Path
  338. && nodeName.equals(LOCATION)) {
  339. // A "location" attribute for a node within a
  340. // Path object.
  341. containingPath.setLocation(resolveFile(attributeValue));
  342. } else if (nodeName.equals(PATHID)) {
  343. // A node identifying a new path
  344. if (container != null) {
  345. throw new BuildException("XmlProperty does not "
  346. + "support nested paths");
  347. }
  348. addedPath = new Path(getProject());
  349. getProject().addReference(attributeValue, addedPath);
  350. } else {
  351. // An arbitrary attribute.
  352. String attributeName = getAttributeName(attributeNode);
  353. addProperty(prefix + attributeName, attributeValue, id);
  354. }
  355. }
  356. }
  357. }
  358. String nodeText = null;
  359. if (node.getNodeType() == Node.TEXT_NODE) {
  360. // For the text node, add a property.
  361. nodeText = getAttributeValue(node);
  362. } else if ((node.getNodeType() == Node.ELEMENT_NODE)
  363. && (node.getChildNodes().getLength() == 1)
  364. && (node.getFirstChild().getNodeType() == Node.CDATA_SECTION_NODE)) {
  365. nodeText = node.getFirstChild().getNodeValue();
  366. }
  367. if (nodeText != null) {
  368. // If the containing object was a String, then use it as the ID.
  369. if (semanticAttributes && id == null
  370. && container instanceof String) {
  371. id = (String) container;
  372. System.out.println("Setting id = " + id);
  373. }
  374. if (nodeText.trim().length() != 0) {
  375. addProperty(prefix, nodeText, id);
  376. }
  377. }
  378. // Return the Path we added or the ID of this node for
  379. // children to reference if needed. Path objects are
  380. // definitely used by child path elements, and ID may be used
  381. // for a child text node.
  382. return (addedPath != null ? addedPath : id);
  383. }
  384. /**
  385. * Actually add the given property/value to the project
  386. * after writing a log message.
  387. */
  388. private void addProperty (String name, String value, String id) {
  389. String msg = name + ":" + value;
  390. if (id != null) {
  391. msg += ("(id=" + id + ")");
  392. }
  393. log(msg, Project.MSG_DEBUG);
  394. if (addedAttributes.containsKey(name)) {
  395. // If this attribute was added by this task, then
  396. // we append this value to the existing value.
  397. // We use the setProperty method which will
  398. // forcibly override the property if it already exists.
  399. // We need to put these properties into the project
  400. // when we read them, though (instead of keeping them
  401. // outside of the project and batch adding them at the end)
  402. // to allow other properties to reference them.
  403. value = (String) addedAttributes.get(name) + "," + value;
  404. getProject().setProperty(name, value);
  405. } else {
  406. getProject().setNewProperty(name, value);
  407. }
  408. addedAttributes.put(name, value);
  409. if (id != null) {
  410. getProject().addReference(id, value);
  411. }
  412. }
  413. /**
  414. * Return a reasonable attribute name for the given node.
  415. * If we are using semantic attributes or collapsing
  416. * attributes, the returned name is ".nodename".
  417. * Otherwise, we return "(nodename)". This is long-standing
  418. * (and default) <xmlproperty> behavior.
  419. */
  420. private String getAttributeName (Node attributeNode) {
  421. String attributeName = attributeNode.getNodeName();
  422. if (semanticAttributes) {
  423. // Never include the "refid" attribute as part of the
  424. // attribute name.
  425. if (attributeName.equals(REF_ID)) {
  426. return "";
  427. // Otherwise, return it appended unless property to hide it is set.
  428. } else if (!isSemanticAttribute(attributeName)
  429. || includeSemanticAttribute) {
  430. return "." + attributeName;
  431. } else {
  432. return "";
  433. }
  434. } else if (collapseAttributes) {
  435. return "." + attributeName;
  436. } else {
  437. return "(" + attributeName + ")";
  438. }
  439. }
  440. /**
  441. * Return whether the provided attribute name is recognized or not.
  442. */
  443. private static boolean isSemanticAttribute (String attributeName) {
  444. for (int i = 0; i < ATTRIBUTES.length; i++) {
  445. if (attributeName.equals(ATTRIBUTES[i])) {
  446. return true;
  447. }
  448. }
  449. return false;
  450. }
  451. /**
  452. * Return the value for the given attribute.
  453. * If we are not using semantic attributes, its just the
  454. * literal string value of the attribute.
  455. *
  456. * <p>If we <em>are</em> using semantic attributes, then first
  457. * dependent properties are resolved (i.e., ${foo} is resolved
  458. * based on the foo property value), and then an appropriate data
  459. * type is used. In particular, location-based properties are
  460. * resolved to absolute file names. Also for refid values, look
  461. * up the referenced object from the project.</p>
  462. */
  463. private String getAttributeValue (Node attributeNode) {
  464. String nodeValue = attributeNode.getNodeValue().trim();
  465. if (semanticAttributes) {
  466. String attributeName = attributeNode.getNodeName();
  467. nodeValue = getProject().replaceProperties(nodeValue);
  468. if (attributeName.equals(LOCATION)) {
  469. File f = resolveFile(nodeValue);
  470. return f.getPath();
  471. } else if (attributeName.equals(REF_ID)) {
  472. Object ref = getProject().getReference(nodeValue);
  473. if (ref != null) {
  474. return ref.toString();
  475. }
  476. }
  477. }
  478. return nodeValue;
  479. }
  480. /**
  481. * The XML file to parse; required.
  482. * @param src the file to parse
  483. */
  484. public void setFile(File src) {
  485. this.src = src;
  486. }
  487. /**
  488. * the prefix to prepend to each property
  489. * @param prefix the prefix to prepend to each property
  490. */
  491. public void setPrefix(String prefix) {
  492. this.prefix = prefix.trim();
  493. }
  494. /**
  495. * flag to include the xml root tag as a
  496. * first value in the property name; optional,
  497. * default is true
  498. * @param keepRoot if true (default), include the xml root tag
  499. */
  500. public void setKeeproot(boolean keepRoot) {
  501. this.keepRoot = keepRoot;
  502. }
  503. /**
  504. * flag to validate the XML file; optional, default false
  505. * @param validate if true validate the XML file, default false
  506. */
  507. public void setValidate(boolean validate) {
  508. this.validate = validate;
  509. }
  510. /**
  511. * flag to treat attributes as nested elements;
  512. * optional, default false
  513. * @param collapseAttributes if true treat attributes as nested elements
  514. */
  515. public void setCollapseAttributes(boolean collapseAttributes) {
  516. this.collapseAttributes = collapseAttributes;
  517. }
  518. public void setSemanticAttributes (boolean semanticAttributes) {
  519. this.semanticAttributes = semanticAttributes;
  520. }
  521. public void setRootDirectory (File rootDirectory) {
  522. this.rootDirectory = rootDirectory;
  523. }
  524. public void setIncludeSemanticAttribute (boolean includeSemanticAttribute) {
  525. this.includeSemanticAttribute = includeSemanticAttribute;
  526. }
  527. /* Expose members for extensibility */
  528. protected File getFile () {
  529. return this.src;
  530. }
  531. protected String getPrefix () {
  532. return this.prefix;
  533. }
  534. protected boolean getKeeproot () {
  535. return this.keepRoot;
  536. }
  537. protected boolean getValidate () {
  538. return this.validate;
  539. }
  540. protected boolean getCollapseAttributes () {
  541. return this.collapseAttributes;
  542. }
  543. protected boolean getSemanticAttributes () {
  544. return this.semanticAttributes;
  545. }
  546. protected File getRootDirectory () {
  547. return this.rootDirectory;
  548. }
  549. protected boolean getIncludeSementicAttribute () {
  550. return this.includeSemanticAttribute;
  551. }
  552. /**
  553. * Let project resolve the file - or do it ourselves if
  554. * rootDirectory has been set.
  555. */
  556. private File resolveFile(String fileName) {
  557. if (rootDirectory == null) {
  558. return getProject().resolveFile(fileName);
  559. }
  560. return fileUtils.resolveFile(rootDirectory, fileName);
  561. }
  562. }