- /*
- * Copyright 2002-2004 The Apache Software Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
- package org.apache.tools.ant.taskdefs;
-
- import java.io.File;
- import java.io.IOException;
- import java.util.Hashtable;
- 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.util.FileUtils;
- import org.w3c.dom.Document;
- 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.
- *
- * <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>
- *
- * <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 "semanticAttributes" 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>
- * <build folder="build">
- * <classes id="build.classes" location="${build.folder}/classes"/>
- * <reference refid="build.classes"/>
- * </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="build"/>
- * <property name="build.classes" location="${build.location}/classes"/>
- * <property name="build.reference" refid="build.classes"/>
- *
- * <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 false.</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 semantic attribute ("location" or "value") as
- * part of the property name. Defaults to false.</li>
- * </ul>
- *
- * @ant.task name="xmlproperty" category="xml"
- */
-
- public class XmlProperty extends org.apache.tools.ant.Task {
-
- private File src;
- private String prefix = "";
- private boolean keepRoot = true;
- private boolean validate = false;
- private boolean collapseAttributes = false;
- 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.
- */
- public XmlProperty() {
- super();
- }
-
- /**
- * Initializes the task.
- */
-
- public void init() {
- super.init();
- }
-
- /**
- * Run the task.
- * @throws BuildException The exception raised during task execution.
- * @todo validate the source file is valid before opening, print a better error message
- * @todo add a verbose level log message listing the name of the file being loaded
- */
- public void execute()
- throws BuildException {
-
- if (getFile() == null) {
- String msg = "XmlProperty task requires a file attribute";
- throw new BuildException(msg);
- }
-
- try {
- log("Loading " + src.getAbsolutePath(), Project.MSG_VERBOSE);
-
- if (src.exists()) {
-
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- factory.setValidating(validate);
- factory.setNamespaceAware(false);
- Document document = factory.newDocumentBuilder().parse(src);
- Element topElement = document.getDocumentElement();
-
- // Keep a hashtable of attributes added by this task.
- // This task is allow to override its own properties
- // but not other properties. So we need to keep track
- // of which properties we've added.
- addedAttributes = new Hashtable();
-
- if (keepRoot) {
- 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, null);
- }
- }
-
- } else {
- log("Unable to find property file: " + src.getAbsolutePath(),
- Project.MSG_VERBOSE);
- }
-
- } catch (SAXException sxe) {
- // Error generated during parsing
- Exception x = sxe;
- if (sxe.getException() != null) {
- x = sxe.getException();
- }
- throw new BuildException(x);
-
- } catch (ParserConfigurationException pce) {
- // Parser with specified options can't be built
- throw new BuildException(pce);
- } catch (IOException ioe) {
- // I/O error
- throw new BuildException(ioe);
- }
- }
-
- /** 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);
- }
-
- /**
- * 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) {
-
- // 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()) {
-
- 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);
-
- 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 nodeText = null;
- if (node.getNodeType() == Node.TEXT_NODE) {
- // For the text node, add a property.
- nodeText = getAttributeValue(node);
- } else if ((node.getNodeType() == Node.ELEMENT_NODE)
- && (node.getChildNodes().getLength() == 1)
- && (node.getFirstChild().getNodeType() == Node.CDATA_SECTION_NODE)) {
-
- nodeText = node.getFirstChild().getNodeValue();
- }
-
- if (nodeText != null) {
- // If the containing object was a String, then use it as the ID.
- if (semanticAttributes && id == null
- && container instanceof String) {
- id = (String) container;
- System.out.println("Setting id = " + id);
- }
-
- if (nodeText.trim().length() != 0) {
- addProperty(prefix, nodeText, id);
- }
- }
-
- // 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);
- }
-
- /**
- * 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.
- // We use the setProperty method which will
- // forcibly override the property if it already exists.
- // We need to put these properties into the project
- // when we read them, though (instead of keeping them
- // outside of the project and batch adding them at the end)
- // to allow other properties to reference them.
- 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);
- }
- }
-
- /**
- * 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.
- * @param src the file to parse
- */
- public void setFile(File src) {
- this.src = src;
- }
-
- /**
- * the prefix to prepend to each property
- * @param prefix the prefix to prepend to each property
- */
- public void setPrefix(String prefix) {
- this.prefix = prefix.trim();
- }
-
- /**
- * flag to include the xml root tag as a
- * first value in the property name; optional,
- * default is true
- * @param keepRoot if true (default), include the xml root tag
- */
- public void setKeeproot(boolean keepRoot) {
- this.keepRoot = keepRoot;
- }
-
- /**
- * flag to validate the XML file; optional, default false
- * @param validate if true validate the XML file, default false
- */
- public void setValidate(boolean validate) {
- this.validate = validate;
- }
-
- /**
- * flag to treat attributes as nested elements;
- * optional, default false
- * @param collapseAttributes if true treat attributes as nested elements
- */
- public void setCollapseAttributes(boolean collapseAttributes) {
- 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);
- }
-
- }