FileDocCategorySizeDatePackage
XmlProperty.javaAPI DocApache Ant 1.7028607Wed Dec 13 06:16:18 GMT 2006org.apache.tools.ant.taskdefs

XmlProperty.java

/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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.DocumentBuilder;
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.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.XMLCatalog;
import org.apache.tools.ant.types.resources.FileResource;
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;
import org.xml.sax.EntityResolver;

/**
 * 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 Resource 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 Hashtable addedAttributes = new Hashtable();
    private XMLCatalog xmlCatalog = new XMLCatalog();

    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
    };
    private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();

    /**
     * Constructor.
     */
    public XmlProperty() {
        super();
    }

    /**
     * Initializes the task.
     */

    public void init() {
        super.init();
        xmlCatalog.setProject(getProject());
    }


    /**
     * @return the xmlCatalog as the entityresolver.
     */
    protected EntityResolver getEntityResolver() {
        return xmlCatalog;
    }

    /**
     * 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 {

        Resource r = getResource();

        if (r == null) {
            String msg = "XmlProperty task requires a source resource";
            throw new BuildException(msg);
        }

        try {
            log("Loading " + src, Project.MSG_VERBOSE);

            if (r.isExists()) {

              DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
              factory.setValidating(validate);
              factory.setNamespaceAware(false);
              DocumentBuilder builder = factory.newDocumentBuilder();
              builder.setEntityResolver(getEntityResolver());
              Document document = null;
              if (src instanceof FileResource) {
                  document = builder.parse(((FileResource) src).getFile());
              } else {
                  document = builder.parse(src.getInputStream());
              }
              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 resource: " + r,
                    Project.MSG_VERBOSE);
            }

        } catch (SAXException sxe) {
            // Error generated during parsing
            Exception x = sxe;
            if (sxe.getException() != null) {
                x = sxe.getException();
            }
            throw new BuildException("Failed to load " + src, 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("Failed to load " + src, 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;
        boolean emptyNode = false;
        boolean semanticEmptyOverride = false;
        if (node.getNodeType() == Node.ELEMENT_NODE
            && semanticAttributes
            && node.hasAttributes()
            && (node.getAttributes().getNamedItem(VALUE) != null
                || node.getAttributes().getNamedItem(LOCATION) != null
                || node.getAttributes().getNamedItem(REF_ID) != null
                || node.getAttributes().getNamedItem(PATH) != null
                || node.getAttributes().getNamedItem(PATHID) != null)) {
            semanticEmptyOverride = true;
        }
        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 ("".equals(nodeText) && !semanticEmptyOverride) {
                emptyNode = true;
            }
        } else if ((node.getNodeType() == Node.ELEMENT_NODE)
                   && (node.getChildNodes().getLength() == 0)
                   && !semanticEmptyOverride) {
            nodeText = "";
            emptyNode = true;
        } else if ((node.getNodeType() == Node.ELEMENT_NODE)
                   && (node.getChildNodes().getLength() == 1)
                   && (node.getFirstChild().getNodeType() == Node.TEXT_NODE)
                   && ("".equals(node.getFirstChild().getNodeValue()))
                   && !semanticEmptyOverride) {
            nodeText = "";
            emptyNode = true;
        }

        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;
            }
            if (nodeText.trim().length() != 0 || emptyNode) {
                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);
            addedAttributes.put(name, value);
        } else if (getProject().getProperty(name) == null) {
            getProject().setNewProperty(name, value);
            addedAttributes.put(name, value);
        } else {
            log("Override ignored for property " + name, Project.MSG_VERBOSE);
        }
        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) {
        setSrcResource(new FileResource(src));
    }

    /**
     * The resource to pack; required.
     * @param src resource to expand
     */
    public void setSrcResource(Resource src) {
        if (src.isDirectory()) {
            throw new BuildException("the source can't be a directory");
        }
        if (src instanceof FileResource && !supportsNonFileResources()) {
            throw new BuildException("Only FileSystem resources are"
                                     + " supported.");
        }
        this.src = src;
    }

    /**
     * Set the source resource.
     * @param a the resource to pack as a single element Resource collection.
     */
    public void addConfigured(ResourceCollection a) {
        if (a.size() != 1) {
            throw new BuildException("only single argument resource collections"
                                     + " are supported as archives");
        }
        setSrcResource((Resource) a.iterator().next());
    }

    /**
     * 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;
    }

    /**
     * Attribute to enable special handling of attributes - see ant manual.
     * @param semanticAttributes if true enable the special handling.
     */
    public void setSemanticAttributes(boolean semanticAttributes) {
        this.semanticAttributes = semanticAttributes;
    }

    /**
     * The directory to use for resolving file references.
     * Ignored if semanticAttributes is not set to true.
     * @param rootDirectory the directory.
     */
    public void setRootDirectory(File rootDirectory) {
        this.rootDirectory = rootDirectory;
    }

    /**
     * Include the semantic attribute name as part of the property name.
     * Ignored if semanticAttributes is not set to true.
     * @param includeSemanticAttribute if true include the sematic attribute
     *                                 name.
     */
    public void setIncludeSemanticAttribute(boolean includeSemanticAttribute) {
        this.includeSemanticAttribute = includeSemanticAttribute;
    }

    /**
     * add an XMLCatalog as a nested element; optional.
     * @param catalog the XMLCatalog to use
     */
    public void addConfiguredXMLCatalog(XMLCatalog catalog) {
        xmlCatalog.addConfiguredXMLCatalog(catalog);
    }

    /* Expose members for extensibility */

    /**
     * @return the file attribute.
     */
    protected File getFile () {
        if (src instanceof FileResource) {
            return ((FileResource) src).getFile();
        } else {
            return null;
        }
    }

    /**
     * @return the resource.
     */
    protected Resource getResource() {
        // delegate this way around to support subclasses that
        // overwrite getFile
        File f = getFile();
        if (f != null) {
            return new FileResource(f);
        } else {
            return src;
        }
    }

    /**
     * @return the prefix attribute.
     */
    protected String getPrefix () {
        return this.prefix;
    }

    /**
     * @return the keeproot attribute.
     */
    protected boolean getKeeproot () {
        return this.keepRoot;
    }

    /**
     * @return the validate attribute.
     */
    protected boolean getValidate () {
        return this.validate;
    }

    /**
     * @return the collapse attributes attribute.
     */
    protected boolean getCollapseAttributes () {
        return this.collapseAttributes;
    }

    /**
     * @return the semantic attributes attribute.
     */
    protected boolean getSemanticAttributes () {
        return this.semanticAttributes;
    }

    /**
     * @return the root directory attribute.
     */
    protected File getRootDirectory () {
        return this.rootDirectory;
    }

    /**
     * @return the include semantic attribute.
     */
    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 FILE_UTILS.resolveFile(getProject().getBaseDir(), fileName);
        }
        return FILE_UTILS.resolveFile(rootDirectory, fileName);
    }

    /**
     * Whether this task can deal with non-file resources.
     *
     * <p>This implementation returns true only if this task is
     * <xmlproperty>.  Any subclass of this class that also wants to
     * support non-file resources needs to override this method.  We
     * need to do so for backwards compatibility reasons since we
     * can't expect subclasses to support resources.</p>
     * @return true for this task.
     * @since Ant 1.7
     */
    protected boolean supportsNonFileResources() {
        return getClass().equals(XmlProperty.class);
    }
}