KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > apache > forrest > util > XPathTransformer


1 /*
2  * Copyright 1999-2004 The Apache Software Foundation or its licensors,
3  * as applicable.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  * http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */

17 package org.apache.forrest.util;
18
19 import java.io.IOException JavaDoc;
20 import java.io.Serializable JavaDoc;
21 import java.util.Map JavaDoc;
22 import java.util.Stack JavaDoc;
23
24 import org.apache.avalon.framework.parameters.Parameters;
25 import org.apache.avalon.framework.service.ServiceManager;
26 import org.apache.avalon.framework.service.ServiceException;
27 import org.apache.cocoon.ProcessingException;
28 import org.apache.cocoon.transformation.AbstractDOMTransformer;
29 import org.apache.cocoon.caching.CacheableProcessingComponent;
30 import org.apache.cocoon.environment.SourceResolver;
31 import org.apache.cocoon.util.HashUtil;
32 import org.apache.excalibur.source.SourceValidity;
33 import org.apache.excalibur.source.impl.validity.NOPValidity;
34 import org.apache.excalibur.xml.dom.DOMParser;
35 import org.apache.excalibur.xml.xpath.XPathProcessor;
36 import org.w3c.dom.Attr JavaDoc;
37 import org.w3c.dom.Document JavaDoc;
38 import org.w3c.dom.Element JavaDoc;
39 import org.w3c.dom.Node JavaDoc;
40 import org.w3c.dom.NodeList JavaDoc;
41 import org.xml.sax.SAXException JavaDoc;
42
43 /**
44  * A transformer that prunes the source tree based on <code>include</code> and
45  * <code>exclude</code> XPath expressions.
46  * The <code>include</code> parameter specifies nodes to let through, and
47  * <code>exclude</code> parameter nodes to filter out. Either or both may be
48  * omitted. The default behaviour is for all nodes to be let through
49  * (<code>include</code> = "/").
50  * <p>
51  * This transformer should be declared in the sitemap at
52  * <b>map:sitemap/map:components/map:transformers</b>, as follows<br>
53  * <pre>
54  * &lt;map:transformer logger="sitemap.transformer.xpath" name="xpath" SRC="org.apache.cocoon.transformation.XPathTransformer" /&gt;<br>
55  * </pre>
56  * <h3>Example usage</h3>
57  * As an example, consider a user manual XML file:
58  * <pre>
59  * &lt;manual&gt;
60  * &lt;s1 title="Introduction"&gt;
61  * &lt;p&gt;This is the introduction&lt;/p&gt;
62  * &lt;p&gt;A second paragraph&lt;/p&gt;
63  * &lt;/s1&gt;
64  * &lt;s1 title="Getting started"&gt;
65  * &lt;p&gt;Getting started&lt;/p&gt;
66  * &lt;fixme&gt;Add some content here&lt;/fixme&gt;
67  * &lt;n:note xmlns:n="urn:notes"&gt;banana 1&lt;/n:note&gt; &lt;note&gt;banana 2&lt;/note&gt;
68  * &lt;p&gt;Yes, we have no bananas&lt;/p&gt;
69  * &lt;/s1&gt;
70  * &lt;/manual&gt;
71  * </pre>
72  * We could now deliver named chapters as follows:
73  * <pre>
74  * &lt;map:match pattern="manual/*"&gt;
75  * &lt;map:generate SRC="manual.xml"/&gt;
76  * &lt;map:transform type="xpath"&gt;
77  * &lt;map:parameter name="include" value="/manual/s1[@title='{1}']"/&gt;
78  * &lt;/map:transform&gt;
79  * &lt;map:serialize type="xml"/&gt;
80  * &lt;/map:match&gt;
81  * </pre>
82  * So <code>manual/Introduction</code> would return the first chapter.
83  */

84 public class XPathTransformer
85     extends AbstractDOMTransformer
86     implements CacheableProcessingComponent
87 {
88
89     /** XPath Processor */
90     private XPathProcessor processor = null;
91     private DOMParser parser = null;
92
93     /** XPath specifying nodes to include. Defaults to the root node */
94     protected String JavaDoc include = null;
95     /** XPath specifying nodes to exclude. Defaults to "" (no exclusions) */
96     protected String JavaDoc exclude = null;
97
98     public void setup(SourceResolver resolver, Map JavaDoc objectModel,
99             String JavaDoc source, Parameters parameters)
100         throws ProcessingException, SAXException JavaDoc, IOException JavaDoc {
101         super.setup(resolver, objectModel, source, parameters);
102         this.include = parameters.getParameter("include", "/");
103         this.exclude = parameters.getParameter("exclude", null);
104         }
105
106
107     public void service(ServiceManager manager) throws ServiceException {
108         super.service(manager);
109         try {
110             this.processor = (XPathProcessor)this.manager.lookup(XPathProcessor.ROLE);
111         } catch (Exception JavaDoc e) {
112             getLogger().error("cannot obtain XPathProcessor", e);
113         }
114         try {
115             this.parser = (DOMParser)this.manager.lookup(DOMParser.ROLE);
116         } catch (Exception JavaDoc e) {
117             getLogger().error("cannot obtain DOMParser", e);
118         }
119     }
120
121
122     /** Implementation of a template method declared in AbstractDOMTransformer.
123      * @param doc DOM of XML received by the transformer
124      * @return A pared-down DOM.
125      */

126     protected Document JavaDoc transform(Document JavaDoc doc) {
127         getLogger().debug("Transforming with include='"+include+"', exclude='"+exclude+"'");
128         Document JavaDoc newDoc = null;
129         try {
130             newDoc = handleIncludes(doc, this.include);
131             newDoc = handleExcludes(newDoc, this.exclude);
132         } catch (SAXException JavaDoc se) {
133             // Really ought to be able to propagate these to caller
134
getLogger().error("Error when transforming XML", se);
135             throw new RuntimeException JavaDoc("Error transforming XML. See error log for details: "+se);
136         }
137         return newDoc;
138     }
139
140     /**
141      * Construct a new DOM containing nodes matched by <code>include</code> XPath expression.
142      * @param doc Original DOM
143      * @param xpath XPath include expression
144      * @return DOM containing nodes from <code>doc</code> matched by <code>XPath</code>
145      */

146     private Document JavaDoc handleIncludes(Document JavaDoc doc, String JavaDoc xpath) throws SAXException JavaDoc {
147         if (xpath == null || xpath.equals("/")) {
148             return doc;
149         }
150         Document JavaDoc newDoc = parser.createDocument();
151         NodeList JavaDoc nodes = processor.selectNodeList(doc, xpath);
152         for (int i=0; i<nodes.getLength(); i++) {
153             Node JavaDoc node = nodes.item(i);
154             addNode(newDoc, node);
155         }
156         return newDoc;
157     }
158
159     /**
160      * Construct a new DOM excluding nodes matched by <code>exclude</code> XPath expression.
161      * @param doc Original DOM
162      * @param xpath XPath exclude expression
163      * @return DOM containing nodes from <code>doc</code>, excluding those
164      * matched by <code>XPath</code>
165      */

166     private Document JavaDoc handleExcludes(Document JavaDoc doc, String JavaDoc xpath) {
167         if (xpath == null || xpath.trim().equals("")) {
168             return doc;
169         }
170         NodeList JavaDoc nodes = processor.selectNodeList(doc, xpath);
171         for (int i = 0; i < nodes.getLength(); i++) {
172             Node JavaDoc node = nodes.item(i);
173             // Detach this node. Attr nodes need to be handled specially
174
if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
175                 Attr JavaDoc attrNode = (Attr JavaDoc)node;
176                 Element JavaDoc parent = attrNode.getOwnerElement();
177                 parent.removeAttributeNode(attrNode);
178             } else {
179                 Node JavaDoc parent = node.getParentNode();
180                 if (parent != null) {
181                     parent.removeChild(node);
182                 }
183             }
184         }
185         return doc;
186     }
187
188     /**
189      * Add a node to the Document, including all of the node's ancestor nodes.
190      * Eg, if <code>node</code> is the title="Introduction" node in the
191      * example, the doc would have node /manual/s1/@title='Introduction' added
192      * to it.
193      *
194      * @fixme This method could do with some optimization. Currently, every
195      * node addition results in one expensive node equality check per ancestor
196      * @param doc Document to add a node to
197      * @param nodeTemplate Node from another Document which we wish to
198      * replicate in <code>doc</code>. This is used as a template, not actually
199      * physically copied.
200      */

201     private void addNode(Document JavaDoc doc, final Node JavaDoc nodeTemplate) {
202         // Get a stack of node's ancestors (inclusive)
203
Stack JavaDoc stack = new Stack JavaDoc();
204         Node JavaDoc parent = nodeTemplate;
205         Document JavaDoc oldDoc = nodeTemplate.getOwnerDocument();
206         while (parent != oldDoc) {
207             stack.push(parent);
208             parent = parent.getParentNode();
209         }
210         // Example stack: (top) [ /manual, /manual/s1, /manual/s1/@title ] (bottom)
211

212         // Now from the earliest (root) ancestor, add cloned nodes to the
213
// doc. We check if a suitable ancestor node doesn't already exist in
214
// addNode()
215
parent = doc;
216         while (!stack.empty()) {
217             Node JavaDoc oldNode = (Node JavaDoc)stack.pop();
218             Node JavaDoc newNode = null;
219             if (!stack.empty()) {
220               // Shallow copy o a parent node (in example: /manual, then /manual/s1)
221
newNode = doc.importNode(oldNode, false); // Do a shallow copy
222
copyNamespaceDeclarations(oldNode, newNode);
223               parent = findOrCreateNode(parent, newNode);
224             } else {
225               // Deep copy of the matched node (in example: /manual/s1/@title)
226
newNode = doc.importNode(oldNode, true);
227               copyNamespaceDeclarations(oldNode, newNode);
228               parent.appendChild(newNode);
229             }
230         }
231     }
232
233     /**
234      * Add xmlns namespace declaration attribute to newNode, based on those from oldNode.
235      * It seems that a DOM object built from SAX with namespace-prefixes=false
236      * doesn't have xmlns attribute declarations by default, so we must
237      * manually add them.
238      * @param oldNode Original node, with namespace attributes intact
239      * @param newNode If an Element, this node will have an <code>xmlns</code>
240      * (or <code>xmlns:prefix</code>) attribute added to define the node's namespace.
241      */

242     private void copyNamespaceDeclarations(final Node JavaDoc oldNode, Node JavaDoc newNode) {
243       if (newNode.getNodeType() == Node.ELEMENT_NODE) {
244         String JavaDoc prefix = oldNode.getPrefix();
245         String JavaDoc nsURI = oldNode.getNamespaceURI();
246         Element JavaDoc newElem = (Element JavaDoc)newNode;
247         if (nsURI != null) {
248           if (prefix == null || prefix.equals("")) {
249             if (!newElem.hasAttribute("xmlns")) newElem.setAttribute("xmlns", nsURI);
250           } else {
251             if (!newElem.hasAttribute("xmlns:"+prefix)) newElem.setAttribute("xmlns:"+prefix, nsURI);
252           }
253         }
254       }
255     }
256  
257     /**
258      * Add newNode as a child of parent, first checking if any equivalent node
259      * to newNode already exists as a child of parent.
260      *
261      * @param parent Parent node of found or created node
262      * @param newNode The node potentially added to parent, unless parent
263      * already has an equivalent node
264      * @return the appended node, or the old equivalent node if found.
265      */

266     private Node JavaDoc findOrCreateNode(Node JavaDoc parent, Node JavaDoc newNode) {
267         NodeList JavaDoc otherChildren = parent.getChildNodes();
268         for (int i = 0; i < otherChildren.getLength(); i++) {
269             Node JavaDoc child = otherChildren.item(i);
270             if (nodeEquality(child, newNode)) {
271                 // Found existing equivalent node
272
return child;
273             }
274         }
275         // No existing equivalent node found; add and return newNode
276
parent.appendChild(newNode);
277         return newNode;
278     }
279
280     /**
281      * Shallow-test two nodes for equality.
282      * To quote from the Xerces DOM3 Node.isEqualNode() javadocs, from where
283      * most of the code is filched:
284      *
285      * [Nodes are equal if] the following string attributes are equal:
286      * <code>nodeName</code>, <code>localName</code>,
287      * <code>namespaceURI</code>, <code>prefix</code>, <code>nodeValue</code>.
288      * This is: they are both <code>null</code>, or they have the same length
289      * and are character for character identical.
290      */

291     private boolean nodeEquality(final Node JavaDoc n1, final Node JavaDoc n2) {
292         if (n1.getNodeType() != n2.getNodeType()) {
293             return false;
294         }
295         if (n1.getNodeName() == null) {
296             if (n2.getNodeName() != null) {
297                 return false;
298             }
299         }
300         else if (!n1.getNodeName().equals(n2.getNodeName())) {
301             return false;
302         }
303
304         if (n1.getLocalName() == null) {
305             if (n2.getLocalName() != null) {
306                 return false;
307             }
308         }
309         else if (!n1.getLocalName().equals(n2.getLocalName())) {
310             return false;
311         }
312
313         if (n1.getNamespaceURI() == null) {
314             if (n2.getNamespaceURI() != null) {
315                 return false;
316             }
317         }
318         else if (!n1.getNamespaceURI().equals(n2.getNamespaceURI())) {
319             return false;
320         }
321
322         if (n1.getPrefix() == null) {
323             if (n2.getPrefix() != null) {
324                 return false;
325             }
326         }
327         else if (!n1.getPrefix().equals(n2.getPrefix())) {
328             return false;
329         }
330
331         if (n1.getNodeValue() == null) {
332             if (n2.getNodeValue() != null) {
333                 return false;
334             }
335         }
336         else if (!n1.getNodeValue().equals(n2.getNodeValue())) {
337             return false;
338         }
339         return true;
340     }
341
342     // Unused debugging methods
343

344     private final void printNode(String JavaDoc msg, Node JavaDoc node) {
345         getLogger().info(msg+" "+node.getNodeName());
346     }
347 /*
348     private final void printDeepNode(String msg, Node node) {
349         try {
350             getLogger().info(msg+" "+XMLUtils.serializeNodeToXML(node));
351         } catch (ProcessingException pe) {
352             getLogger().error("Error printing node", pe);
353         }
354     }
355 */

356     // Cache methods
357

358     /**
359      * Generate the unique key.
360      * This key must be unique inside the space of this component.
361      *
362      * @return A hash of the include and exclude parameters, thus uniquely
363      * identifying this XPathTransformer amongst it's peers.
364      */

365     public Serializable JavaDoc getKey() {
366         return ""+HashUtil.hash(this.include+this.exclude);
367     }
368     // For backwards-compat
369
public Serializable JavaDoc generateKey() {
370       return getKey();
371     }
372
373     /**
374      * Generate the validity object.
375      *
376      * @return An "always valid" SourceValidity object. This transformer has no
377      * inputs other than the incoming SAX events.
378      */

379     public SourceValidity getValidity() {
380         return new NOPValidity();
381     }
382
383     // for backwards-compat
384
public SourceValidity generateValidity() {
385       return getValidity();
386     }
387
388     /**
389      * Recycle the component.
390      */

391     public void recycle() {
392         super.recycle();
393         this.include = null;
394         this.exclude = null;
395         // note that we don't turf our parser and processor,
396
}
397
398     /**
399      * dispose
400      */

401     public void dispose() {
402         super.dispose();
403         this.processor = null;
404         this.parser = null;
405         this.include = null;
406         this.exclude = null;
407     }
408 }
409
410
Popular Tags