org.apache.tools.ant.taskdefs.XmlProperty Maven / Gradle / Ivy
/*
* 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.Arrays;
import java.util.Hashtable;
import java.util.Map;
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.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.XMLCatalog;
import org.apache.tools.ant.types.resources.FileProvider;
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.EntityResolver;
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.
*
* 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 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 "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):
*
*
* - value: Identifies a text value for a property.
* - location: Identifies a file location for a property.
* - id: Sets an id for a property
* - refid: Sets a property to the value of another property
* based upon the provided id
* - pathid: Defines a path rather than a property with
* the given id.
*
*
* For example, with keepRoot = false, the following properties file:
*
*
* <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>
*
*
* is equivalent to the following entries in a build file:
*
*
* <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>
*
*
* This task requires the following attributes:
*
*
* - file: The name of the file to load.
*
*
* This task supports the following attributes:
*
*
* - prefix: Optionally specify a prefix applied to
* all properties loaded. Defaults to an empty string.
* - keepRoot: Indicate whether the root xml element
* is kept as part of property name. Defaults to true.
* - validate: Indicate whether the xml file is validated.
* Defaults to false.
* - collapseAttributes: 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)).
* - semanticAttributes: Indicate whether attributes
* named "location", "value", "refid" and "path"
* are interpreted as ant properties. Defaults
* to false.
* - rootDirectory: Indicate the directory to use
* as the root directory for resolving location
* properties. Defaults to the directory
* of the project using the task.
* - includeSemanticAttribute: Indicate whether to include
* the semantic attribute ("location" or "value") as
* part of the property name. Defaults to false.
*
*
* @ant.task name="xmlproperty" category="xml"
*/
public class XmlProperty extends org.apache.tools.ant.Task {
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();
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 Map addedAttributes = new Hashtable<>();
private XMLCatalog xmlCatalog = new XMLCatalog();
private String delimiter = ",";
/**
* Initializes the task.
*/
@Override
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
*/
@Override
public void execute() throws BuildException {
Resource r = getResource();
if (r == null) {
throw new BuildException("XmlProperty task requires a source resource");
}
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;
FileProvider fp = src.as(FileProvider.class);
if (fp != null) {
document = builder.parse(fp.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 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) {
// 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 (ID.equals(nodeName)) {
// ID has already been found above.
continue;
}
if (containingPath != null && PATH.equals(nodeName)) {
// A "path" attribute for a node within a Path object.
containingPath.setPath(attributeValue);
} else if (containingPath != null
&& container instanceof Path && REF_ID.equals(nodeName)) {
// A "refid" attribute for a node within a Path object.
containingPath.setPath(attributeValue);
} else if (containingPath != null && container instanceof Path
&& LOCATION.equals(nodeName)) {
// A "location" attribute for a node within a
// Path object.
containingPath.setLocation(resolveFile(attributeValue));
} else if (PATHID.equals(nodeName)) {
// 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 (nodeText.isEmpty() && !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
&& node.getFirstChild().getNodeValue().isEmpty()
&& !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().isEmpty() || 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 = addedAttributes.get(name) + getDelimiter() + 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 (REF_ID.equals(attributeName)) {
return "";
}
// Otherwise, return it appended unless property to hide it is set.
if (!isSemanticAttribute(attributeName) || includeSemanticAttribute) {
return "." + attributeName;
}
return "";
}
return collapseAttributes ? "." + attributeName : "(" + attributeName + ")";
}
/**
* Return whether the provided attribute name is recognized or not.
*/
private static boolean isSemanticAttribute(String attributeName) {
return Arrays.asList(ATTRIBUTES).contains(attributeName);
}
/**
* Return the value for the given attribute.
* If we are not using semantic attributes, its just the
* literal string value of the attribute.
*
* If 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 (LOCATION.equals(attributeName)) {
File f = resolveFile(nodeValue);
return f.getPath();
}
if (REF_ID.equals(attributeName)) {
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.as(FileProvider.class) != null || supportsNonFileResources()) {
this.src = src;
} else {
throw new BuildException("Only FileSystem resources are supported.");
}
}
/**
* 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(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 semantic 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() {
FileProvider fp = src.as(FileProvider.class);
return fp != null ? fp.getFile() : null;
}
/**
* @return the resource.
*/
protected Resource getResource() {
// delegate this way around to support subclasses that
// overwrite getFile
File f = getFile();
FileProvider fp = src.as(FileProvider.class);
return f == null ? src : fp != null
&& fp.getFile().equals(f) ? src : new FileResource(f);
}
/**
* @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;
}
@Deprecated
protected boolean getIncludeSementicAttribute() {
return getIncludeSemanticAttribute();
}
/**
* @return the include semantic attribute.
*/
protected boolean getIncludeSemanticAttribute() {
return this.includeSemanticAttribute;
}
/**
* Let project resolve the file - or do it ourselves if
* rootDirectory has been set.
*/
private File resolveFile(String fileName) {
return FILE_UTILS.resolveFile(rootDirectory == null ? getProject().getBaseDir()
: rootDirectory, fileName);
}
/**
* Whether this task can deal with non-file resources.
*
* 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.
* @return true for this task.
* @since Ant 1.7
*/
protected boolean supportsNonFileResources() {
return getClass().equals(XmlProperty.class);
}
/**
* Get the current delimiter.
* @return delimiter
*/
public String getDelimiter() {
return delimiter;
}
/**
* Sets a new delimiter.
* @param delimiter new value
* @since Ant 1.7.1
*/
public void setDelimiter(String delimiter) {
this.delimiter = delimiter;
}
}