001: /*
002: * Copyright (c) 2003 The Visigoth Software Society. All rights
003: * reserved.
004: *
005: * Redistribution and use in source and binary forms, with or without
006: * modification, are permitted provided that the following conditions
007: * are met:
008: *
009: * 1. Redistributions of source code must retain the above copyright
010: * notice, this list of conditions and the following disclaimer.
011: *
012: * 2. Redistributions in binary form must reproduce the above copyright
013: * notice, this list of conditions and the following disclaimer in
014: * the documentation and/or other materials provided with the
015: * distribution.
016: *
017: * 3. The end-user documentation included with the redistribution, if
018: * any, must include the following acknowledgement:
019: * "This product includes software developed by the
020: * Visigoth Software Society (http://www.visigoths.org/)."
021: * Alternately, this acknowledgement may appear in the software itself,
022: * if and wherever such third-party acknowledgements normally appear.
023: *
024: * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
025: * project contributors may be used to endorse or promote products derived
026: * from this software without prior written permission. For written
027: * permission, please contact visigoths@visigoths.org.
028: *
029: * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
030: * nor may "FreeMarker" or "Visigoth" appear in their names
031: * without prior written permission of the Visigoth Software Society.
032: *
033: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
034: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
035: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
036: * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
037: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
038: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
039: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
040: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
041: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
042: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
043: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
044: * SUCH DAMAGE.
045: * ====================================================================
046: *
047: * This software consists of voluntary contributions made by many
048: * individuals on behalf of the Visigoth Software Society. For more
049: * information on the Visigoth Software Society, please see
050: * http://www.visigoths.org/
051: */
052:
053: package freemarker.ext.dom;
054:
055: import java.io.File;
056: import java.io.IOException;
057: import java.lang.ref.WeakReference;
058: import java.util.Collections;
059: import java.util.List;
060: import java.util.Map;
061: import java.util.WeakHashMap;
062:
063: import javax.xml.parsers.DocumentBuilder;
064: import javax.xml.parsers.DocumentBuilderFactory;
065: import javax.xml.parsers.ParserConfigurationException;
066:
067: import org.w3c.dom.Attr;
068: import org.w3c.dom.CDATASection;
069: import org.w3c.dom.CharacterData;
070: import org.w3c.dom.Document;
071: import org.w3c.dom.DocumentType;
072: import org.w3c.dom.Element;
073: import org.w3c.dom.Node;
074: import org.w3c.dom.NodeList;
075: import org.w3c.dom.ProcessingInstruction;
076: import org.w3c.dom.Text;
077: import org.xml.sax.ErrorHandler;
078: import org.xml.sax.InputSource;
079: import org.xml.sax.SAXException;
080:
081: import freemarker.ext.util.WrapperTemplateModel;
082: import freemarker.log.Logger;
083: import freemarker.template.AdapterTemplateModel;
084: import freemarker.template.SimpleScalar;
085: import freemarker.template.TemplateHashModel;
086: import freemarker.template.TemplateModel;
087: import freemarker.template.TemplateModelException;
088: import freemarker.template.TemplateNodeModel;
089: import freemarker.template.TemplateSequenceModel;
090:
091: /**
092: * A base class for wrapping a W3C DOM Node as a FreeMarker template model.
093: * @author <a href="mailto:jon@revusky.com">Jonathan Revusky</a>
094: * @version $Id: NodeModel.java,v 1.80 2005/06/22 11:33:31 ddekany Exp $
095: */
096: abstract public class NodeModel implements TemplateNodeModel,
097: TemplateHashModel, TemplateSequenceModel, AdapterTemplateModel,
098: WrapperTemplateModel {
099:
100: static final Logger logger = Logger.getLogger("freemarker.dom");
101:
102: static private DocumentBuilderFactory docBuilderFactory;
103:
104: static private Map xpathSupportMap = Collections
105: .synchronizedMap(new WeakHashMap());
106:
107: static private XPathSupport jaxenXPathSupport;
108:
109: static private ErrorHandler errorHandler;
110:
111: static Class xpathSupportClass;
112:
113: static {
114: try {
115: useDefaultXPathSupport();
116: } catch (Exception e) {
117: // do nothing
118: }
119: if (xpathSupportClass == null && logger.isWarnEnabled()) {
120: logger.warn("No XPath support is available.");
121: }
122: }
123:
124: /**
125: * The W3C DOM Node being wrapped.
126: */
127: final Node node;
128: private TemplateSequenceModel children;
129: private NodeModel parent;
130:
131: /**
132: * Sets the DOM Parser implementation to be used when building NodeModel
133: * objects from XML files.
134: */
135: static public void setDocumentBuilderFactory(
136: DocumentBuilderFactory docBuilderFactory) {
137: NodeModel.docBuilderFactory = docBuilderFactory;
138: }
139:
140: /**
141: * @return the DOM Parser implementation that is used when
142: * building NodeModel objects from XML files.
143: */
144: static public DocumentBuilderFactory getDocumentBuilderFactory() {
145: if (docBuilderFactory == null) {
146: docBuilderFactory = DocumentBuilderFactory.newInstance();
147: docBuilderFactory.setNamespaceAware(true);
148: docBuilderFactory.setIgnoringElementContentWhitespace(true);
149: }
150: return docBuilderFactory;
151: }
152:
153: /**
154: * sets the error handler to use when parsing the document.
155: */
156: static public void setErrorHandler(ErrorHandler errorHandler) {
157: NodeModel.errorHandler = errorHandler;
158: }
159:
160: /**
161: * Create a NodeModel from a SAX input source. Adjacent text nodes will be merged (and CDATA sections
162: * are considered as text nodes).
163: * @param removeComments whether to remove all comment nodes
164: * (recursively) from the tree before processing
165: * @param removePIs whether to remove all processing instruction nodes
166: * (recursively from the tree before processing
167: */
168: static public NodeModel parse(InputSource is,
169: boolean removeComments, boolean removePIs)
170: throws SAXException, IOException,
171: ParserConfigurationException {
172: DocumentBuilder builder = getDocumentBuilderFactory()
173: .newDocumentBuilder();
174: if (errorHandler != null)
175: builder.setErrorHandler(errorHandler);
176: Document doc = builder.parse(is);
177: if (removeComments && removePIs) {
178: simplify(doc);
179: } else {
180: if (removeComments) {
181: removeComments(doc);
182: }
183: if (removePIs) {
184: removePIs(doc);
185: }
186: mergeAdjacentText(doc);
187: }
188: return wrap(doc);
189: }
190:
191: /**
192: * Create a NodeModel from an XML input source. By default,
193: * all comments and processing instruction nodes are
194: * stripped from the tree.
195: */
196: static public NodeModel parse(InputSource is) throws SAXException,
197: IOException, ParserConfigurationException {
198: return parse(is, true, true);
199: }
200:
201: /**
202: * Create a NodeModel from an XML file.
203: * @param removeComments whether to remove all comment nodes
204: * (recursively) from the tree before processing
205: * @param removePIs whether to remove all processing instruction nodes
206: * (recursively from the tree before processing
207: */
208: static public NodeModel parse(File f, boolean removeComments,
209: boolean removePIs) throws SAXException, IOException,
210: ParserConfigurationException {
211: DocumentBuilder builder = getDocumentBuilderFactory()
212: .newDocumentBuilder();
213: if (errorHandler != null)
214: builder.setErrorHandler(errorHandler);
215: Document doc = builder.parse(f);
216: if (removeComments) {
217: removeComments(doc);
218: }
219: if (removePIs) {
220: removePIs(doc);
221: }
222: mergeAdjacentText(doc);
223: return wrap(doc);
224: }
225:
226: /**
227: * Create a NodeModel from an XML file. By default,
228: * all comments and processing instruction nodes are
229: * stripped from the tree.
230: */
231: static public NodeModel parse(File f) throws SAXException,
232: IOException, ParserConfigurationException {
233: return parse(f, true, true);
234: }
235:
236: protected NodeModel(Node node) {
237: this .node = node;
238: }
239:
240: /**
241: * @return the underling W3C DOM Node object that this TemplateNodeModel
242: * is wrapping.
243: */
244: public Node getNode() {
245: return node;
246: }
247:
248: public TemplateModel get(String key) throws TemplateModelException {
249: if (key.startsWith("@@")) {
250: if (key.equals("@@text")) {
251: return new SimpleScalar(getText(node));
252: }
253: if (key.equals("@@namespace")) {
254: String nsURI = node.getNamespaceURI();
255: return nsURI == null ? null : new SimpleScalar(nsURI);
256: }
257: if (key.equals("@@local_name")) {
258: String localName = node.getLocalName();
259: if (localName == null) {
260: localName = getNodeName();
261: }
262: return new SimpleScalar(localName);
263: }
264: if (key.equals("@@markup")) {
265: StringBuffer buf = new StringBuffer();
266: NodeOutputter nu = new NodeOutputter(node);
267: nu.outputContent(node, buf);
268: return new SimpleScalar(buf.toString());
269: }
270: if (key.equals("@@nested_markup")) {
271: StringBuffer buf = new StringBuffer();
272: NodeOutputter nu = new NodeOutputter(node);
273: nu.outputContent(node.getChildNodes(), buf);
274: return new SimpleScalar(buf.toString());
275: }
276: if (key.equals("@@qname")) {
277: String qname = getQualifiedName();
278: return qname == null ? null : new SimpleScalar(qname);
279: }
280: }
281: XPathSupport xps = getXPathSupport();
282: if (xps != null) {
283: return xps.executeQuery(node, key);
284: } else {
285: throw new TemplateModelException(
286: "Can't try to resolve the XML query key, because no XPath support is available. "
287: + "It's either malformed or an XPath expression: "
288: + key);
289: }
290: }
291:
292: public TemplateNodeModel getParentNode() {
293: if (parent == null) {
294: Node parentNode = node.getParentNode();
295: if (parentNode == null) {
296: if (node instanceof Attr) {
297: parentNode = ((Attr) node).getOwnerElement();
298: }
299: }
300: parent = wrap(parentNode);
301: }
302: return parent;
303: }
304:
305: public TemplateSequenceModel getChildNodes() {
306: if (children == null) {
307: children = new NodeListModel(node.getChildNodes(), this );
308: }
309: return children;
310: }
311:
312: public final String getNodeType() throws TemplateModelException {
313: short nodeType = node.getNodeType();
314: switch (nodeType) {
315: case Node.ATTRIBUTE_NODE:
316: return "attribute";
317: case Node.CDATA_SECTION_NODE:
318: return "text";
319: case Node.COMMENT_NODE:
320: return "comment";
321: case Node.DOCUMENT_FRAGMENT_NODE:
322: return "document_fragment";
323: case Node.DOCUMENT_NODE:
324: return "document";
325: case Node.DOCUMENT_TYPE_NODE:
326: return "document_type";
327: case Node.ELEMENT_NODE:
328: return "element";
329: case Node.ENTITY_NODE:
330: return "entity";
331: case Node.ENTITY_REFERENCE_NODE:
332: return "entity_reference";
333: case Node.NOTATION_NODE:
334: return "notation";
335: case Node.PROCESSING_INSTRUCTION_NODE:
336: return "pi";
337: case Node.TEXT_NODE:
338: return "text";
339: }
340: throw new TemplateModelException("Unknown node type: "
341: + nodeType + ". This should be impossible!");
342: }
343:
344: public TemplateModel exec(List args) throws TemplateModelException {
345: if (args.size() != 1) {
346: throw new TemplateModelException(
347: "Expecting exactly one arguments");
348: }
349: String query = (String) args.get(0);
350: // Now, we try to behave as if this is an XPath expression
351: XPathSupport xps = getXPathSupport();
352: if (xps == null) {
353: throw new TemplateModelException(
354: "No XPath support available");
355: }
356: return xps.executeQuery(node, query);
357: }
358:
359: public final int size() {
360: return 1;
361: }
362:
363: public final TemplateModel get(int i) {
364: return i == 0 ? this : null;
365: }
366:
367: public String getNodeNamespace() {
368: int nodeType = node.getNodeType();
369: if (nodeType != Node.ATTRIBUTE_NODE
370: && nodeType != Node.ELEMENT_NODE) {
371: return null;
372: }
373: String result = node.getNamespaceURI();
374: if (result == null && nodeType == Node.ELEMENT_NODE) {
375: result = "";
376: } else if ("".equals(result) && nodeType == Node.ATTRIBUTE_NODE) {
377: result = null;
378: }
379: return result;
380: }
381:
382: public final int hashCode() {
383: return node.hashCode();
384: }
385:
386: public boolean equals(Object other) {
387: if (other == null)
388: return false;
389: return other.getClass() == this .getClass()
390: && ((NodeModel) other).node.equals(this .node);
391: }
392:
393: static public NodeModel wrap(Node node) {
394: if (node == null) {
395: return null;
396: }
397: NodeModel result = null;
398: switch (node.getNodeType()) {
399: case Node.DOCUMENT_NODE:
400: result = new DocumentModel((Document) node);
401: break;
402: case Node.ELEMENT_NODE:
403: result = new ElementModel((Element) node);
404: break;
405: case Node.ATTRIBUTE_NODE:
406: result = new AttributeNodeModel((Attr) node);
407: break;
408: case Node.CDATA_SECTION_NODE:
409: case Node.COMMENT_NODE:
410: case Node.TEXT_NODE:
411: result = new CharacterDataNodeModel(
412: (org.w3c.dom.CharacterData) node);
413: break;
414: case Node.PROCESSING_INSTRUCTION_NODE:
415: result = new PINodeModel((ProcessingInstruction) node);
416: break;
417: case Node.DOCUMENT_TYPE_NODE:
418: result = new DocumentTypeModel((DocumentType) node);
419: break;
420: }
421: return result;
422: }
423:
424: /**
425: * Recursively removes all comment nodes
426: * from the subtree.
427: *
428: * @see #simplify
429: */
430: static public void removeComments(Node node) {
431: NodeList children = node.getChildNodes();
432: int i = 0;
433: int len = children.getLength();
434: while (i < len) {
435: Node child = children.item(i);
436: if (child.hasChildNodes()) {
437: removeComments(child);
438: i++;
439: } else {
440: if (child.getNodeType() == Node.COMMENT_NODE) {
441: node.removeChild(child);
442: len--;
443: } else {
444: i++;
445: }
446: }
447: }
448: }
449:
450: /**
451: * Recursively removes all processing instruction nodes
452: * from the subtree.
453: *
454: * @see #simplify
455: */
456: static public void removePIs(Node node) {
457: NodeList children = node.getChildNodes();
458: int i = 0;
459: int len = children.getLength();
460: while (i < len) {
461: Node child = children.item(i);
462: if (child.hasChildNodes()) {
463: removePIs(child);
464: i++;
465: } else {
466: if (child.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
467: node.removeChild(child);
468: len--;
469: } else {
470: i++;
471: }
472: }
473: }
474: }
475:
476: /**
477: * Merges adjacent text/cdata nodes, so that there are no
478: * adjacent text/cdata nodes. Operates recursively
479: * on the entire subtree. You thus lose information
480: * about any CDATA sections occurring in the doc.
481: *
482: * @see #simplify
483: */
484: static public void mergeAdjacentText(Node node) {
485: Node child = node.getFirstChild();
486: while (child != null) {
487: if (child instanceof Text || child instanceof CDATASection) {
488: Node next = child.getNextSibling();
489: if (next instanceof Text
490: || next instanceof CDATASection) {
491: String fullText = child.getNodeValue()
492: + next.getNodeValue();
493: ((CharacterData) child).setData(fullText);
494: node.removeChild(next);
495: }
496: } else {
497: mergeAdjacentText(child);
498: }
499: child = child.getNextSibling();
500: }
501: }
502:
503: /**
504: * Removes comments and processing instruction, and then unites adjacent text nodes.
505: * Note that CDATA sections count as text nodes.
506: */
507: static public void simplify(Node node) {
508: NodeList children = node.getChildNodes();
509: int i = 0;
510: int len = children.getLength();
511: Node prevTextChild = null;
512: while (i < len) {
513: Node child = children.item(i);
514: if (child.hasChildNodes()) {
515: simplify(child);
516: prevTextChild = null;
517: i++;
518: } else {
519: int type = child.getNodeType();
520: if (type == Node.PROCESSING_INSTRUCTION_NODE) {
521: node.removeChild(child);
522: len--;
523: } else if (type == Node.COMMENT_NODE) {
524: node.removeChild(child);
525: len--;
526: } else if (type == Node.TEXT_NODE
527: || type == Node.CDATA_SECTION_NODE) {
528: if (prevTextChild != null) {
529: CharacterData ptc = (CharacterData) prevTextChild;
530: ptc.setData(ptc.getNodeValue()
531: + child.getNodeValue());
532: node.removeChild(child);
533: len--;
534: } else {
535: prevTextChild = child;
536: i++;
537: }
538: } else {
539: prevTextChild = null;
540: i++;
541: }
542: }
543: }
544: }
545:
546: NodeModel getDocumentNodeModel() {
547: if (node instanceof Document) {
548: return this ;
549: } else {
550: return wrap(node.getOwnerDocument());
551: }
552: }
553:
554: /**
555: * Tells the system to use (restore) the default (initial) XPath system used by
556: * this FreeMarker version on this system.
557: */
558: static public void useDefaultXPathSupport() {
559: xpathSupportClass = null;
560: jaxenXPathSupport = null;
561: try {
562: useXalanXPathSupport();
563: } catch (Exception e) {
564: ; // ignore
565: }
566: if (xpathSupportClass == null)
567: try {
568: useJaxenXPathSupport();
569: } catch (Exception e) {
570: ; // ignore
571: }
572: }
573:
574: /**
575: * Convenience method. Tells the system to use Jaxen for XPath queries.
576: * @throws Exception if the Jaxen classes are not present.
577: */
578: static public void useJaxenXPathSupport() throws Exception {
579: Class.forName("org.jaxen.dom.DOMXPath");
580: Class c = Class.forName("freemarker.ext.dom.JaxenXPathSupport");
581: jaxenXPathSupport = (XPathSupport) c.newInstance();
582: if (logger.isDebugEnabled()) {
583: logger.debug("Using Jaxen classes for XPath support");
584: }
585: xpathSupportClass = c;
586: }
587:
588: /**
589: * Convenience method. Tells the system to use Xalan for XPath queries.
590: * @throws Exception if the Xalan XPath classes are not present.
591: */
592: static public void useXalanXPathSupport() throws Exception {
593: Class.forName("org.apache.xpath.XPath");
594: Class c = Class.forName("freemarker.ext.dom.XalanXPathSupport");
595: if (logger.isDebugEnabled()) {
596: logger.debug("Using Xalan classes for XPath support");
597: }
598: xpathSupportClass = c;
599: }
600:
601: /**
602: * Set an alternative implementation of freemarker.ext.dom.XPathSupport to use
603: * as the XPath engine.
604: * @param cl the class, or <code>null</code> to disable XPath support.
605: */
606: static public void setXPathSupportClass(Class cl) {
607: if (cl != null && !cl.isAssignableFrom(XPathSupport.class)) {
608: throw new RuntimeException(
609: "Class "
610: + cl.getName()
611: + " does not implement freemarker.ext.dom.XPathSupport");
612: }
613: xpathSupportClass = cl;
614: }
615:
616: /**
617: * Get the currently used freemarker.ext.dom.XPathSupport used as the XPath engine.
618: * Returns <code>null</code> if XPath support is disabled.
619: */
620: static public Class getXPathSupportClass() {
621: return xpathSupportClass;
622: }
623:
624: static private String getText(Node node) {
625: String result = "";
626: if (node instanceof Text || node instanceof CDATASection) {
627: result = ((org.w3c.dom.CharacterData) node).getData();
628: } else if (node instanceof Element) {
629: NodeList children = node.getChildNodes();
630: for (int i = 0; i < children.getLength(); i++) {
631: result += getText(children.item(i));
632: }
633: } else if (node instanceof Document) {
634: result = getText(((Document) node).getDocumentElement());
635: }
636: return result;
637: }
638:
639: XPathSupport getXPathSupport() {
640: if (jaxenXPathSupport != null) {
641: return jaxenXPathSupport;
642: }
643: XPathSupport xps = null;
644: Document doc = node.getOwnerDocument();
645: if (doc == null) {
646: doc = (Document) node;
647: }
648: synchronized (doc) {
649: WeakReference ref = (WeakReference) xpathSupportMap
650: .get(doc);
651: if (ref != null) {
652: xps = (XPathSupport) ref.get();
653: }
654: if (xps == null) {
655: try {
656: xps = (XPathSupport) xpathSupportClass
657: .newInstance();
658: xpathSupportMap.put(doc, new WeakReference(xps));
659: } catch (Exception e) {
660: logger
661: .error("Error instantiating xpathSupport class");
662: }
663: }
664: }
665: return xps;
666: }
667:
668: String getQualifiedName() throws TemplateModelException {
669: return getNodeName();
670: }
671:
672: public Object getAdaptedObject(Class hint) {
673: return node;
674: }
675:
676: public Object getWrappedObject() {
677: return node;
678: }
679: }
|