@@ -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:
* <p> Example:</p>
* <pre>
* <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>
*</pre>
* this generates
*
* <p>this generates the following properties:</p>
*
* <pre>
* 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
* </pre>
*
* <p>The <i>collapseAttributes</i> 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):</p>
*
* <pre>
* 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
* </pre>
*
* <p>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):</p>
*
* <ul>
* <li><b>value</b>: Identifies a text value for a property.</li>
* <li><b>location</b>: Identifies a file location for a property.</li>
* <li><b>id</b>: Sets an id for a property</li>
* <li><b>refid</b>: Sets a property to the value of another property
* based upon the provided id</li>
* <li><b>pathid</b>: Defines a path rather than a property with
* the given id.</li>
* </ul>
*
* <p>For example, with keepRoot = false, the following properties file:</p>
*
* <pre>
* <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>
* </pre>
*
* <p>is equivalent to the following entries in a build file:</p>
*
* <pre>
* <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>
* </pre>
*
* <p> This task <i>requires</i> the following attributes:</p>
*
* <ul>
* <li><b>file</b>: The name of the file to load.</li>
* </ul>
*
* <p>This task supports the following attributes:</p>
*
* <ul>
* <li><b>prefix</b>: Optionally specify a prefix applied to
* all properties loaded. Defaults to an empty string.</li>
* <li><b>keepRoot</b>: Indicate whether the root xml element
* is kept as part of property name. Defaults to true.</li>
* <li><b>validate</b>: Indicate whether the xml file is validated.
* Defaults to false.</li>
* <li><b>collapseAttributes</b>: Indicate whether attributes are
* stored in property names with parens or with period
* delimiters. Defaults to false, meaning properties
* are stored with parens (i.e., foo(attr)).</li>
* <li><b>semanticAttributes</b>: Indicate whether attributes
* named "location", "value", "refid" and "path"
* are interpreted as ant properties. Defaults
* to true.</li>
* <li><b>rootDirectory</b>: Indicate the directory to use
* as the root directory for resolving location
* properties. Defaults to the directory
* of the project using the task.</li>
* <li><b>includeSemanticAttribute</b>: Indicate whether to include
* the semanticAttribute ("location" or "value") as
* part of the property name. Defaults to false.</li>
* </ul>
*
* @author <a href="mailto:nicolaken@apache.org">Nicola Ken Barozzi</a>
* @author Erik Hatcher
* @author <a href="mailto:paul@priorartisans.com">Paul Christmann</a>
*
* @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 <em>not</em> 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;i<ATTRIBUTES.length;i++) {
if (attributeName.equals(ATTRIBUTES[i])) {
return true;
}
}
return false;
}
/**
* Return the value for the given attribute.
* If we are not using semantic attributes, its just the
* literal string value of the attribute.
*
* <p>If we <em>are</em> 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.</p>
*/
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);
}
}