001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package anttasks;
018:
019: import java.io.File;
020: import java.io.IOException;
021: import java.net.UnknownHostException;
022: import java.util.ArrayList;
023: import java.util.Iterator;
024:
025: import javax.xml.transform.TransformerException;
026:
027: import org.apache.tools.ant.BuildException;
028: import org.apache.tools.ant.DirectoryScanner;
029: import org.apache.tools.ant.Project;
030: import org.apache.tools.ant.taskdefs.MatchingTask;
031: import org.apache.tools.ant.types.XMLCatalog;
032: import org.apache.xpath.XPathAPI;
033: import org.w3c.dom.Attr;
034: import org.w3c.dom.DOMException;
035: import org.w3c.dom.Document;
036: import org.w3c.dom.Element;
037: import org.w3c.dom.NamedNodeMap;
038: import org.w3c.dom.Node;
039: import org.w3c.dom.NodeList;
040: import org.xml.sax.SAXException;
041:
042: /**
043: * Ant task to patch xmlfiles.
044: *
045: *
046: * replace-properties no|false,anything else
047: * xpath: xpath expression for context node
048: * unless-path: xpath expression that must return empty node set
049: * unless: (deprecated) xpath expression that must return empty node set
050: * if-prop: use path file only when project property is set
051: * remove: xpath expression to remove before adding nodes
052: * add-comments: if specified, overrides the ant task value
053: * add-attribute: name of attribute to add to context node (requires value)
054: * add-attribute-<i>name</i>: add attribute <i>name</i> with the specified value
055: * value: value of attribute to add to context node (requires add-attribute)
056: * insert-before: xpath expression, add new nodes before
057: * insert-after: xpath expression, add new nodes after
058: *
059: * @author <a href="mailto:cziegeler@apache.org">Carsten Ziegeler</a>
060: * @author <a href="mailto:vgritsenko@apache.org">Vadim Gritsenko</a>
061: * @author <a href="mailto:crafterm@fztig938.bank.dresdner.net">Marcus Crafter</a>
062: * @author <a href="mailto:ovidiu@cup.hp.com">Ovidiu Predescu</a>
063: * @author <a href="mailto:stephan@apache.org">Stephan Michels</a>
064: * @version CVS $Id: XConfToolTask.java 433543 2006-08-22 06:22:54Z crossley $
065: */
066: public final class XConfToolTask extends MatchingTask {
067:
068: private static final String NL = System
069: .getProperty("line.separator");
070: private static final String FSEP = System
071: .getProperty("file.separator");
072:
073: private File file;
074: //private File directory;
075: private File srcdir;
076: private boolean addComments;
077: /** for resolving entities such as dtds */
078: private XMLCatalog xmlCatalog = new XMLCatalog();
079:
080: /**
081: * Set file, which should be patched.
082: *
083: * @param file File, which should be patched.
084: */
085: public void setFile(File file) {
086: this .file = file;
087: }
088:
089: /**
090: * Set base directory for the patch files.
091: *
092: * @param srcdir Base directory for the patch files.
093: */
094: public void setSrcdir(File srcdir) {
095: this .srcdir = srcdir;
096: }
097:
098: /**
099: * Add the catalog to our internal catalog
100: *
101: * @param xmlCatalog the XMLCatalog instance to use to look up DTDs
102: */
103: public void addConfiguredXMLCatalog(XMLCatalog newXMLCatalog) {
104: this .xmlCatalog.addConfiguredXMLCatalog(newXMLCatalog);
105: }
106:
107: /**
108: * Whether to add a comment indicating where this block of code comes
109: * from.
110: */
111: public void setAddComments(Boolean addComments) {
112: this .addComments = addComments.booleanValue();
113: }
114:
115: /**
116: * Initialize internal instance of XMLCatalog
117: */
118: public void init() throws BuildException {
119: super .init();
120: xmlCatalog.setProject(this .getProject());
121: }
122:
123: /**
124: * Execute task.
125: */
126: public void execute() throws BuildException {
127: if (this .file == null) {
128: throw new BuildException("file attribute is required", this
129: .getLocation());
130: }
131: try {
132: Document document = DocumentCache.getDocument(this .file,
133: this );
134:
135: if (this .srcdir == null) {
136: this .srcdir = this .getProject().resolveFile(".");
137: }
138:
139: DirectoryScanner scanner = getDirectoryScanner(this .srcdir);
140: String[] list = scanner.getIncludedFiles();
141: boolean modified = false;
142: // process recursive
143: File patchfile;
144: ArrayList suspended = new ArrayList();
145: boolean hasChanged = false;
146: for (int i = 0; i < list.length; i++) {
147: patchfile = new File(this .srcdir, list[i]);
148: try {
149: // Adds configuration snippet from the file to the configuration
150: boolean changed = patch(document, patchfile);
151: hasChanged |= changed;
152: if (!changed) {
153: suspended.add(patchfile);
154: }
155: } catch (SAXException e) {
156: log("Ignoring: " + patchfile
157: + "\n(not a valid XML)");
158: }
159: }
160: modified = hasChanged;
161:
162: if (hasChanged && !suspended.isEmpty()) {
163: log("Try to apply suspended patch files",
164: Project.MSG_DEBUG);
165: }
166:
167: ArrayList newSuspended = new ArrayList();
168: while (hasChanged && !suspended.isEmpty()) {
169: hasChanged = false;
170: for (Iterator i = suspended.iterator(); i.hasNext();) {
171: patchfile = (File) i.next();
172: try {
173: // Adds configuration snippet from the file to the configuration
174: boolean changed = patch(document, patchfile);
175: hasChanged |= changed;
176: if (!changed) {
177: newSuspended.add(patchfile);
178: }
179: } catch (SAXException e) {
180: log("Ignoring: " + patchfile
181: + "\n(not a valid XML)");
182: }
183: }
184: suspended = newSuspended;
185: newSuspended = new ArrayList();
186: }
187:
188: if (!suspended.isEmpty()) {
189: for (Iterator i = suspended.iterator(); i.hasNext();) {
190: patchfile = (File) i.next();
191: log("Dismiss: " + patchfile.toString(),
192: Project.MSG_DEBUG);
193: }
194: }
195:
196: if (modified) {
197: DocumentCache.writeDocument(this .file, document, this );
198: } else {
199: log("No Changes: " + this .file, Project.MSG_DEBUG);
200: }
201: DocumentCache.storeDocument(this .file, document, this );
202: } catch (TransformerException e) {
203: throw new BuildException("TransformerException: " + e);
204: } catch (SAXException e) {
205: throw new BuildException("SAXException:" + e);
206: } catch (DOMException e) {
207: throw new BuildException("DOMException:" + e);
208: } catch (UnknownHostException e) {
209: throw new BuildException(
210: "UnknownHostException. Probable cause: The parser is "
211: + "trying to resolve a dtd from the internet and no connection exists.\n"
212: + "You can either connect to the internet during the build, or patch \n"
213: + "XConfToolTask.java to ignore DTD declarations when your parser is in use.");
214: } catch (IOException ioe) {
215: throw new BuildException("IOException: " + ioe);
216: }
217: }
218:
219: /**
220: * Patch XML document with a given patch file.
221: *
222: * @param configuration Orginal document
223: * @param component Patch document
224: * @param patchFile Patch file
225: *
226: * @return True, if the document was successfully patched
227: */
228: private boolean patch(final Document configuration,
229: final File patchFile) throws TransformerException,
230: IOException, DOMException, SAXException {
231:
232: Document component = DocumentCache.getDocument(patchFile, this );
233: String filename = patchFile.toString();
234:
235: // Check to see if Document is an xconf-tool document
236: Element elem = component.getDocumentElement();
237:
238: String extension = filename.lastIndexOf(".") > 0 ? filename
239: .substring(filename.lastIndexOf(".") + 1) : "";
240: String basename = basename(filename);
241:
242: if (!elem.getTagName().equals(extension)) {
243: throw new BuildException("Not a valid xpatch file: "
244: + filename);
245: }
246:
247: String replacePropertiesStr = elem
248: .getAttribute("replace-properties");
249:
250: boolean replaceProperties = !("no"
251: .equalsIgnoreCase(replacePropertiesStr) || "false"
252: .equalsIgnoreCase(replacePropertiesStr));
253:
254: // Get 'root' node were 'component' will be inserted into
255: String xpath = getAttribute(elem, "xpath", replaceProperties);
256: if (xpath == null) {
257: throw new IOException("Attribute 'xpath' is required.");
258: }
259: NodeList nodes = XPathAPI.selectNodeList(configuration, xpath);
260:
261: // Suspend, because the xpath returned not one node
262: if (nodes.getLength() != 1) {
263: log("Suspending: " + filename, Project.MSG_DEBUG);
264: return false;
265: }
266: Node root = nodes.item(0);
267:
268: // Test that 'root' node satisfies 'component' insertion criteria
269: String testPath = getAttribute(elem, "unless-path",
270: replaceProperties);
271: if (testPath == null || testPath.length() == 0) {
272: // only look for old "unless" attr if unless-path is not present
273: testPath = getAttribute(elem, "unless", replaceProperties);
274: }
275: // Is if-path needed?
276: String ifProp = getAttribute(elem, "if-prop", replaceProperties);
277: boolean ifValue = false;
278: if (ifProp != null && !ifProp.equals("")) {
279: ifValue = Boolean.valueOf(
280: this .getProject().getProperty(ifProp))
281: .booleanValue();
282: }
283:
284: if (ifProp != null && ifProp.length() > 0 && !ifValue) {
285: log("Skipping: " + filename, Project.MSG_DEBUG);
286: return false;
287: } else if (testPath != null && testPath.length() > 0
288: && XPathAPI.eval(root, testPath).bool()) {
289: log("Skipping: " + filename, Project.MSG_DEBUG);
290: return false;
291: } else {
292: // Test if component wants us to remove a list of nodes first
293: xpath = getAttribute(elem, "remove", replaceProperties);
294:
295: if (xpath != null && xpath.length() > 0) {
296: nodes = XPathAPI.selectNodeList(configuration, xpath);
297:
298: for (int i = 0, length = nodes.getLength(); i < length; i++) {
299: Node node = nodes.item(i);
300: Node parent = node.getParentNode();
301:
302: parent.removeChild(node);
303: }
304: }
305:
306: // Test for an attribute that needs to be added to an element
307: String name = getAttribute(elem, "add-attribute",
308: replaceProperties);
309: String value = getAttribute(elem, "value",
310: replaceProperties);
311:
312: if (name != null && name.length() > 0) {
313: if (value == null) {
314: throw new IOException(
315: "No attribute value specified for 'add-attribute' "
316: + xpath);
317: }
318: if (root instanceof Element) {
319: ((Element) root).setAttribute(name, value);
320: }
321: }
322:
323: // Override addComments from ant task if specified as an attribute
324: String addCommentsAttr = getAttribute(elem, "add-comments",
325: replaceProperties);
326: if ((addCommentsAttr != null)
327: && (addCommentsAttr.length() > 0)) {
328: setAddComments(new Boolean(addCommentsAttr));
329: }
330:
331: // Allow multiple attributes to be added or modified
332: if (root instanceof Element) {
333: NamedNodeMap attrMap = elem.getAttributes();
334: for (int i = 0; i < attrMap.getLength(); ++i) {
335: Attr attr = (Attr) attrMap.item(i);
336: final String addAttr = "add-attribute-";
337: if (attr.getName().startsWith(addAttr)) {
338: String key = attr.getName().substring(
339: addAttr.length());
340: ((Element) root).setAttribute(key, attr
341: .getValue());
342: }
343: }
344: }
345:
346: // Test if 'component' provides desired insertion point
347: xpath = getAttribute(elem, "insert-before",
348: replaceProperties);
349: Node before = null;
350:
351: if (xpath != null && xpath.length() > 0) {
352: nodes = XPathAPI.selectNodeList(root, xpath);
353: if (nodes.getLength() == 0) {
354: log("Error in: " + filename);
355: throw new IOException("XPath (" + xpath
356: + ") returned zero nodes");
357: }
358: before = nodes.item(0);
359: } else {
360: xpath = getAttribute(elem, "insert-after",
361: replaceProperties);
362: if (xpath != null && xpath.length() > 0) {
363: nodes = XPathAPI.selectNodeList(root, xpath);
364: if (nodes.getLength() == 0) {
365: log("Error in: " + filename);
366: throw new IOException("XPath (" + xpath
367: + ") zero nodes.");
368: }
369: before = nodes.item(nodes.getLength() - 1)
370: .getNextSibling();
371: }
372: }
373:
374: // Add 'component' data into 'root' node
375: log("Processing: " + filename);
376: NodeList componentNodes = component.getDocumentElement()
377: .getChildNodes();
378:
379: if (this .addComments) {
380: root
381: .appendChild(configuration
382: .createComment("..... Start configuration from '"
383: + basename + "' "));
384: root.appendChild(configuration.createTextNode(NL));
385: }
386: for (int i = 0; i < componentNodes.getLength(); i++) {
387: Node node = configuration.importNode(componentNodes
388: .item(i), true);
389:
390: if (replaceProperties) {
391: replaceProperties(node);
392: }
393: if (before == null) {
394: root.appendChild(node);
395: } else {
396: root.insertBefore(node, before);
397: }
398: }
399: if (this .addComments) {
400: root.appendChild(configuration
401: .createComment("..... End configuration from '"
402: + basename + "' "));
403: root.appendChild(configuration.createTextNode(NL));
404: }
405: return true;
406: }
407: }
408:
409: private String getAttribute(Element elem, String attrName,
410: boolean replaceProperties) {
411: String attr = elem.getAttribute(attrName);
412: if (attr == null) {
413: return null;
414: } else if (replaceProperties) {
415: return getProject().replaceProperties(attr);
416: } else {
417: return attr;
418: }
419: }
420:
421: private void replaceProperties(Node n) throws DOMException {
422: NamedNodeMap attrs = n.getAttributes();
423: if (attrs != null) {
424: for (int i = 0; i < attrs.getLength(); i++) {
425: Node attr = attrs.item(i);
426: attr.setNodeValue(getProject().replaceProperties(
427: attr.getNodeValue()));
428: }
429: }
430: switch (n.getNodeType()) {
431: case Node.ATTRIBUTE_NODE:
432: case Node.CDATA_SECTION_NODE:
433: case Node.TEXT_NODE: {
434: n.setNodeValue(getProject().replaceProperties(
435: n.getNodeValue()));
436: break;
437: }
438: case Node.DOCUMENT_NODE:
439: case Node.DOCUMENT_FRAGMENT_NODE:
440: case Node.ELEMENT_NODE: {
441: Node child = n.getFirstChild();
442: while (child != null) {
443: replaceProperties(child);
444: child = child.getNextSibling();
445: }
446: break;
447: }
448: default: {
449: // ignore all other node types
450: }
451: }
452: }
453:
454: /** Returns the file name (excluding directories and extension). */
455: private String basename(String fileName) {
456: int start = fileName.lastIndexOf(FSEP) + 1; // last '/'
457: int end = fileName.lastIndexOf("."); // last '.'
458:
459: if (end == 0) {
460: end = fileName.length();
461: }
462: return fileName.substring(start, end);
463: }
464: }
|