diff --git a/WHATSNEW b/WHATSNEW index 96e2f2b94..fc5c90ed2 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -63,6 +63,10 @@ Other changes: * has a new attribute pathref that can be used to reference previously defined paths. +* has been improved, you can now expand ${properties}, + define ids or paths and use Ant's location magic for filename resolutions + in the XML file. + Changes from Ant 1.5.1Beta1 to 1.5.1 ==================================== diff --git a/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-collapse-input1.properties b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-collapse-input1.properties new file mode 100644 index 000000000..bed8609b8 --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-collapse-input1.properties @@ -0,0 +1,6 @@ +properties.root=foo,bar +properties.a.b.c=d +properties.a.b=e +properties.foo.bar=quux,quux1 +properties.foo.quux=bar +properties.tag.value=foo diff --git a/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-collapse-override.properties b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-collapse-override.properties new file mode 100644 index 000000000..5d4d70643 --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-collapse-override.properties @@ -0,0 +1,2 @@ +# Match value hardwired in code, NOT in the input... +override.property.test=foo diff --git a/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-nocollapse-input1.properties b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-nocollapse-input1.properties new file mode 100644 index 000000000..153ac765d --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-nocollapse-input1.properties @@ -0,0 +1,8 @@ +properties.root=foo,bar +properties.a.b(c)=d +properties.a.b=e +properties.foo(bar)=quux +properties.foo.bar=quux1 +properties.foo.quux=bar +properties.tag(value)=foo + diff --git a/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-semantic-include.properties b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-semantic-include.properties new file mode 100644 index 000000000..bed8609b8 --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-semantic-include.properties @@ -0,0 +1,6 @@ +properties.root=foo,bar +properties.a.b.c=d +properties.a.b=e +properties.foo.bar=quux,quux1 +properties.foo.quux=bar +properties.tag.value=foo diff --git a/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-semantic-input1.properties b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-semantic-input1.properties new file mode 100644 index 000000000..50cc98184 --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-semantic-input1.properties @@ -0,0 +1,6 @@ +properties.root=foo,bar +properties.a.b.c=d +properties.a.b=e +properties.foo.bar=quux,quux1 +properties.foo.quux=bar +properties.tag=foo diff --git a/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-semantic-override.properties b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-semantic-override.properties new file mode 100644 index 000000000..5d4d70643 --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/keeproot-semantic-override.properties @@ -0,0 +1,2 @@ +# Match value hardwired in code, NOT in the input... +override.property.test=foo diff --git a/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-collapse-input1.properties b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-collapse-input1.properties new file mode 100644 index 000000000..d6640f40f --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-collapse-input1.properties @@ -0,0 +1,6 @@ +root=foo,bar +a.b.c=d +a.b=e +foo.bar=quux,quux1 +foo.quux=bar +tag.value=foo diff --git a/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-nocollapse-input1.properties b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-nocollapse-input1.properties new file mode 100644 index 000000000..3eca36838 --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-nocollapse-input1.properties @@ -0,0 +1,7 @@ +root=foo,bar +a.b(c)=d +a.b=e +foo(bar)=quux +foo.bar=quux1 +foo.quux=bar +tag(value)=foo diff --git a/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-nocollapse-multi.properties b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-nocollapse-multi.properties new file mode 100644 index 000000000..9dd7a54ff --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-nocollapse-multi.properties @@ -0,0 +1 @@ +foo.bar=1,2,3,4 \ No newline at end of file diff --git a/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-semantic-include-input1.properties b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-semantic-include-input1.properties new file mode 100644 index 000000000..d6640f40f --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-semantic-include-input1.properties @@ -0,0 +1,6 @@ +root=foo,bar +a.b.c=d +a.b=e +foo.bar=quux,quux1 +foo.quux=bar +tag.value=foo diff --git a/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-semantic-input1.properties b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-semantic-input1.properties new file mode 100644 index 000000000..f041c804d --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-semantic-input1.properties @@ -0,0 +1,6 @@ +root=foo,bar +a.b.c=d +a.b=e +foo.bar=quux,quux1 +foo.quux=bar +tag=foo diff --git a/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-semantic-locations.properties b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-semantic-locations.properties new file mode 100644 index 000000000..fdf835aeb --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-semantic-locations.properties @@ -0,0 +1 @@ +file=FILE.foo \ No newline at end of file diff --git a/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-semantic-paths.properties b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-semantic-paths.properties new file mode 100644 index 000000000..b856cd77b --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-semantic-paths.properties @@ -0,0 +1 @@ +foo=ID.path \ No newline at end of file diff --git a/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-semantic-references.properties b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-semantic-references.properties new file mode 100644 index 000000000..e03249b45 --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/goldfiles/nokeeproot-semantic-references.properties @@ -0,0 +1,5 @@ +property=foo +foo.bar=foo +foo.quux=foo +foo.thunk=foo +foo.property=ID.foo diff --git a/src/etc/testcases/taskdefs/xmlproperty/inputs/input1.xml b/src/etc/testcases/taskdefs/xmlproperty/inputs/input1.xml new file mode 100644 index 000000000..5c977cbbf --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/inputs/input1.xml @@ -0,0 +1,10 @@ + + foo + bar + e + + quux1 + bar + + + diff --git a/src/etc/testcases/taskdefs/xmlproperty/inputs/locations.xml b/src/etc/testcases/taskdefs/xmlproperty/inputs/locations.xml new file mode 100644 index 000000000..60f855aae --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/inputs/locations.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/etc/testcases/taskdefs/xmlproperty/inputs/multi.xml b/src/etc/testcases/taskdefs/xmlproperty/inputs/multi.xml new file mode 100644 index 000000000..9d2c9a12f --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/inputs/multi.xml @@ -0,0 +1,8 @@ + + + 1 + 2 + 3 + 4 + + \ No newline at end of file diff --git a/src/etc/testcases/taskdefs/xmlproperty/inputs/override.xml b/src/etc/testcases/taskdefs/xmlproperty/inputs/override.xml new file mode 100644 index 000000000..9ae88991f --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/inputs/override.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/etc/testcases/taskdefs/xmlproperty/inputs/paths.xml b/src/etc/testcases/taskdefs/xmlproperty/inputs/paths.xml new file mode 100644 index 000000000..e08270d9b --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/inputs/paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/etc/testcases/taskdefs/xmlproperty/inputs/references.xml b/src/etc/testcases/taskdefs/xmlproperty/inputs/references.xml new file mode 100644 index 000000000..a7274bc25 --- /dev/null +++ b/src/etc/testcases/taskdefs/xmlproperty/inputs/references.xml @@ -0,0 +1,7 @@ + + + + + ${property} + + \ No newline at end of file diff --git a/src/main/org/apache/tools/ant/taskdefs/XmlProperty.java b/src/main/org/apache/tools/ant/taskdefs/XmlProperty.java index aa2af432c..90ec7f48c 100644 --- a/src/main/org/apache/tools/ant/taskdefs/XmlProperty.java +++ b/src/main/org/apache/tools/ant/taskdefs/XmlProperty.java @@ -58,36 +58,155 @@ import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.util.Hashtable; +import java.util.Enumeration; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; +import org.apache.tools.ant.types.Path; +import org.apache.tools.ant.types.Reference; +import org.apache.tools.ant.util.FileUtils; import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** - * Loads property values from a valid XML file, - * generating the property names from the file's element and attribute names. + * Loads property values from a valid XML file, generating the + * property names from the file's element and attribute names. * - * Example: + *

Example:

*
  *   <root-tag myattr="true">
  *     <inner-tag someattr="val">Text</inner-tag>
  *     <a2><a3><a4>false</a4></a3></a2>
+ *     <x>x1</x>
+ *     <x>x2</x>
  *   </root-tag>
  *
- * this generates + * + *

this generates the following properties:

+ * *
  *  root-tag(myattr)=true
  *  root-tag.inner-tag=Text
  *  root-tag.inner-tag(someattr)=val
  *  root-tag.a2.a3.a4=false
+ *  root-tag.x=x1,x2
+ * 
+ * + *

The collapseAttributes property of this task can be set + * to true (the default is false) which will instead result in the + * following properties (note the difference in names of properties + * corresponding to XML attributes):

+ * + *
+ *  root-tag.myattr=true
+ *  root-tag.inner-tag=Text
+ *  root-tag.inner-tag.someattr=val
+ *  root-tag.a2.a3.a4=false
+ *  root-tag.x=x1,x2
+ * 
+ * + *

Optionally, to more closely mirror the abilities of the Property + * task, a selected set of attributes can be treated specially. To + * enable this behavior, the "semanticAttribute" property of this task + * must be set to true (it defaults to false). If this attribute is + * specified, the following attributes take on special meaning + * (setting this to true implicitly sets collapseAttributes to true as + * well):

+ * + * + * + *

For example, with keepRoot = false, the following properties file:

+ * + *
+ * <root-tag>
+ *   <build location="build">
+ *     <classes id="build.classes" location="${build.location}/classes"/>
+ *     <reference refid="build.location"/>
+ *   </build>
+ *   <compile>
+ *     <classpath pathid="compile.classpath">
+ *       <pathelement location="${build.classes}"/>
+ *     </classpath>
+ *   </compile>
+ *   <run-time>
+ *     <jars>*.jar</jars>
+ *     <classpath pathid="run-time.classpath">
+ *       <path refid="compile.classpath"/>
+ *       <pathelement path="${run-time.jars}"/>
+ *     </classpath>
+ *   </run-time>
+ * </root-tag>
  * 
+ * + *

is equivalent to the following entries in a build file:

+ * + *
+ * <property name="build.location" location="build"/>
+ * <property name="build.classes.location" location="${build.location}/classes"/>
+ * <property name="build.reference" refid="build.location"/>
+ *
+ * <property name="run-time.jars" value="*.jar/>
+ *
+ * <classpath id="compile.classpath">
+ *   <pathelement location="${build.classes}"/>
+ * </classpath>
+ *
+ * <classpath id="run-time.classpath">
+ *   <path refid="compile.classpath"/>
+ *   <pathelement path="${run-time.jars}"/>
+ * </classpath>
+ * 
+ * + *

This task requires the following attributes:

+ * + * + * + *

This task supports the following attributes:

+ * + * + * * @author Nicola Ken Barozzi * @author Erik Hatcher + * @author Paul Christmann + * * @ant.task name="xmlproperty" category="xml" */ @@ -98,7 +217,21 @@ public class XmlProperty extends org.apache.tools.ant.Task { private boolean keepRoot = true; private boolean validate = false; private boolean collapseAttributes = false; - private org.w3c.dom.Document document; + private boolean semanticAttributes = false; + private boolean includeSemanticAttribute = false; + private File rootDirectory = null; + private FileUtils fileUtils = FileUtils.newFileUtils(); + private Hashtable addedAttributes = new Hashtable(); + + private static final String ID = "id"; + private static final String REF_ID = "refid"; + private static final String LOCATION = "location"; + private static final String VALUE = "value"; + private static final String PATH = "path"; + private static final String PATHID = "pathid"; + private static final String[] ATTRIBUTES = new String[] { + ID, REF_ID, LOCATION, VALUE, PATH, PATHID + }; /** * Constructor. @@ -124,6 +257,11 @@ public class XmlProperty extends org.apache.tools.ant.Task { public void execute() throws BuildException { + if (getFile() == null) { + String msg = "XmlProperty task requires a file attribute"; + throw new BuildException(msg); + } + BufferedInputStream configurationStream = null; try { @@ -135,21 +273,17 @@ public class XmlProperty extends org.apache.tools.ant.Task { factory.setValidating(validate); factory.setNamespaceAware(false); - DocumentBuilder builder = factory.newDocumentBuilder(); - document = builder.parse(configurationStream); - - Element topElement = document.getDocumentElement(); - NodeList topChildren = topElement.getChildNodes(); - int numChildren = topChildren.getLength(); + Element topElement = factory.newDocumentBuilder().parse(configurationStream).getDocumentElement(); - log("Using prefix: \"" + prefix + "\"", Project.MSG_DEBUG); + addedAttributes = new Hashtable(); if (keepRoot) { - addNodeRecursively(topElement, prefix); - } - else { + addNodeRecursively(topElement, prefix, null); + } else { + NodeList topChildren = topElement.getChildNodes(); + int numChildren = topChildren.getLength(); for (int i = 0; i < numChildren; i++) { - addNodeRecursively(topChildren.item(i), prefix); + addNodeRecursively(topChildren.item(i), prefix, null); } } @@ -176,51 +310,251 @@ public class XmlProperty extends org.apache.tools.ant.Task { } } + /** Iterate through all nodes in the tree. */ + private void addNodeRecursively(Node node, String prefix, + Object container) { + + // Set the prefix for this node to include its tag name. + String nodePrefix = prefix; + if (node.getNodeType() != Node.TEXT_NODE) { + if (prefix.trim().length() > 0) { + nodePrefix += "."; + } + nodePrefix += node.getNodeName(); + } + + // Pass the container to the processing of this node, + Object nodeObject = processNode(node, nodePrefix, container); + + // now, iterate through children. + if (node.hasChildNodes()) { + + NodeList nodeChildren = node.getChildNodes(); + int numChildren = nodeChildren.getLength(); + + for (int i = 0; i < numChildren; i++) { + // For each child, pass the object added by + // processNode to its children -- in other word, each + // object can pass information along to its children. + addNodeRecursively(nodeChildren.item(i), nodePrefix, + nodeObject); + } + } + } + + void addNodeRecursively(org.w3c.dom.Node node, String prefix) { + addNodeRecursively(node, prefix, null); + } + /** - * add all attributes of a node, and its inner text, and then recursively add all nested elements + * Process the given node, adding any required attributes from + * this child node alone -- but not processing any + * children. + * + * @param node the XML Node to parse + * @param prefix A string to prepend to any properties that get + * added by this node. + * @param container Optionally, an object that a parent node + * generated that this node might belong to. For example, this + * node could be within a node that generated a Path. + * @return the Object created by this node. Generally, this is + * either a String if this node resulted in setting an attribute, + * or a Path. */ + public Object processNode (Node node, String prefix, Object container) { - void addNodeRecursively(org.w3c.dom.Node node, String prefix) { + // Parse the attribute(s) and text of this node, adding + // properties for each. + // if the "path" attribute is specified, then return the created path + // which will be passed to the children of this node. + Object addedPath = null; + + // The value of an id attribute of this node. + String id = null; if (node.hasAttributes()) { - org.w3c.dom.NamedNodeMap nodeAttributes = node.getAttributes(); + + NamedNodeMap nodeAttributes = node.getAttributes(); + + // Is there an id attribute? + Node idNode = nodeAttributes.getNamedItem(ID); + id = (semanticAttributes && idNode != null + ? idNode.getNodeValue() : null); + + // Now, iterate through the attributes adding them. for (int i = 0; i < nodeAttributes.getLength(); i++) { + Node attributeNode = nodeAttributes.item(i); - String attributeName; - if(collapseAttributes){ - attributeName = prefix + (prefix.trim().equals("")?"":".") + node.getNodeName() + "." + attributeNode.getNodeName(); - } - else{ - attributeName = prefix + (prefix.trim().equals("")?"":".") + node.getNodeName() + "(" + attributeNode.getNodeName() + ")"; + if (!semanticAttributes) { + String attributeName = getAttributeName(attributeNode); + String attributeValue = getAttributeValue(attributeNode); + addProperty(prefix + attributeName, attributeValue, null); + } else { + + String nodeName = attributeNode.getNodeName(); + String attributeValue = getAttributeValue(attributeNode); + + Path containingPath = + (container != null && container instanceof Path + ? (Path) container : null ); + + /* + * The main conditional logic -- if the attribute + * is somehow "special" (i.e., it has known + * semantic meaning) then deal with it + * appropriately. + */ + if (nodeName.equals(ID)) { + // ID has already been found above. + continue; + } else if (containingPath != null + && nodeName.equals(PATH)) { + // A "path" attribute for a node within a Path object. + containingPath.setPath(attributeValue); + } else if (container instanceof Path + && nodeName.equals(REF_ID)) { + // A "refid" attribute for a node within a Path object. + containingPath.setPath(attributeValue); + } else if (container instanceof Path + && nodeName.equals(LOCATION)) { + // A "location" attribute for a node within a + // Path object. + containingPath.setLocation(resolveFile(attributeValue)); + } else if (nodeName.equals(PATHID)) { + // A node identifying a new path + if (container != null) { + throw new BuildException("XmlProperty does not " + + "support nested paths"); + } + + addedPath = new Path(getProject()); + getProject().addReference(attributeValue, addedPath); + } else { + // An arbitrary attribute. + String attributeName = getAttributeName(attributeNode); + addProperty(prefix + attributeName, attributeValue, id); + } } - - String attributeValue = attributeNode.getNodeValue(); - log(attributeName + ":" + attributeValue, Project.MSG_DEBUG); - getProject().setNewProperty(attributeName, attributeValue); } } if (node.getNodeType() == Node.TEXT_NODE) { - String nodeText = node.getNodeValue(); + // If the containing object was a String, then use it as the ID. + if (semanticAttributes && id == null + && container instanceof String) { + id = (String) container; + } + // For the text node, add a property. + String nodeText = getAttributeValue(node); if (nodeText.trim().length() != 0) { - log(prefix + ":" + nodeText, Project.MSG_DEBUG); - getProject().setNewProperty(prefix, nodeText); + addProperty(prefix, nodeText, id); } } - if (node.hasChildNodes()) { - prefix += ((prefix.trim().equals("")?"":".") + node.getNodeName()); + // Return the Path we added or the ID of this node for + // children to reference if needed. Path objects are + // definitely used by child path elements, and ID may be used + // for a child text node. + return (addedPath != null ? addedPath : id); + } - org.w3c.dom.NodeList nodeChildren = node.getChildNodes(); - int numChildren = nodeChildren.getLength(); + /** + * Actually add the given property/value to the project + * after writing a log message. + */ + private void addProperty (String name, String value, String id) { + String msg = name + ":" + value; + if (id != null) { + msg += ("(id=" + id + ")"); + } + log(msg, Project.MSG_DEBUG); + + if (addedAttributes.containsKey(name)) { + // If this attribute was added by this task, then + // we append this value to the existing value. + value = (String)addedAttributes.get(name) + "," + value; + getProject().setProperty(name, value); + } else { + getProject().setNewProperty(name, value); + } + addedAttributes.put(name, value); + if (id != null) { + getProject().addReference(id, value); + } + } - for (int i = 0; i < numChildren; i++) { - addNodeRecursively(nodeChildren.item(i), prefix); + /** + * Return a reasonable attribute name for the given node. + * If we are using semantic attributes or collapsing + * attributes, the returned name is ".nodename". + * Otherwise, we return "(nodename)". This is long-standing + * (and default) <xmlproperty> behavior. + */ + private String getAttributeName (Node attributeNode) { + String attributeName = attributeNode.getNodeName(); + + if (semanticAttributes) { + // Never include the "refid" attribute as part of the + // attribute name. + if (attributeName.equals(REF_ID)) { + return ""; + // Otherwise, return it appended unless property to hide it is set. + } else if (!isSemanticAttribute(attributeName) + || includeSemanticAttribute) { + return "." + attributeName; + } else { + return ""; } + } else if (collapseAttributes) { + return "." + attributeName; + } else { + return "(" + attributeName + ")"; } } + /** + * Return whether the provided attribute name is recognized or not. + */ + private static boolean isSemanticAttribute (String attributeName) { + for (int i=0;iIf we are using semantic attributes, then first + * dependent properties are resolved (i.e., ${foo} is resolved + * based on the foo property value), and then an appropriate data + * type is used. In particular, location-based properties are + * resolved to absolute file names. Also for refid values, look + * up the referenced object from the project.

+ */ + private String getAttributeValue (Node attributeNode) { + String nodeValue = attributeNode.getNodeValue().trim(); + if (semanticAttributes) { + String attributeName = attributeNode.getNodeName(); + nodeValue = getProject().replaceProperties(nodeValue); + if (attributeName.equals(LOCATION)) { + File f = resolveFile(nodeValue); + return f.getPath(); + } else if (attributeName.equals(REF_ID)) { + Object ref = getProject().getReference(nodeValue); + if (ref != null) { + return ref.toString(); + } + } + } + return nodeValue; + } + /** * The XML file to parse; required. */ @@ -259,4 +593,61 @@ public class XmlProperty extends org.apache.tools.ant.Task { this.collapseAttributes = collapseAttributes; } + public void setSemanticAttributes (boolean semanticAttributes) { + this.semanticAttributes = semanticAttributes; + } + + public void setRootDirectory (File rootDirectory) { + this.rootDirectory = rootDirectory; + } + + public void setIncludeSemanticAttribute (boolean includeSemanticAttribute) { + this.includeSemanticAttribute = includeSemanticAttribute; + } + + /* Expose members for extensibility */ + + protected File getFile () { + return this.src; + } + + protected String getPrefix () { + return this.prefix; + } + + protected boolean getKeeproot () { + return this.keepRoot; + } + + protected boolean getValidate () { + return this.validate; + } + + protected boolean getCollapseAttributes () { + return this.collapseAttributes; + } + + protected boolean getSemanticAttributes () { + return this.semanticAttributes; + } + + protected File getRootDirectory () { + return this.rootDirectory; + } + + protected boolean getIncludeSementicAttribute () { + return this.includeSemanticAttribute; + } + + /** + * Let project resolve the file - or do it ourselves if + * rootDirectory has been set. + */ + private File resolveFile(String fileName) { + if (rootDirectory == null) { + return getProject().resolveFile(fileName); + } + return fileUtils.resolveFile(rootDirectory, fileName); + } + } diff --git a/src/testcases/org/apache/tools/ant/taskdefs/XmlPropertyTest.java b/src/testcases/org/apache/tools/ant/taskdefs/XmlPropertyTest.java index 98d35d840..f424d1f32 100644 --- a/src/testcases/org/apache/tools/ant/taskdefs/XmlPropertyTest.java +++ b/src/testcases/org/apache/tools/ant/taskdefs/XmlPropertyTest.java @@ -54,12 +54,26 @@ package org.apache.tools.ant.taskdefs; +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Properties; +import java.util.Vector; + import org.apache.tools.ant.BuildFileTest; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.types.Path; +import org.apache.tools.ant.util.FileUtils; /** * @author Erik Hatcher + * @author Paul Christmann */ public class XmlPropertyTest extends BuildFileTest { + private static FileUtils fileUtils = FileUtils.newFileUtils(); public XmlPropertyTest(String name) { super(name); @@ -69,14 +83,288 @@ public class XmlPropertyTest extends BuildFileTest { configureProject("src/etc/testcases/taskdefs/xmlproperty.xml"); } - public void testProperties() { executeTarget("test"); -// System.out.println(this.getFullLog()); - assertEquals("true", project.getProperty("root-tag(myattr)")); - assertEquals("Text", project.getProperty("root-tag.inner-tag")); - assertEquals("val", project.getProperty("root-tag.inner-tag(someattr)")); - assertEquals("false", project.getProperty("root-tag.a2.a3.a4")); + assertEquals("true", getProject().getProperty("root-tag(myattr)")); + assertEquals("Text", getProject().getProperty("root-tag.inner-tag")); + assertEquals("val", + getProject().getProperty("root-tag.inner-tag(someattr)")); + assertEquals("false", getProject().getProperty("root-tag.a2.a3.a4")); + } + + public void testNone () { + doTest("testNone", false, false, false, false, false); + } + + public void testKeeproot() { + doTest("testKeeproot", true, false, false, false, false); + } + + public void testCollapse () { + doTest("testCollapse", false, true, false, false, false); + } + + public void testSemantic () { + doTest("testSemantic", false, false, true, false, false); + } + + public void testKeeprootCollapse () { + doTest("testKeeprootCollapse", true, true, false, false, false); + } + + public void testKeeprootSemantic () { + doTest("testKeeprootSemantic", true, false, true, false, false); + } + + public void testCollapseSemantic () { + doTest("testCollapseSemantic", false, true, true, false, false); + } + + public void testKeeprootCollapseSemantic () { + doTest("testKeeprootCollapseSemantic", true, true, true, false, false); + } + + public void testInclude () { + doTest("testInclude", false, false, false, true, false); + } + + public void testSemanticInclude () { + doTest("testSemanticInclude", false, false, true, true, false); + } + + public void testSemanticLocal () { + doTest("testSemanticInclude", false, false, true, false, true); + } + + /** + * Actually run a test, finding all input files (and corresponding + * goldfile) + */ + private void doTest(String msg, boolean keepRoot, boolean collapse, + boolean semantic, boolean include, boolean localRoot) { + Enumeration iter = + getFiles(new File("src/etc/testcases/taskdefs/xmlproperty/inputs")); + while (iter.hasMoreElements()) { + File inputFile = (File) iter.nextElement(); + // What's the working directory? If local, then its the + // folder of the input file. Otherwise, its the "current" dir.. + File workingDir; + if ( localRoot ) { + workingDir = fileUtils.getParentFile(inputFile); + } else { + workingDir = fileUtils.resolveFile(new File("."), "."); + } + + try { + + File propertyFile = getGoldfile(inputFile, keepRoot, collapse, + semantic, include, localRoot); + if (!propertyFile.exists()) { +// System.out.println("Skipping as " +// + propertyFile.getAbsolutePath() +// + ") doesn't exist."); + continue; + } + + // System.out.println(msg + " (" + propertyFile.getName() + ") in (" + workingDir + ")"); + + Project project = new Project(); + + XmlProperty xmlproperty = new XmlProperty(); + xmlproperty.setProject(project); + xmlproperty.setFile(inputFile); + + xmlproperty.setKeeproot(keepRoot); + xmlproperty.setCollapseAttributes(collapse); + xmlproperty.setSemanticAttributes(semantic); + xmlproperty.setIncludeSemanticAttribute(include); + xmlproperty.setRootDirectory(workingDir); + + project.setNewProperty("override.property.test", "foo"); + xmlproperty.execute(); + + Properties props = new Properties(); + props.load(new FileInputStream(propertyFile)); + + //printProperties(project.getProperties()); + ensureProperties(msg, inputFile, workingDir, project, props); + ensureReferences(msg, inputFile, project.getReferences()); + + } catch (IOException ex) { + fail(ex.toString()); + } + } + } + + /** + * Make sure every property loaded from the goldfile was also + * read from the XmlProperty. We could try and test the other way, + * but some other properties may get set in the XmlProperty due + * to generic Project/Task configuration. + */ + private static void ensureProperties (String msg, File inputFile, + File workingDir, Project project, + Properties properties) { + Hashtable xmlproperties = project.getProperties(); + // Every key identified by the Properties must have been loaded. + Enumeration propertyKeyEnum = properties.propertyNames(); + while(propertyKeyEnum.hasMoreElements()){ + String currentKey = propertyKeyEnum.nextElement().toString(); + String assertMsg = msg + "-" + inputFile.getName() + + " Key=" + currentKey; + + String propertyValue = properties.getProperty(currentKey); + + String xmlValue = (String)xmlproperties.get(currentKey); + + if ( propertyValue.indexOf("ID.") == 0 ) { + // The property is an id's thing -- either a property + // or a path. We need to make sure + // that the object was created with the given id. + // We don't have an adequate way of testing the actual + // *value* of the Path object, though... + String id = currentKey; + Object obj = project.getReferences().get(id); + + if ( obj == null ) { + fail(assertMsg + " Object ID does not exist."); + } + + // What is the property supposed to be? + propertyValue = + propertyValue.substring(3, propertyValue.length()); + if (propertyValue.equals("path")) { + if (!(obj instanceof Path)) { + fail(assertMsg + " Path ID is a " + + obj.getClass().getName()); + } + } else { + assertEquals(assertMsg, propertyValue, obj.toString()); + } + + } else { + + if (propertyValue.indexOf("FILE.") == 0) { + // The property is the name of a file. We are testing + // a location attribute, so we need to resolve the given + // file name in the provided folder. + String fileName = + propertyValue.substring(5, propertyValue.length()); + File f = new File(workingDir, fileName); + propertyValue = f.getAbsolutePath(); + } + + assertEquals(assertMsg, propertyValue, xmlValue); + } + + } } + /** + * Debugging method to print the properties in the given hashtable + */ + private static void printProperties(Hashtable xmlproperties) { + Enumeration keyEnum = xmlproperties.keys(); + while (keyEnum.hasMoreElements()) { + String currentKey = keyEnum.nextElement().toString(); + System.out.println(currentKey + " = " + + xmlproperties.get(currentKey)); + } + } + + /** + * Ensure all references loaded by the project are valid. + */ + private static void ensureReferences (String msg, File inputFile, + Hashtable references) { + Enumeration referenceKeyEnum = references.keys(); + while(referenceKeyEnum.hasMoreElements()){ + String currentKey = referenceKeyEnum.nextElement().toString(); + Object currentValue = references.get(currentKey); + + if (currentValue instanceof Path) { + } else if (currentValue instanceof String) { + } else { + fail(msg + "-" + inputFile.getName() + " Key=" + + currentKey + " is not a recognized type."); + } + } + } + + /** + * Munge the name of the input file to find an appropriate goldfile, + * based on hardwired naming conventions. + */ + private static File getGoldfile (File input, boolean keepRoot, + boolean collapse, boolean semantic, + boolean include, boolean localRoot) { + // Substitute .xml with .properties + String baseName = input.getName().toLowerCase(); + if (baseName.endsWith(".xml")) { + baseName = baseName.substring(0, baseName.length() - 4) + + ".properties"; + } + + File dir = fileUtils.getParentFile(fileUtils.getParentFile(input)); + + String goldFileFolder = "goldfiles/"; + + if (keepRoot) { + goldFileFolder += "keeproot-"; + } else { + goldFileFolder += "nokeeproot-"; + } + + if (semantic) { + goldFileFolder += "semantic-"; + if (include) { + goldFileFolder += "include-"; + } + } else { + if (collapse) { + goldFileFolder += "collapse-"; + } else { + goldFileFolder += "nocollapse-"; + } + } + + return new File(dir, goldFileFolder + baseName); + } + + /** + * Retrieve a list of xml files in the specified folder + * and below. + */ + private static Enumeration getFiles (final File startingDir) { + Vector result = new Vector(); + getFiles(startingDir, result); + return result.elements(); + } + + /** + * Collect a list of xml files in the specified folder + * and below. + */ + private static void getFiles (final File startingDir, Vector collect) { + FileFilter filter = new FileFilter() { + public boolean accept (File file) { + if (file.isDirectory()) { + return true; + } else { + return (file.getPath().indexOf("taskdefs") > 0 && + file.getPath().toLowerCase().endsWith(".xml") ); + } + } + }; + + File[] files = startingDir.listFiles(filter); + for (int i=0;i