blob: 4624c73643376cbaca1709b24d2c691f9d97c611 [file] [log] [blame]
/*
* Copyright 1999-2005 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.
*/
import java.io.File;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Iterator;
import javax.xml.transform.TransformerException;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.types.XMLCatalog;
import org.apache.xpath.XPathAPI;
import org.w3c.dom.Attr;
import org.w3c.dom.DOMException;
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;
/**
* Ant task to patch xmlfiles.
*
*
* replace-properties no|false,anything else
* xpath: xpath expression for context node
* unless-path: xpath expression that must return empty node set
* unless: (deprecated) xpath expression that must return empty node set
* if-prop: use path file only when project property is set
* remove: xpath expression to remove before adding nodes
* add-comments: if specified, overrides the ant task value
* add-attribute: name of attribute to add to context node (requires value)
* add-attribute-<i>name</i>: add attribute <i>name</i> with the specified value
* value: value of attribute to add to context node (requires add-attribute)
* insert-before: xpath expression, add new nodes before
* insert-after: xpath expression, add new nodes after
*
* @version $Id$
*/
public final class XConfToolTask extends MatchingTask {
private static final String NL=System.getProperty("line.separator");
private static final String FSEP=System.getProperty("file.separator");
private File file;
//private File directory;
private File srcdir;
private boolean addComments;
/** for resolving entities such as dtds */
private XMLCatalog xmlCatalog = new XMLCatalog();
/**
* Set file, which should be patched.
*
* @param file File, which should be patched.
*/
public void setFile(File file) {
this.file = file;
}
/**
* Set base directory for the patch files.
*
* @param srcdir Base directory for the patch files.
*/
public void setSrcdir(File srcdir) {
this.srcdir = srcdir;
}
/**
* Add the catalog to our internal catalog
*
* @param xmlCatalog the XMLCatalog instance to use to look up DTDs
*/
public void addConfiguredXMLCatalog(XMLCatalog newXMLCatalog) {
this.xmlCatalog.addConfiguredXMLCatalog(newXMLCatalog);
}
/**
* Whether to add a comment indicating where this block of code comes
* from.
*/
public void setAddComments(Boolean addComments) {
this.addComments = addComments.booleanValue();
}
/**
* Initialize internal instance of XMLCatalog
*/
public void init() throws BuildException {
super.init();
xmlCatalog.setProject(this.getProject());
}
/**
* Execute task.
*/
public void execute() throws BuildException {
if (this.file == null) {
throw new BuildException("file attribute is required", this.getLocation());
}
try {
Document document = DocumentCache.getDocument(this.file, this);
if (this.srcdir == null) {
this.srcdir = this.getProject().resolveFile(".");
}
DirectoryScanner scanner = getDirectoryScanner(this.srcdir);
String[] list = scanner.getIncludedFiles();
boolean modified = false;
// process recursive
File patchfile;
ArrayList suspended = new ArrayList();
boolean hasChanged = false;
for (int i = 0; i < list.length; i++) {
patchfile = new File(this.srcdir, list[i]);
try {
// Adds configuration snippet from the file to the configuration
boolean changed = patch(document, patchfile);
hasChanged |= changed;
if (!changed) {
suspended.add(patchfile);
}
} catch (SAXException e) {
log("Ignoring: "+patchfile+"\n(not a valid XML)");
}
}
modified = hasChanged;
if (hasChanged && !suspended.isEmpty()) {
log("Try to apply suspended patch files", Project.MSG_DEBUG);
}
ArrayList newSuspended = new ArrayList();
while (hasChanged && !suspended.isEmpty()) {
hasChanged = false;
for(Iterator i=suspended.iterator(); i.hasNext();) {
patchfile = (File)i.next();
try {
// Adds configuration snippet from the file to the configuration
boolean changed = patch(document, patchfile);
hasChanged |= changed;
if (!changed) {
newSuspended.add(patchfile);
}
} catch (SAXException e) {
log("Ignoring: "+patchfile+"\n(not a valid XML)");
}
}
suspended = newSuspended;
newSuspended = new ArrayList();
}
if (!suspended.isEmpty()) {
for(Iterator i=suspended.iterator(); i.hasNext();) {
patchfile = (File)i.next();
log("Dismiss: "+patchfile.toString(), Project.MSG_DEBUG);
}
}
if (modified) {
DocumentCache.writeDocument(this.file, document, this);
} else {
log("No Changes: " + this.file, Project.MSG_DEBUG);
}
DocumentCache.storeDocument(this.file, document, this);
} catch (TransformerException e) {
throw new BuildException("TransformerException: "+e);
} catch (SAXException e) {
throw new BuildException("SAXException:" +e);
} catch (DOMException e) {
throw new BuildException("DOMException:" +e);
} catch (UnknownHostException e) {
throw new BuildException("UnknownHostException. Probable cause: The parser is " +
"trying to resolve a dtd from the internet and no connection exists.\n" +
"You can either connect to the internet during the build, or patch \n" +
"XConfToolTask.java to ignore DTD declarations when your parser is in use.");
} catch (IOException ioe) {
throw new BuildException("IOException: "+ioe);
}
}
/**
* Patch XML document with a given patch file.
*
* @param configuration Orginal document
* @param component Patch document
* @param patchFile Patch file
*
* @return True, if the document was successfully patched
*/
private boolean patch(final Document configuration,
final File patchFile)
throws TransformerException, IOException, DOMException, SAXException {
Document component = DocumentCache.getDocument(patchFile, this);
String filename = patchFile.toString();
// Check to see if Document is an xconf-tool document
Element elem = component.getDocumentElement();
String extension = filename.lastIndexOf(".")>0?filename.substring(filename.lastIndexOf(".")+1):"";
String basename = basename(filename);
if (!elem.getTagName().equals(extension)) {
throw new BuildException("Not a valid xpatch file: "+filename);
}
String replacePropertiesStr = elem.getAttribute("replace-properties");
boolean replaceProperties = !("no".equalsIgnoreCase(replacePropertiesStr) ||
"false".equalsIgnoreCase(replacePropertiesStr));
// Get 'root' node were 'component' will be inserted into
String xpath = getAttribute(elem, "xpath", replaceProperties);
if ( xpath == null ) {
throw new IOException("Attribute 'xpath' is required.");
}
NodeList nodes = XPathAPI.selectNodeList(configuration, xpath);
// Suspend, because the xpath returned not one node
if (nodes.getLength() !=1 ) {
log("Suspending: "+filename, Project.MSG_DEBUG);
return false;
}
Node root = nodes.item(0);
// Test that 'root' node satisfies 'component' insertion criteria
String testPath = getAttribute(elem, "unless-path", replaceProperties);
if (testPath == null || testPath.length() == 0) {
// only look for old "unless" attr if unless-path is not present
testPath = getAttribute(elem, "unless", replaceProperties);
}
// Is if-path needed?
String ifProp = getAttribute(elem, "if-prop", replaceProperties);
boolean ifValue = false;
if (ifProp != null && !ifProp.equals("")) {
ifValue = Boolean.valueOf(this.getProject().getProperty(ifProp)).booleanValue();
}
if (ifProp != null && ifProp.length() > 0 && !ifValue ) {
log("Skipping: " + filename, Project.MSG_DEBUG);
return false;
} else if (testPath != null && testPath.length() > 0 &&
XPathAPI.eval(root, testPath).bool()) {
log("Skipping: " + filename, Project.MSG_DEBUG);
return false;
} else {
// Test if component wants us to remove a list of nodes first
xpath = getAttribute(elem, "remove", replaceProperties);
if (xpath != null && xpath.length() > 0) {
nodes = XPathAPI.selectNodeList(configuration, xpath);
for (int i = 0, length = nodes.getLength(); i<length; i++) {
Node node = nodes.item(i);
Node parent = node.getParentNode();
parent.removeChild(node);
}
}
// Test for an attribute that needs to be added to an element
String name = getAttribute(elem, "add-attribute", replaceProperties);
String value = getAttribute(elem, "value", replaceProperties);
if (name != null && name.length() > 0) {
if (value == null) {
throw new IOException("No attribute value specified for 'add-attribute' "+
xpath);
}
if (root instanceof Element) {
((Element) root).setAttribute(name, value);
}
}
// Override addComments from ant task if specified as an attribute
String addCommentsAttr = getAttribute(elem, "add-comments", replaceProperties);
if ((addCommentsAttr!=null) && (addCommentsAttr.length()>0)) {
setAddComments(Boolean.valueOf(addCommentsAttr));
}
// Allow multiple attributes to be added or modified
if (root instanceof Element) {
NamedNodeMap attrMap = elem.getAttributes();
for (int i=0; i<attrMap.getLength(); ++i){
Attr attr = (Attr)attrMap.item(i);
final String addAttr = "add-attribute-";
if (attr.getName().startsWith(addAttr)) {
String key = attr.getName().substring(addAttr.length());
((Element) root).setAttribute(key, attr.getValue());
}
}
}
// Test if 'component' provides desired insertion point
xpath = getAttribute(elem, "insert-before", replaceProperties);
Node before = null;
if (xpath != null && xpath.length() > 0) {
nodes = XPathAPI.selectNodeList(root, xpath);
if (nodes.getLength() == 0) {
log("Error in: "+filename);
throw new IOException("XPath ("+xpath+") returned zero nodes");
}
before = nodes.item(0);
} else {
xpath = getAttribute(elem, "insert-after", replaceProperties);
if (xpath != null && xpath.length() > 0) {
nodes = XPathAPI.selectNodeList(root, xpath);
if (nodes.getLength() == 0) {
log("Error in: "+filename);
throw new IOException("XPath ("+xpath+") zero nodes.");
}
before = nodes.item(nodes.getLength()-1).getNextSibling();
}
}
// Add 'component' data into 'root' node
log("Processing: "+filename);
NodeList componentNodes = component.getDocumentElement().getChildNodes();
if (this.addComments) {
root.appendChild(configuration.createComment("..... Start configuration from '"+basename+"' "));
root.appendChild(configuration.createTextNode(NL));
}
for (int i = 0; i<componentNodes.getLength(); i++) {
Node node = configuration.importNode(componentNodes.item(i),
true);
if (replaceProperties) {
replaceProperties(node);
}
if (before==null) {
root.appendChild(node);
} else {
root.insertBefore(node, before);
}
}
if (this.addComments) {
root.appendChild(configuration.createComment("..... End configuration from '"+basename+"' "));
root.appendChild(configuration.createTextNode(NL));
}
return true;
}
}
private String getAttribute(Element elem, String attrName, boolean replaceProperties) {
String attr = elem.getAttribute(attrName);
if (attr == null) {
return null;
} else if (replaceProperties) {
return getProject().replaceProperties(attr);
} else {
return attr;
}
}
private void replaceProperties(Node n) throws DOMException {
NamedNodeMap attrs = n.getAttributes();
if (attrs != null) {
for (int i = 0; i < attrs.getLength(); i++) {
Node attr = attrs.item(i);
attr.setNodeValue(getProject().replaceProperties(attr.getNodeValue()));
}
}
switch (n.getNodeType()) {
case Node.ATTRIBUTE_NODE:
case Node.CDATA_SECTION_NODE:
case Node.TEXT_NODE: {
n.setNodeValue(getProject().replaceProperties(n.getNodeValue()));
break;
}
case Node.DOCUMENT_NODE:
case Node.DOCUMENT_FRAGMENT_NODE:
case Node.ELEMENT_NODE: {
Node child = n.getFirstChild();
while (child != null) {
replaceProperties(child);
child = child.getNextSibling();
}
break;
}
default: {
// ignore all other node types
}
}
}
/** Returns the file name (excluding directories and extension). */
private String basename(String fileName) {
int start = fileName.lastIndexOf(FSEP)+1; // last '/'
int end = fileName.lastIndexOf("."); // last '.'
if (end == 0) {
end = fileName.length();
}
return fileName.substring(start, end);
}
}