http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59412b29/src/main/java/org/apache/freemarker/dom/NodeModel.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/dom/NodeModel.java 
b/src/main/java/org/apache/freemarker/dom/NodeModel.java
new file mode 100644
index 0000000..5077285
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/dom/NodeModel.java
@@ -0,0 +1,618 @@
+/*
+ * 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.freemarker.dom;
+
+
+import java.lang.ref.WeakReference;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core._UnexpectedTypeErrorExplainerTemplateModel;
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateNodeModelEx;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.slf4j.Logger;
+import org.w3c.dom.Attr;
+import org.w3c.dom.CDATASection;
+import org.w3c.dom.CharacterData;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentType;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.ProcessingInstruction;
+import org.w3c.dom.Text;
+
+/**
+ * A base class for wrapping a single W3C DOM_WRAPPER Node as a FreeMarker 
template model.
+ * 
+ * <p>
+ * Note that {@link DefaultObjectWrapper} automatically wraps W3C DOM_WRAPPER 
{@link Node}-s into this, so you may need do that
+ * with this class manually. However, before dropping the {@link Node}-s into 
the data-model, you certainly want to
+ * apply {@link NodeModel#simplify(Node)} on them.
+ * 
+ * <p>
+ * This class is not guaranteed to be thread safe, so instances of this 
shouldn't be used as shared variable (
+ * {@link Configuration#setSharedVariable(String, Object)}).
+ * 
+ * <p>
+ * To represent a node sequence (such as a query result) of exactly 1 nodes, 
this class should be used instead of
+ * {@link NodeListModel}, as it adds extra capabilities by utilizing that we 
have exactly 1 node. If you need to wrap a
+ * node sequence of 0 or multiple nodes, you must use {@link NodeListModel}.
+ */
+abstract public class NodeModel
+implements TemplateNodeModelEx, TemplateHashModel, TemplateSequenceModel,
+    AdapterTemplateModel, WrapperTemplateModel, 
_UnexpectedTypeErrorExplainerTemplateModel {
+
+    static private final Logger LOG = DomLog.LOG;
+
+    private static final Object STATIC_LOCK = new Object();
+    
+    static private final Map xpathSupportMap = Collections.synchronizedMap(new 
WeakHashMap());
+    
+    static private XPathSupport jaxenXPathSupport;
+    
+    static Class xpathSupportClass;
+    
+    static {
+        try {
+            useDefaultXPathSupport();
+        } catch (Exception e) {
+            // do nothing
+        }
+        if (xpathSupportClass == null && LOG.isWarnEnabled()) {
+            LOG.warn("No XPath support is available.");
+        }
+    }
+    
+    /**
+     * The W3C DOM_WRAPPER Node being wrapped.
+     */
+    final Node node;
+    private TemplateSequenceModel children;
+    private NodeModel parent;
+    
+    protected NodeModel(Node node) {
+        this.node = node;
+    }
+    
+    /**
+     * @return the underling W3C DOM_WRAPPER Node object that this 
TemplateNodeModel
+     * is wrapping.
+     */
+    public Node getNode() {
+        return node;
+    }
+    
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        if (key.startsWith("@@")) {
+            if (key.equals(AtAtKey.TEXT.getKey())) {
+                return new SimpleScalar(getText(node));
+            } else if (key.equals(AtAtKey.NAMESPACE.getKey())) {
+                String nsURI = node.getNamespaceURI();
+                return nsURI == null ? null : new SimpleScalar(nsURI);
+            } else if (key.equals(AtAtKey.LOCAL_NAME.getKey())) {
+                String localName = node.getLocalName();
+                if (localName == null) {
+                    localName = getNodeName();
+                }
+                return new SimpleScalar(localName);
+            } else if (key.equals(AtAtKey.MARKUP.getKey())) {
+                StringBuilder buf = new StringBuilder();
+                NodeOutputter nu = new NodeOutputter(node);
+                nu.outputContent(node, buf);
+                return new SimpleScalar(buf.toString());
+            } else if (key.equals(AtAtKey.NESTED_MARKUP.getKey())) {
+                StringBuilder buf = new StringBuilder();
+                NodeOutputter nu = new NodeOutputter(node);
+                nu.outputContent(node.getChildNodes(), buf);
+                return new SimpleScalar(buf.toString());
+            } else if (key.equals(AtAtKey.QNAME.getKey())) {
+                String qname = getQualifiedName();
+                return qname != null ? new SimpleScalar(qname) : null;
+            } else {
+                // As @@... would cause exception in the XPath engine, we 
throw a nicer exception now. 
+                if (AtAtKey.containsKey(key)) {
+                    throw new TemplateModelException(
+                            "\"" + key + "\" is not supported for an XML node 
of type \"" + getNodeType() + "\".");
+                } else {
+                    throw new TemplateModelException("Unsupported @@ key: " + 
key);
+                }
+            }
+        } else {
+            XPathSupport xps = getXPathSupport();
+            if (xps != null) {
+                return xps.executeQuery(node, key);
+            } else {
+                throw new TemplateModelException(
+                        "Can't try to resolve the XML query key, because no 
XPath support is available. "
+                        + "This is either malformed or an XPath expression: " 
+ key);
+            }
+        }
+    }
+    
+    @Override
+    public TemplateNodeModel getParentNode() {
+        if (parent == null) {
+            Node parentNode = node.getParentNode();
+            if (parentNode == null) {
+                if (node instanceof Attr) {
+                    parentNode = ((Attr) node).getOwnerElement();
+                }
+            }
+            parent = wrap(parentNode);
+        }
+        return parent;
+    }
+
+    @Override
+    public TemplateNodeModelEx getPreviousSibling() throws 
TemplateModelException {
+        return wrap(node.getPreviousSibling());
+    }
+
+    @Override
+    public TemplateNodeModelEx getNextSibling() throws TemplateModelException {
+        return wrap(node.getNextSibling());
+    }
+
+    @Override
+    public TemplateSequenceModel getChildNodes() {
+        if (children == null) {
+            children = new NodeListModel(node.getChildNodes(), this);
+        }
+        return children;
+    }
+    
+    @Override
+    public final String getNodeType() throws TemplateModelException {
+        short nodeType = node.getNodeType();
+        switch (nodeType) {
+            case Node.ATTRIBUTE_NODE : return "attribute";
+            case Node.CDATA_SECTION_NODE : return "text";
+            case Node.COMMENT_NODE : return "comment";
+            case Node.DOCUMENT_FRAGMENT_NODE : return "document_fragment";
+            case Node.DOCUMENT_NODE : return "document";
+            case Node.DOCUMENT_TYPE_NODE : return "document_type";
+            case Node.ELEMENT_NODE : return "element";
+            case Node.ENTITY_NODE : return "entity";
+            case Node.ENTITY_REFERENCE_NODE : return "entity_reference";
+            case Node.NOTATION_NODE : return "notation";
+            case Node.PROCESSING_INSTRUCTION_NODE : return "pi";
+            case Node.TEXT_NODE : return "text";
+        }
+        throw new TemplateModelException("Unknown node type: " + nodeType + ". 
This should be impossible!");
+    }
+    
+    public TemplateModel exec(List args) throws TemplateModelException {
+        if (args.size() != 1) {
+            throw new TemplateModelException("Expecting exactly one 
arguments");
+        }
+        String query = (String) args.get(0);
+        // Now, we try to behave as if this is an XPath expression
+        XPathSupport xps = getXPathSupport();
+        if (xps == null) {
+            throw new TemplateModelException("No XPath support available");
+        }
+        return xps.executeQuery(node, query);
+    }
+    
+    /**
+     * Always returns 1.
+     */
+    @Override
+    public final int size() {
+        return 1;
+    }
+    
+    @Override
+    public final TemplateModel get(int i) {
+        return i == 0 ? this : null;
+    }
+    
+    @Override
+    public String getNodeNamespace() {
+        int nodeType = node.getNodeType();
+        if (nodeType != Node.ATTRIBUTE_NODE && nodeType != Node.ELEMENT_NODE) 
{ 
+            return null;
+        }
+        String result = node.getNamespaceURI();
+        if (result == null && nodeType == Node.ELEMENT_NODE) {
+            result = "";
+        } else if ("".equals(result) && nodeType == Node.ATTRIBUTE_NODE) {
+            result = null;
+        }
+        return result;
+    }
+    
+    @Override
+    public final int hashCode() {
+        return node.hashCode();
+    }
+    
+    @Override
+    public boolean equals(Object other) {
+        if (other == null) return false;
+        return other.getClass() == getClass()
+                && ((NodeModel) other).node.equals(node);
+    }
+    
+    /**
+     * Creates a {@link NodeModel} from a DOM {@link Node}. It's strongly 
recommended modify the {@link Node} with
+     * {@link #simplify(Node)}, so the DOM will be easier to process in 
templates.
+     * 
+     * @param node
+     *            The DOM node to wrap. This is typically an {@link Element} 
or a {@link Document}, but all kind of node
+     *            types are supported. If {@code null}, {@code null} will be 
returned.
+     */
+    static public NodeModel wrap(Node node) {
+        if (node == null) {
+            return null;
+        }
+        NodeModel result = null;
+        switch (node.getNodeType()) {
+            case Node.DOCUMENT_NODE : result = new DocumentModel((Document) 
node); break;
+            case Node.ELEMENT_NODE : result = new ElementModel((Element) 
node); break;
+            case Node.ATTRIBUTE_NODE : result = new AttributeNodeModel((Attr) 
node); break;
+            case Node.CDATA_SECTION_NODE : 
+            case Node.COMMENT_NODE :
+            case Node.TEXT_NODE : result = new 
CharacterDataNodeModel((org.w3c.dom.CharacterData) node); break;
+            case Node.PROCESSING_INSTRUCTION_NODE : result = new 
PINodeModel((ProcessingInstruction) node); break;
+            case Node.DOCUMENT_TYPE_NODE : result = new 
DocumentTypeModel((DocumentType) node); break;
+            default: throw new IllegalArgumentException(
+                    "Unsupported node type: " + node.getNodeType() + " ("
+                    + node.getClass().getName() + ")");
+        }
+        return result;
+    }
+    
+    /**
+     * Recursively removes all comment nodes from the subtree.
+     *
+     * @see #simplify
+     */
+    static public void removeComments(Node parent) {
+        Node child = parent.getFirstChild();
+        while (child != null) {
+            Node nextSibling = child.getNextSibling();
+            if (child.getNodeType() == Node.COMMENT_NODE) {
+                parent.removeChild(child);
+            } else if (child.hasChildNodes()) {
+                removeComments(child);
+            }
+            child = nextSibling;
+        }
+    }
+    
+    /**
+     * Recursively removes all processing instruction nodes from the subtree.
+     *
+     * @see #simplify
+     */
+    static public void removePIs(Node parent) {
+        Node child = parent.getFirstChild();
+        while (child != null) {
+            Node nextSibling = child.getNextSibling();
+            if (child.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
+                parent.removeChild(child);
+            } else if (child.hasChildNodes()) {
+                removePIs(child);
+            }
+            child = nextSibling;
+        }
+    }
+    
+    /**
+     * Merges adjacent text nodes (where CDATA counts as text node too). 
Operates recursively on the entire subtree.
+     * The merged node will have the type of the first node of the adjacent 
merged nodes.
+     * 
+     * <p>Because XPath assumes that there are no adjacent text nodes in the 
tree, not doing this can have
+     * undesirable side effects. Xalan queries like {@code text()} will only 
return the first of a list of matching
+     * adjacent text nodes instead of all of them, while Jaxen will return all 
of them as intuitively expected. 
+     *
+     * @see #simplify
+     */
+    static public void mergeAdjacentText(Node parent) {
+        mergeAdjacentText(parent, new StringBuilder(0));
+    }
+    
+    static private void mergeAdjacentText(Node parent, StringBuilder 
collectorBuf) {
+        Node child = parent.getFirstChild();
+        while (child != null) {
+            Node next = child.getNextSibling();
+            if (child instanceof Text) {
+                boolean atFirstText = true;
+                while (next instanceof Text) { //
+                    if (atFirstText) {
+                        collectorBuf.setLength(0);
+                        
collectorBuf.ensureCapacity(child.getNodeValue().length() + 
next.getNodeValue().length());
+                        collectorBuf.append(child.getNodeValue());
+                        atFirstText = false;
+                    }
+                    collectorBuf.append(next.getNodeValue());
+                    
+                    parent.removeChild(next);
+                    
+                    next = child.getNextSibling();
+                }
+                if (!atFirstText && collectorBuf.length() != 0) {
+                    ((CharacterData) child).setData(collectorBuf.toString());
+                }
+            } else {
+                mergeAdjacentText(child, collectorBuf);
+            }
+            child = next;
+        }
+    }
+    
+    /**
+     * Removes all comments and processing instruction, and unites adjacent 
text nodes (here CDATA counts as text as
+     * well). This is similar to applying {@link #removeComments(Node)}, 
{@link #removePIs(Node)}, and finally
+     * {@link #mergeAdjacentText(Node)}, but it does all that somewhat faster.
+     */    
+    static public void simplify(Node parent) {
+        simplify(parent, new StringBuilder(0));
+    }
+    
+    static private void simplify(Node parent, StringBuilder 
collectorTextChildBuff) {
+        Node collectorTextChild = null;
+        Node child = parent.getFirstChild();
+        while (child != null) {
+            Node next = child.getNextSibling();
+            if (child.hasChildNodes()) {
+                if (collectorTextChild != null) {
+                    // Commit pending text node merge:
+                    if (collectorTextChildBuff.length() != 0) {
+                        ((CharacterData) 
collectorTextChild).setData(collectorTextChildBuff.toString());
+                        collectorTextChildBuff.setLength(0);
+                    }
+                    collectorTextChild = null;
+                }
+                
+                simplify(child, collectorTextChildBuff);
+            } else {
+                int type = child.getNodeType();
+                if (type == Node.TEXT_NODE || type == Node.CDATA_SECTION_NODE 
) {
+                    if (collectorTextChild != null) {
+                        if (collectorTextChildBuff.length() == 0) {
+                            collectorTextChildBuff.ensureCapacity(
+                                    collectorTextChild.getNodeValue().length() 
+ child.getNodeValue().length());
+                            
collectorTextChildBuff.append(collectorTextChild.getNodeValue());
+                        }
+                        collectorTextChildBuff.append(child.getNodeValue());
+                        parent.removeChild(child);
+                    } else {
+                        collectorTextChild = child;
+                        collectorTextChildBuff.setLength(0);
+                    }
+                } else if (type == Node.COMMENT_NODE) {
+                    parent.removeChild(child);
+                } else if (type == Node.PROCESSING_INSTRUCTION_NODE) {
+                    parent.removeChild(child);
+                } else if (collectorTextChild != null) {
+                    // Commit pending text node merge:
+                    if (collectorTextChildBuff.length() != 0) {
+                        ((CharacterData) 
collectorTextChild).setData(collectorTextChildBuff.toString());
+                        collectorTextChildBuff.setLength(0);
+                    }
+                    collectorTextChild = null;
+                }
+            }
+            child = next;
+        }
+        
+        if (collectorTextChild != null) {
+            // Commit pending text node merge:
+            if (collectorTextChildBuff.length() != 0) {
+                ((CharacterData) 
collectorTextChild).setData(collectorTextChildBuff.toString());
+                collectorTextChildBuff.setLength(0);
+            }
+        }
+    }
+    
+    NodeModel getDocumentNodeModel() {
+        if (node instanceof Document) {
+            return this;
+        } else {
+            return wrap(node.getOwnerDocument());
+        }
+    }
+
+    /**
+     * Tells the system to use (restore) the default (initial) XPath system 
used by
+     * this FreeMarker version on this system.
+     */
+    static public void useDefaultXPathSupport() {
+        synchronized (STATIC_LOCK) {
+            xpathSupportClass = null;
+            jaxenXPathSupport = null;
+            try {
+                useXalanXPathSupport();
+            } catch (Exception e) {
+                // ignore
+            }
+            if (xpathSupportClass == null) try {
+               useSunInternalXPathSupport();
+            } catch (Exception e) {
+                // ignore
+            }
+            if (xpathSupportClass == null) try {
+                useJaxenXPathSupport();
+            } catch (Exception e) {
+                // ignore
+            }
+        }
+    }
+    
+    /**
+     * Convenience method. Tells the system to use Jaxen for XPath queries.
+     * @throws Exception if the Jaxen classes are not present.
+     */
+    static public void useJaxenXPathSupport() throws Exception {
+        Class.forName("org.jaxen.dom.DOMXPath");
+        Class c = Class.forName("org.apache.freemarker.dom.JaxenXPathSupport");
+        jaxenXPathSupport = (XPathSupport) c.newInstance();
+        synchronized (STATIC_LOCK) {
+            xpathSupportClass = c;
+        }
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("Using Jaxen classes for XPath support");
+        }
+    }
+    
+    /**
+     * Convenience method. Tells the system to use Xalan for XPath queries.
+     * @throws Exception if the Xalan XPath classes are not present.
+     */
+    static public void useXalanXPathSupport() throws Exception {
+        Class.forName("org.apache.xpath.XPath");
+        Class c = Class.forName("org.apache.freemarker.dom.XalanXPathSupport");
+        synchronized (STATIC_LOCK) {
+            xpathSupportClass = c;
+        }
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("Using Xalan classes for XPath support");
+        }
+    }
+    
+    static public void useSunInternalXPathSupport() throws Exception {
+        Class.forName("com.sun.org.apache.xpath.internal.XPath");
+        Class c = 
Class.forName("org.apache.freemarker.dom.SunInternalXalanXPathSupport");
+        synchronized (STATIC_LOCK) {
+            xpathSupportClass = c;
+        }
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("Using Sun's internal Xalan classes for XPath support");
+        }
+    }
+    
+    /**
+     * Set an alternative implementation of 
org.apache.freemarker.dom.XPathSupport to use
+     * as the XPath engine.
+     * @param cl the class, or <code>null</code> to disable XPath support.
+     */
+    static public void setXPathSupportClass(Class cl) {
+        if (cl != null && !XPathSupport.class.isAssignableFrom(cl)) {
+            throw new RuntimeException("Class " + cl.getName()
+                    + " does not implement 
org.apache.freemarker.dom.XPathSupport");
+        }
+        synchronized (STATIC_LOCK) {
+            xpathSupportClass = cl;
+        }
+    }
+
+    /**
+     * Get the currently used org.apache.freemarker.dom.XPathSupport used as 
the XPath engine.
+     * Returns <code>null</code> if XPath support is disabled.
+     */
+    static public Class getXPathSupportClass() {
+        synchronized (STATIC_LOCK) {
+            return xpathSupportClass;
+        }
+    }
+
+    static private String getText(Node node) {
+        String result = "";
+        if (node instanceof Text || node instanceof CDATASection) {
+            result = ((org.w3c.dom.CharacterData) node).getData();
+        } else if (node instanceof Element) {
+            NodeList children = node.getChildNodes();
+            for (int i = 0; i < children.getLength(); i++) {
+                result += getText(children.item(i));
+            }
+        } else if (node instanceof Document) {
+            result = getText(((Document) node).getDocumentElement());
+        }
+        return result;
+    }
+    
+    XPathSupport getXPathSupport() {
+        if (jaxenXPathSupport != null) {
+            return jaxenXPathSupport;
+        }
+        XPathSupport xps = null;
+        Document doc = node.getOwnerDocument();
+        if (doc == null) {
+            doc = (Document) node;
+        }
+        synchronized (doc) {
+            WeakReference ref = (WeakReference) xpathSupportMap.get(doc);
+            if (ref != null) {
+                xps = (XPathSupport) ref.get();
+            }
+            if (xps == null) {
+                try {
+                    xps = (XPathSupport) xpathSupportClass.newInstance();
+                    xpathSupportMap.put(doc, new WeakReference(xps));
+                } catch (Exception e) {
+                    LOG.error("Error instantiating xpathSupport class", e);
+                }                
+            }
+        }
+        return xps;
+    }
+    
+    
+    String getQualifiedName() throws TemplateModelException {
+        return getNodeName();
+    }
+    
+    @Override
+    public Object getAdaptedObject(Class hint) {
+        return node;
+    }
+    
+    @Override
+    public Object getWrappedObject() {
+        return node;
+    }
+    
+    @Override
+    public Object[] explainTypeError(Class[] expectedClasses) {
+        for (Class expectedClass : expectedClasses) {
+            if (TemplateDateModel.class.isAssignableFrom(expectedClass)
+                    || 
TemplateNumberModel.class.isAssignableFrom(expectedClass)
+                    || 
TemplateBooleanModel.class.isAssignableFrom(expectedClass)) {
+                return new Object[]{
+                        "XML node values are always strings (text), that is, 
they can't be used as number, "
+                                + "date/time/datetime or boolean without 
explicit conversion (such as "
+                                + "someNode?number, someNode?datetime.xs, 
someNode?date.xs, someNode?time.xs, "
+                                + "someNode?boolean).",
+                };
+            }
+        }
+        return null;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59412b29/src/main/java/org/apache/freemarker/dom/NodeOutputter.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/dom/NodeOutputter.java 
b/src/main/java/org/apache/freemarker/dom/NodeOutputter.java
new file mode 100644
index 0000000..c4e5447
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/dom/NodeOutputter.java
@@ -0,0 +1,258 @@
+/*
+ * 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.freemarker.dom;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._StringUtil;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentType;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+class NodeOutputter {
+    
+    private Element contextNode;
+    private Environment env;
+    private String defaultNS;
+    private boolean hasDefaultNS;
+    private boolean explicitDefaultNSPrefix;
+    private LinkedHashMap<String, String> namespacesToPrefixLookup = new 
LinkedHashMap<>();
+    private String namespaceDecl;
+    int nextGeneratedPrefixNumber = 1;
+    
+    NodeOutputter(Node node) {
+        if (node instanceof Element) {
+            setContext((Element) node);
+        } else if (node instanceof Attr) {
+            setContext(((Attr) node).getOwnerElement());
+        } else if (node instanceof Document) {
+            setContext(((Document) node).getDocumentElement());
+        }
+    }
+    
+    private void setContext(Element contextNode) {
+        this.contextNode = contextNode;
+        env = Environment.getCurrentEnvironment();
+        defaultNS = env.getDefaultNS();
+        hasDefaultNS = defaultNS != null && defaultNS.length() > 0;
+        namespacesToPrefixLookup.put(null, "");
+        namespacesToPrefixLookup.put("", "");
+        buildPrefixLookup(contextNode);
+        if (!explicitDefaultNSPrefix && hasDefaultNS) {
+            namespacesToPrefixLookup.put(defaultNS, "");
+        }
+        constructNamespaceDecl();
+    }
+    
+    private void buildPrefixLookup(Node n) {
+        String nsURI = n.getNamespaceURI();
+        if (nsURI != null && nsURI.length() > 0) {
+            String prefix = env.getPrefixForNamespace(nsURI);
+            if (prefix == null) {
+                prefix = namespacesToPrefixLookup.get(nsURI);
+                if (prefix == null) {
+                    // Assign a generated prefix:
+                    do {
+                        prefix = 
_StringUtil.toLowerABC(nextGeneratedPrefixNumber++);
+                    } while (env.getNamespaceForPrefix(prefix) != null);
+                }
+            }
+            namespacesToPrefixLookup.put(nsURI, prefix);
+        } else if (hasDefaultNS && n.getNodeType() == Node.ELEMENT_NODE) {
+            namespacesToPrefixLookup.put(defaultNS, 
Template.DEFAULT_NAMESPACE_PREFIX); 
+            explicitDefaultNSPrefix = true;
+        } else if (n.getNodeType() == Node.ATTRIBUTE_NODE && hasDefaultNS && 
defaultNS.equals(nsURI)) {
+            namespacesToPrefixLookup.put(defaultNS, 
Template.DEFAULT_NAMESPACE_PREFIX); 
+            explicitDefaultNSPrefix = true;
+        }
+        NodeList childNodes = n.getChildNodes();
+        for (int i = 0; i < childNodes.getLength(); i++) {
+            buildPrefixLookup(childNodes.item(i));
+        }
+    }
+    
+    private void constructNamespaceDecl() {
+        StringBuilder buf = new StringBuilder();
+        if (explicitDefaultNSPrefix) {
+            buf.append(" xmlns=\"");
+            buf.append(defaultNS);
+            buf.append("\"");
+        }
+        for (Iterator<String> it = 
namespacesToPrefixLookup.keySet().iterator(); it.hasNext(); ) {
+            String nsURI = it.next();
+            if (nsURI == null || nsURI.length() == 0) {
+                continue;
+            }
+            String prefix = namespacesToPrefixLookup.get(nsURI);
+            if (prefix == null) {
+                throw new BugException("No xmlns prefix was associated to URI: 
" + nsURI);
+            }
+            buf.append(" xmlns");
+            if (prefix.length() > 0) {
+                buf.append(":");
+                buf.append(prefix);
+            }
+            buf.append("=\"");
+            buf.append(nsURI);
+            buf.append("\"");
+        }
+        namespaceDecl = buf.toString();
+    }
+    
+    private void outputQualifiedName(Node n, StringBuilder buf) {
+        String nsURI = n.getNamespaceURI();
+        if (nsURI == null || nsURI.length() == 0) {
+            buf.append(n.getNodeName());
+        } else {
+            String prefix = namespacesToPrefixLookup.get(nsURI);
+            if (prefix == null) {
+                //REVISIT!
+                buf.append(n.getNodeName());
+            } else {
+                if (prefix.length() > 0) {
+                    buf.append(prefix);
+                    buf.append(':');
+                }
+                buf.append(n.getLocalName());
+            }
+        }
+    }
+    
+    void outputContent(Node n, StringBuilder buf) {
+        switch(n.getNodeType()) {
+            case Node.ATTRIBUTE_NODE: {
+                if (((Attr) n).getSpecified()) {
+                    buf.append(' ');
+                    outputQualifiedName(n, buf);
+                    buf.append("=\"")
+                       .append(_StringUtil.XMLEncQAttr(n.getNodeValue()))
+                       .append('"');
+                }
+                break;
+            }
+            case Node.COMMENT_NODE: {
+                buf.append("<!--").append(n.getNodeValue()).append("-->");
+                break;
+            }
+            case Node.DOCUMENT_NODE: {
+                outputContent(n.getChildNodes(), buf);
+                break;
+            }
+            case Node.DOCUMENT_TYPE_NODE: {
+                buf.append("<!DOCTYPE ").append(n.getNodeName());
+                DocumentType dt = (DocumentType) n;
+                if (dt.getPublicId() != null) {
+                    buf.append(" PUBLIC 
\"").append(dt.getPublicId()).append('"');
+                }
+                if (dt.getSystemId() != null) {
+                    buf.append(" \"").append(dt.getSystemId()).append('"');
+                }
+                if (dt.getInternalSubset() != null) {
+                    buf.append(" 
[").append(dt.getInternalSubset()).append(']');
+                }
+                buf.append('>');
+                break;
+            }
+            case Node.ELEMENT_NODE: {
+                buf.append('<');
+                outputQualifiedName(n, buf);
+                if (n == contextNode) {
+                    buf.append(namespaceDecl);
+                }
+                outputContent(n.getAttributes(), buf);
+                NodeList children = n.getChildNodes();
+                if (children.getLength() == 0) {
+                    buf.append(" />");
+                } else {
+                    buf.append('>');
+                    outputContent(n.getChildNodes(), buf);
+                    buf.append("</");
+                    outputQualifiedName(n, buf);
+                    buf.append('>');
+                }
+                break;
+            }
+            case Node.ENTITY_NODE: {
+                outputContent(n.getChildNodes(), buf);
+                break;
+            }
+            case Node.ENTITY_REFERENCE_NODE: {
+                buf.append('&').append(n.getNodeName()).append(';');
+                break;
+            }
+            case Node.PROCESSING_INSTRUCTION_NODE: {
+                buf.append("<?").append(n.getNodeName()).append(' 
').append(n.getNodeValue()).append("?>");
+                break;
+            }
+            /*            
+                        case Node.CDATA_SECTION_NODE: {
+                            
buf.append("<![CDATA[").append(n.getNodeValue()).append("]]>");
+                            break;
+                        }*/
+            case Node.CDATA_SECTION_NODE:
+            case Node.TEXT_NODE: {
+                buf.append(_StringUtil.XMLEncNQG(n.getNodeValue()));
+                break;
+            }
+        }
+    }
+
+    void outputContent(NodeList nodes, StringBuilder buf) {
+        for (int i = 0; i < nodes.getLength(); ++i) {
+            outputContent(nodes.item(i), buf);
+        }
+    }
+    
+    void outputContent(NamedNodeMap nodes, StringBuilder buf) {
+        for (int i = 0; i < nodes.getLength(); ++i) {
+            Node n = nodes.item(i);
+            if (n.getNodeType() != Node.ATTRIBUTE_NODE 
+                || (!n.getNodeName().startsWith("xmlns:") && 
!n.getNodeName().equals("xmlns"))) { 
+                outputContent(n, buf);
+            }
+        }
+    }
+    
+    String getOpeningTag(Element element) {
+        StringBuilder buf = new StringBuilder();
+        buf.append('<');
+        outputQualifiedName(element, buf);
+        buf.append(namespaceDecl);
+        outputContent(element.getAttributes(), buf);
+        buf.append('>');
+        return buf.toString();
+    }
+    
+    String getClosingTag(Element element) {
+        StringBuilder buf = new StringBuilder();
+        buf.append("</");
+        outputQualifiedName(element, buf);
+        buf.append('>');
+        return buf.toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59412b29/src/main/java/org/apache/freemarker/dom/PINodeModel.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/dom/PINodeModel.java 
b/src/main/java/org/apache/freemarker/dom/PINodeModel.java
new file mode 100644
index 0000000..81c8dc2
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/dom/PINodeModel.java
@@ -0,0 +1,45 @@
+/*
+ * 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.freemarker.dom;
+
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.w3c.dom.ProcessingInstruction;
+
+class PINodeModel extends NodeModel implements TemplateScalarModel {
+    
+    public PINodeModel(ProcessingInstruction pi) {
+        super(pi);
+    }
+    
+    @Override
+    public String getAsString() {
+        return ((ProcessingInstruction) node).getData();
+    }
+    
+    @Override
+    public String getNodeName() {
+        return "@pi$" + ((ProcessingInstruction) node).getTarget();
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        return true;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59412b29/src/main/java/org/apache/freemarker/dom/SunInternalXalanXPathSupport.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/dom/SunInternalXalanXPathSupport.java 
b/src/main/java/org/apache/freemarker/dom/SunInternalXalanXPathSupport.java
new file mode 100644
index 0000000..1e13362
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/dom/SunInternalXalanXPathSupport.java
@@ -0,0 +1,163 @@
+/*
+ * 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.freemarker.dom;
+
+import java.util.List;
+
+import javax.xml.transform.TransformerException;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.w3c.dom.Node;
+import org.w3c.dom.traversal.NodeIterator;
+
+import com.sun.org.apache.xml.internal.utils.PrefixResolver;
+import com.sun.org.apache.xpath.internal.XPath;
+import com.sun.org.apache.xpath.internal.XPathContext;
+import com.sun.org.apache.xpath.internal.objects.XBoolean;
+import com.sun.org.apache.xpath.internal.objects.XNodeSet;
+import com.sun.org.apache.xpath.internal.objects.XNull;
+import com.sun.org.apache.xpath.internal.objects.XNumber;
+import com.sun.org.apache.xpath.internal.objects.XObject;
+import com.sun.org.apache.xpath.internal.objects.XString;
+
+/**
+ * This is just the XalanXPathSupport class using the sun internal
+ * package names
+ */
+
+class SunInternalXalanXPathSupport implements XPathSupport {
+    
+    private XPathContext xpathContext = new XPathContext();
+        
+    private static final String ERRMSG_RECOMMEND_JAXEN
+            = "(Note that there is no such restriction if you "
+                    + "configure FreeMarker to use Jaxen instead of Xalan.)";
+
+    private static final String ERRMSG_EMPTY_NODE_SET
+            = "Cannot perform an XPath query against an empty node set." + 
ERRMSG_RECOMMEND_JAXEN;
+    
+    @Override
+    synchronized public TemplateModel executeQuery(Object context, String 
xpathQuery) throws TemplateModelException {
+        if (!(context instanceof Node)) {
+            if (context != null) {
+                if (isNodeList(context)) {
+                    int cnt = ((List) context).size();
+                    if (cnt != 0) {
+                        throw new TemplateModelException(
+                                "Cannot perform an XPath query against a node 
set of " + cnt
+                                + " nodes. Expecting a single node." + 
ERRMSG_RECOMMEND_JAXEN);
+                    } else {
+                        throw new 
TemplateModelException(ERRMSG_EMPTY_NODE_SET);
+                    }
+                } else {
+                    throw new TemplateModelException(
+                            "Cannot perform an XPath query against a " + 
context.getClass().getName()
+                            + ". Expecting a single org.w3c.dom.Node.");
+                }
+            } else {
+                throw new TemplateModelException(ERRMSG_EMPTY_NODE_SET);
+            }
+        }
+        Node node = (Node) context;
+        try {
+            XPath xpath = new XPath(xpathQuery, null, customPrefixResolver, 
XPath.SELECT, null);
+            int ctxtNode = xpathContext.getDTMHandleFromNode(node);
+            XObject xresult = xpath.execute(xpathContext, ctxtNode, 
customPrefixResolver);
+            if (xresult instanceof XNodeSet) {
+                NodeListModel result = new NodeListModel(node);
+                result.xpathSupport = this;
+                NodeIterator nodeIterator = xresult.nodeset();
+                Node n;
+                do {
+                    n = nodeIterator.nextNode();
+                    if (n != null) {
+                        result.add(n);
+                    }
+                } while (n != null);
+                return result.size() == 1 ? result.get(0) : result;
+            }
+            if (xresult instanceof XBoolean) {
+                return ((XBoolean) xresult).bool() ? TemplateBooleanModel.TRUE 
: TemplateBooleanModel.FALSE;
+            }
+            if (xresult instanceof XNull) {
+                return null;
+            }
+            if (xresult instanceof XString) {
+                return new SimpleScalar(xresult.toString());
+            }
+            if (xresult instanceof XNumber) {
+                return new SimpleNumber(Double.valueOf(((XNumber) 
xresult).num()));
+            }
+            throw new TemplateModelException("Cannot deal with type: " + 
xresult.getClass().getName());
+        } catch (TransformerException te) {
+            throw new TemplateModelException(te);
+        }
+    }
+    
+    private static PrefixResolver customPrefixResolver = new PrefixResolver() {
+        
+        @Override
+        public String getNamespaceForPrefix(String prefix, Node node) {
+            return getNamespaceForPrefix(prefix);
+        }
+        
+        @Override
+        public String getNamespaceForPrefix(String prefix) {
+            if (prefix.equals(Template.DEFAULT_NAMESPACE_PREFIX)) {
+                return Environment.getCurrentEnvironment().getDefaultNS();
+            }
+            return 
Environment.getCurrentEnvironment().getNamespaceForPrefix(prefix);
+        }
+        
+        @Override
+        public String getBaseIdentifier() {
+            return null;
+        }
+        
+        @Override
+        public boolean handlesNullPrefixes() {
+            return false;
+        }
+    };
+    
+    /**
+     * Used for generating more intelligent error messages.
+     */
+    private static boolean isNodeList(Object context) {
+        if (context instanceof List) {
+            List ls = (List) context;
+            int ln = ls.size();
+            for (int i = 0; i < ln; i++) {
+                if (!(ls.get(i) instanceof Node)) {
+                    return false;
+                }
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59412b29/src/main/java/org/apache/freemarker/dom/XPathSupport.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/dom/XPathSupport.java 
b/src/main/java/org/apache/freemarker/dom/XPathSupport.java
new file mode 100644
index 0000000..f896628
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/dom/XPathSupport.java
@@ -0,0 +1,30 @@
+/*
+ * 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.freemarker.dom;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+
+public interface XPathSupport {
+    
+    // [2.4] Add argument to pass down the ObjectWrapper to use 
+    TemplateModel executeQuery(Object context, String xpathQuery) throws 
TemplateModelException;
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59412b29/src/main/java/org/apache/freemarker/dom/XalanXPathSupport.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/dom/XalanXPathSupport.java 
b/src/main/java/org/apache/freemarker/dom/XalanXPathSupport.java
new file mode 100644
index 0000000..5f40ff4
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/dom/XalanXPathSupport.java
@@ -0,0 +1,163 @@
+/*
+ * 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.freemarker.dom;
+
+import java.util.List;
+
+import javax.xml.transform.TransformerException;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.xml.utils.PrefixResolver;
+import org.apache.xpath.XPath;
+import org.apache.xpath.XPathContext;
+import org.apache.xpath.objects.XBoolean;
+import org.apache.xpath.objects.XNodeSet;
+import org.apache.xpath.objects.XNull;
+import org.apache.xpath.objects.XNumber;
+import org.apache.xpath.objects.XObject;
+import org.apache.xpath.objects.XString;
+import org.w3c.dom.Node;
+import org.w3c.dom.traversal.NodeIterator;
+
+/**
+ * Some glue code that bridges the Xalan XPath stuff (that is built into the 
JDK 1.4.x)
+ * with FreeMarker TemplateModel semantics
+ */
+
+class XalanXPathSupport implements XPathSupport {
+    
+    private XPathContext xpathContext = new XPathContext();
+        
+    /* I don't recommend Jaxen...
+    private static final String ERRMSG_RECOMMEND_JAXEN
+            = "(Note that there is no such restriction if you "
+                    + "configure FreeMarker to use Jaxen instead of Xalan.)";
+    */
+    private static final String ERRMSG_EMPTY_NODE_SET
+            = "Cannot perform an XPath query against an empty node set."; /* " 
+ ERRMSG_RECOMMEND_JAXEN;*/
+    
+    @Override
+    synchronized public TemplateModel executeQuery(Object context, String 
xpathQuery) throws TemplateModelException {
+        if (!(context instanceof Node)) {
+            if (context != null) {
+                if (isNodeList(context)) {
+                    int cnt = ((List) context).size();
+                    if (cnt != 0) {
+                        throw new TemplateModelException(
+                                "Cannot perform an XPath query against a node 
set of " + cnt
+                                + " nodes. Expecting a single node."/* " + 
ERRMSG_RECOMMEND_JAXEN*/);
+                    } else {
+                        throw new 
TemplateModelException(ERRMSG_EMPTY_NODE_SET);
+                    }
+                } else {
+                    throw new TemplateModelException(
+                            "Cannot perform an XPath query against a " + 
context.getClass().getName()
+                            + ". Expecting a single org.w3c.dom.Node.");
+                }
+            } else {
+                throw new TemplateModelException(ERRMSG_EMPTY_NODE_SET);
+            }
+        }
+        Node node = (Node) context;
+        try {
+            XPath xpath = new XPath(xpathQuery, null, customPrefixResolver, 
XPath.SELECT, null);
+            int ctxtNode = xpathContext.getDTMHandleFromNode(node);
+            XObject xresult = xpath.execute(xpathContext, ctxtNode, 
customPrefixResolver);
+            if (xresult instanceof XNodeSet) {
+                NodeListModel result = new NodeListModel(node);
+                result.xpathSupport = this;
+                NodeIterator nodeIterator = xresult.nodeset();
+                Node n;
+                do {
+                    n = nodeIterator.nextNode();
+                    if (n != null) {
+                        result.add(n);
+                    }
+                } while (n != null);
+                return result.size() == 1 ? result.get(0) : result;
+            }
+            if (xresult instanceof XBoolean) {
+                return ((XBoolean) xresult).bool() ? TemplateBooleanModel.TRUE 
: TemplateBooleanModel.FALSE;
+            }
+            if (xresult instanceof XNull) {
+                return null;
+            }
+            if (xresult instanceof XString) {
+                return new SimpleScalar(xresult.toString());
+            }
+            if (xresult instanceof XNumber) {
+                return new SimpleNumber(Double.valueOf(((XNumber) 
xresult).num()));
+            }
+            throw new TemplateModelException("Cannot deal with type: " + 
xresult.getClass().getName());
+        } catch (TransformerException te) {
+            throw new TemplateModelException(te);
+        }
+    }
+    
+    private static PrefixResolver customPrefixResolver = new PrefixResolver() {
+        
+        @Override
+        public String getNamespaceForPrefix(String prefix, Node node) {
+            return getNamespaceForPrefix(prefix);
+        }
+        
+        @Override
+        public String getNamespaceForPrefix(String prefix) {
+            if (prefix.equals(Template.DEFAULT_NAMESPACE_PREFIX)) {
+                return Environment.getCurrentEnvironment().getDefaultNS();
+            }
+            return 
Environment.getCurrentEnvironment().getNamespaceForPrefix(prefix);
+        }
+        
+        @Override
+        public String getBaseIdentifier() {
+            return null;
+        }
+        
+        @Override
+        public boolean handlesNullPrefixes() {
+            return false;
+        }
+    };
+    
+    /**
+     * Used for generating more intelligent error messages.
+     */
+    private static boolean isNodeList(Object context) {
+        if (context instanceof List) {
+            List ls = (List) context;
+            int ln = ls.size();
+            for (int i = 0; i < ln; i++) {
+                if (!(ls.get(i) instanceof Node)) {
+                    return false;
+                }
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59412b29/src/main/java/org/apache/freemarker/dom/package.html
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/dom/package.html 
b/src/main/java/org/apache/freemarker/dom/package.html
new file mode 100644
index 0000000..a3518ff
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/dom/package.html
@@ -0,0 +1,31 @@
+<!--
+  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.
+-->
+<html>
+<head>
+<title></title>
+</head>
+<body>
+
+<p>Exposes DOM XML nodes to templates as easily traversable trees;
+see <a href="http://freemarker.org/docs/xgui.html"; target="_blank">in the 
Manual</a>.
+The {@link freemarker.template.DefaultObjectWrapper default object wrapper} of 
FreeMarker
+automatically wraps W3C nodes with this.
+
+</body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59412b29/src/manual/en_US/FM3-CHANGE-LOG.txt
----------------------------------------------------------------------
diff --git a/src/manual/en_US/FM3-CHANGE-LOG.txt 
b/src/manual/en_US/FM3-CHANGE-LOG.txt
index 2f2ac35..52564b7 100644
--- a/src/manual/en_US/FM3-CHANGE-LOG.txt
+++ b/src/manual/en_US/FM3-CHANGE-LOG.txt
@@ -75,7 +75,7 @@ the FreeMarer 3 changelog here:
   ArithmeticEngine related classes were moved to 
org.apache.freemarker.core.arithmetic.
   freemarker.ext.beans were moved under 
org.apache.freemarker.core.model.impl.beans for now (but later
   we only want a DefaultObject wrapper, no BeansWrapper, so this will change) 
and freemarker.ext.dom
-  was moved to org.apache.freemarker.core.model.impl.dom.
+  was moved to org.apache.freemarker.dom.
 - Moved the all the static final ObjectWrapper-s to the new 
_StaticObjectWrappers class, and made them
   write protected (non-configurable). Also now they come from the pool that 
ObjectWrapper builders use.
 - WrappingTemplateModel.objectWrapper is now final, and its statically stored 
default value can't be set anymore.

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59412b29/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMSiblingTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMSiblingTest.java 
b/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMSiblingTest.java
deleted file mode 100644
index 435fb4e..0000000
--- 
a/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMSiblingTest.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * 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.freemarker.core.model.impl.dom;
-
-import java.io.IOException;
-
-import javax.xml.parsers.ParserConfigurationException;
-
-import org.apache.freemarker.core.TemplateException;
-import org.apache.freemarker.test.TemplateTest;
-import org.apache.freemarker.test.util.XMLLoader;
-import org.junit.Before;
-import org.junit.Test;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
-
-public class DOMSiblingTest extends TemplateTest {
-
-    @Before
-    public void setUp() throws SAXException, IOException, 
ParserConfigurationException {
-        InputSource is = new 
InputSource(getClass().getResourceAsStream("DOMSiblingTest.xml"));
-        addToDataModel("doc", XMLLoader.toModel(is));
-    }
-
-    @Test
-    public void testBlankPreviousSibling() throws IOException, 
TemplateException {
-        assertOutput("${doc.person.name?previousSibling}", "\n    ");
-        assertOutput("${doc.person.name?previous_sibling}", "\n    ");
-    }
-
-    @Test
-    public void testNonBlankPreviousSibling() throws IOException, 
TemplateException {
-        assertOutput("${doc.person.address?previousSibling}", "12th August");
-    }
-
-    @Test
-    public void testBlankNextSibling() throws IOException, TemplateException {
-        assertOutput("${doc.person.name?nextSibling}", "\n    ");
-        assertOutput("${doc.person.name?next_sibling}", "\n    ");
-    }
-
-    @Test
-    public void testNonBlankNextSibling() throws IOException, 
TemplateException {
-        assertOutput("${doc.person.dob?nextSibling}", "Chennai, India");
-    }
-
-    @Test
-    public void testNullPreviousSibling() throws IOException, 
TemplateException {
-        assertOutput("${doc.person?previousSibling?? ?c}", "false");
-    }
-
-    @Test
-    public void testSignificantPreviousSibling() throws IOException, 
TemplateException {
-        assertOutput("${doc.person.name.@@previous_sibling_element}", "male");
-    }
-
-    @Test
-    public void testSignificantNextSibling() throws IOException, 
TemplateException {
-        assertOutput("${doc.person.name.@@next_sibling_element}", "12th 
August");
-    }
-
-    @Test
-    public void testNullSignificantPreviousSibling() throws IOException, 
TemplateException {
-        assertOutput("${doc.person.phone.@@next_sibling_element?size}", "0");
-    }
-
-    @Test
-    public void testSkippingCommentNode() throws IOException, 
TemplateException {
-        assertOutput("${doc.person.profession.@@previous_sibling_element}", 
"Chennai, India");
-    }
-
-    @Test
-    public void testSkippingEmptyCDataNode() throws IOException, 
TemplateException {
-        assertOutput("${doc.person.hobby.@@previous_sibling_element}", 
"Software Engineer");
-    }
-
-    @Test
-    public void testValidCDataNode() throws IOException, TemplateException {
-        assertOutput("${doc.person.phone.@@previous_sibling_element?size}", 
"0");
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59412b29/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMSimplifiersTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMSimplifiersTest.java
 
b/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMSimplifiersTest.java
deleted file mode 100644
index 91d0b89..0000000
--- 
a/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMSimplifiersTest.java
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * 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.freemarker.core.model.impl.dom;
-
-import static org.junit.Assert.assertEquals;
-
-import java.io.IOException;
-
-import javax.xml.parsers.ParserConfigurationException;
-
-import org.apache.freemarker.test.util.XMLLoader;
-import org.junit.Test;
-import org.w3c.dom.CDATASection;
-import org.w3c.dom.Comment;
-import org.w3c.dom.Document;
-import org.w3c.dom.DocumentType;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.ProcessingInstruction;
-import org.w3c.dom.Text;
-import org.xml.sax.SAXException;
-
-public class DOMSimplifiersTest {
-
-    private static final String COMMON_TEST_XML
-            = "<!DOCTYPE a 
[]><?p?><a>x<![CDATA[y]]><!--c--><?p?>z<?p?><b><!--c--></b><c></c>"
-              + "<d>a<e>c</e>b<!--c--><!--c--><!--c--><?p?><?p?><?p?></d>"
-              + "<f><![CDATA[1]]>2</f></a><!--c-->";
-
-    private static final String TEXT_MERGE_CONTENT =
-            "<a>"
-            + "a<!--c--><s/>"
-            + "<!--c-->a<s/>"
-            + "a<!--c-->b<s/>"
-            + "<!--c-->a<!--c-->b<!--c--><s/>"
-            + "a<b>b</b>c<s/>"
-            + "a<b>b</b><!--c-->c<s/>"
-            + "a<!--c-->1<b>b<!--c--></b>c<!--c-->1<s/>"
-            + "a<!--c-->1<b>b<!--c-->c</b>d<!--c-->1<s/>"
-            + "a<!--c-->1<b>b<!--c-->c</b>d<!--c-->1<s/>"
-            + 
"a<!--c-->1<b>b<!--c-->1<e>c<!--c-->1</e>d<!--c-->1</b>e<!--c-->1<s/>"
-            + "</a>";
-    private static final String TEXT_MERGE_EXPECTED =
-            "<a>"
-            + "%a<s/>"
-            + "%a<s/>"
-            + "%ab<s/>"
-            + "%ab<s/>"
-            + "%a<b>%b</b>%c<s/>"
-            + "%a<b>%b</b>%c<s/>"
-            + "%a1<b>%b</b>%c1<s/>"
-            + "%a1<b>%bc</b>%d1<s/>"
-            + "%a1<b>%bc</b>%d1<s/>"
-            + "%a1<b>%b1<e>%c1</e>%d1</b>%e1<s/>"
-            + "</a>";
-    
-    @Test
-    public void testTest() throws Exception {
-        String expected = "<!DOCTYPE 
...><?p?><a>%x<![CDATA[y]]><!--c--><?p?>%z<?p?><b><!--c--></b><c/>"
-                   + 
"<d>%a<e>%c</e>%b<!--c--><!--c--><!--c--><?p?><?p?><?p?></d>"
-                   + "<f><![CDATA[1]]>%2</f></a><!--c-->";
-        assertEquals(expected, toString(XMLLoader.toDOM(COMMON_TEST_XML)));
-    }
-
-    @Test
-    public void testMergeAdjacentText() throws Exception {
-        Document dom = XMLLoader.toDOM(COMMON_TEST_XML);
-        NodeModel.mergeAdjacentText(dom);
-        assertEquals(
-                "<!DOCTYPE 
...><?p?><a>%xy<!--c--><?p?>%z<?p?><b><!--c--></b><c/>"
-                + "<d>%a<e>%c</e>%b<!--c--><!--c--><!--c--><?p?><?p?><?p?></d>"
-                + "<f><![CDATA[12]]></f></a><!--c-->",
-                toString(dom));
-    }
-
-    @Test
-    public void testRemoveComments() throws Exception {
-        Document dom = XMLLoader.toDOM(COMMON_TEST_XML);
-        NodeModel.removeComments(dom);
-        assertEquals(
-                "<!DOCTYPE ...><?p?><a>%x<![CDATA[y]]><?p?>%z<?p?><b/><c/>"
-                + "<d>%a<e>%c</e>%b<?p?><?p?><?p?></d>"
-                + "<f><![CDATA[1]]>%2</f></a>",
-                toString(dom));
-    }
-
-    @Test
-    public void testRemovePIs() throws Exception {
-        Document dom = XMLLoader.toDOM(COMMON_TEST_XML);
-        NodeModel.removePIs(dom);
-        assertEquals(
-                "<!DOCTYPE ...><a>%x<![CDATA[y]]><!--c-->%z<b><!--c--></b><c/>"
-                + "<d>%a<e>%c</e>%b<!--c--><!--c--><!--c--></d>"
-                + "<f><![CDATA[1]]>%2</f></a><!--c-->",
-                toString(dom));
-    }
-    
-    @Test
-    public void testSimplify() throws Exception {
-        testSimplify(
-                "<!DOCTYPE ...><a>%xyz<b/><c/>"
-                + "<d>%a<e>%c</e>%b</d><f><![CDATA[12]]></f></a>",
-                COMMON_TEST_XML);
-    }
-
-    @Test
-    public void testSimplify2() throws Exception {
-        testSimplify(TEXT_MERGE_EXPECTED, TEXT_MERGE_CONTENT);
-    }
-
-    @Test
-    public void testSimplify3() throws Exception {
-        testSimplify("<a/>", "<a/>");
-    }
-    
-    private void testSimplify(String expected, String content)
-            throws SAXException, IOException, ParserConfigurationException {
-        {
-            Document dom = XMLLoader.toDOM(content);
-            NodeModel.simplify(dom);
-            assertEquals(expected, toString(dom));
-        }
-        
-        // Must be equivalent:
-        {
-            Document dom = XMLLoader.toDOM(content);
-            NodeModel.removeComments(dom);
-            NodeModel.removePIs(dom);
-            NodeModel.mergeAdjacentText(dom);
-            assertEquals(expected, toString(dom));
-        }
-        
-        // Must be equivalent:
-        {
-            Document dom = XMLLoader.toDOM(content);
-            NodeModel.removeComments(dom);
-            NodeModel.removePIs(dom);
-            NodeModel.simplify(dom);
-            assertEquals(expected, toString(dom));
-        }
-    }
-
-    private String toString(Document doc) {
-        StringBuilder sb = new StringBuilder();
-        toString(doc, sb);
-        return sb.toString();
-    }
-
-    private void toString(Node node, StringBuilder sb) {
-        if (node instanceof Document) {
-            childrenToString(node, sb);
-        } else if (node instanceof Element) {
-            if (node.hasChildNodes()) {
-                sb.append("<").append(node.getNodeName()).append(">");
-                childrenToString(node, sb);
-                sb.append("</").append(node.getNodeName()).append(">");
-            } else {
-                sb.append("<").append(node.getNodeName()).append("/>");
-            }
-        } else if (node instanceof Text) {
-            if (node instanceof CDATASection) {
-                
sb.append("<![CDATA[").append(node.getNodeValue()).append("]]>");
-            } else {
-                sb.append("%").append(node.getNodeValue());
-            }
-        } else if (node instanceof Comment) {
-            sb.append("<!--").append(node.getNodeValue()).append("-->");
-        } else if (node instanceof ProcessingInstruction) {
-            sb.append("<?").append(node.getNodeName()).append("?>");
-        } else if (node instanceof DocumentType) {
-            sb.append("<!DOCTYPE ...>");
-        } else {
-            throw new IllegalStateException("Unhandled node type: " + 
node.getClass().getName());
-        }
-    }
-
-    private void childrenToString(Node node, StringBuilder sb) {
-        Node child = node.getFirstChild();
-        while (child != null) {
-            toString(child, sb);
-            child = child.getNextSibling();
-        }
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59412b29/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMTest.java 
b/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMTest.java
deleted file mode 100644
index 387b863..0000000
--- a/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMTest.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * 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.freemarker.core.model.impl.dom;
-
-import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.startsWith;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assert.fail;
-
-import java.io.IOException;
-import java.io.StringReader;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-
-import org.apache.freemarker.core.TemplateException;
-import org.apache.freemarker.test.TemplateTest;
-import org.apache.freemarker.test.util.XMLLoader;
-import org.junit.Test;
-import org.w3c.dom.Document;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
-
-public class DOMTest extends TemplateTest {
-
-    @Test
-    public void xpathDetectionBugfix() throws Exception {
-        addDocToDataModel("<root><a>A</a><b>B</b><c>C</c></root>");
-        assertOutput("${doc.root.b['following-sibling::c']}", "C");
-        assertOutput("${doc.root.b['following-sibling::*']}", "C");
-    }
-
-    @Test
-    public void xmlnsPrefixes() throws Exception {
-        addDocToDataModel("<root xmlns='http://example.com/ns1' 
xmlns:ns2='http://example.com/ns2'>"
-                + "<a>A</a><ns2:b>B</ns2:b><c a1='1' ns2:a2='2'/></root>");
-
-        String ftlHeader = "<#ftl ns_prefixes={'D':'http://example.com/ns1', 
'n2':'http://example.com/ns2'}>";
-        
-        // @@markup:
-        assertOutput("${doc.@@markup}",
-                "<a:root xmlns:a=\"http://example.com/ns1\"; 
xmlns:b=\"http://example.com/ns2\";>"
-                + "<a:a>A</a:a><b:b>B</b:b><a:c a1=\"1\" b:a2=\"2\" />"
-                + "</a:root>");
-        assertOutput(ftlHeader
-                + "${doc.@@markup}",
-                "<root xmlns=\"http://example.com/ns1\"; 
xmlns:n2=\"http://example.com/ns2\";>"
-                + "<a>A</a><n2:b>B</n2:b><c a1=\"1\" n2:a2=\"2\" /></root>");
-        assertOutput("<#ftl ns_prefixes={'D':'http://example.com/ns1'}>"
-                + "${doc.@@markup}",
-                "<root xmlns=\"http://example.com/ns1\"; 
xmlns:a=\"http://example.com/ns2\";>"
-                + "<a>A</a><a:b>B</a:b><c a1=\"1\" a:a2=\"2\" /></root>");
-        
-        // When there's no matching prefix declared via the #ftl header, 
return null for qname:
-        assertOutput("${doc?children[0].@@qname!'null'}", "null");
-        assertOutput("${doc?children[0]?children[1].@@qname!'null'}", "null");
-        assertOutput("${doc?children[0]?children[2]['@*'][1].@@qname!'null'}", 
"null");
-        
-        // When we have prefix declared in the #ftl header:
-        assertOutput(ftlHeader + "${doc?children[0].@@qname}", "root");
-        assertOutput(ftlHeader + "${doc?children[0]?children[1].@@qname}", 
"n2:b");
-        assertOutput(ftlHeader + "${doc?children[0]?children[2].@@qname}", 
"c");
-        assertOutput(ftlHeader + 
"${doc?children[0]?children[2]['@*'][0].@@qname}", "a1");
-        assertOutput(ftlHeader + 
"${doc?children[0]?children[2]['@*'][1].@@qname}", "n2:a2");
-        // Unfortunately these include the xmlns attributes, but that would be 
non-BC to fix now:
-        assertThat(getOutput(ftlHeader + "${doc?children[0].@@start_tag}"), 
startsWith("<root"));
-        assertThat(getOutput(ftlHeader + 
"${doc?children[0]?children[1].@@start_tag}"), startsWith("<n2:b"));
-    }
-    
-    @Test
-    public void namespaceUnaware() throws Exception {
-        
addNSUnawareDocToDataModel("<root><x:a>A</x:a><:>B</:><xyz::c>C</xyz::c></root>");
-        assertOutput("${doc.root['x:a']}", "A");
-        assertOutput("${doc.root[':']}", "B");
-        try {
-            assertOutput("${doc.root['xyz::c']}", "C");
-            fail();
-        } catch (TemplateException e) {
-            assertThat(e.getMessage(), containsString("xyz"));
-        }
-    }
-    
-    private void addDocToDataModel(String xml) throws SAXException, 
IOException, ParserConfigurationException {
-        addToDataModel("doc", XMLLoader.toModel(new InputSource(new 
StringReader(xml))));
-    }
-
-    private void addDocToDataModelNoSimplification(String xml) throws 
SAXException, IOException, ParserConfigurationException {
-        addToDataModel("doc", XMLLoader.toModel(new InputSource(new 
StringReader(xml)), false));
-    }
-    
-    private void addNSUnawareDocToDataModel(String xml) throws 
ParserConfigurationException, SAXException, IOException {
-        DocumentBuilderFactory newFactory = 
DocumentBuilderFactory.newInstance();
-        newFactory.setNamespaceAware(false);
-        DocumentBuilder builder = newFactory.newDocumentBuilder();
-        Document doc = builder.parse(new InputSource(new StringReader(xml)));
-        addToDataModel("doc", doc);
-    }
-
-    @Test
-    public void testInvalidAtAtKeyErrors() throws Exception {
-        addDocToDataModel("<r><multipleMatches /><multipleMatches /></r>");
-        assertErrorContains("${doc.r.@@invalid_key}", "Unsupported @@ key", 
"@invalid_key");
-        assertErrorContains("${doc.@@start_tag}", "@@start_tag", "not 
supported", "document");
-        assertErrorContains("${doc.@@}", "\"@@\"", "not supported", 
"document");
-        assertErrorContains("${doc.r.noMatch.@@invalid_key}", "Unsupported @@ 
key", "@invalid_key");
-        assertErrorContains("${doc.r.multipleMatches.@@invalid_key}", 
"Unsupported @@ key", "@invalid_key");
-        assertErrorContains("${doc.r.noMatch.@@attributes_markup}", "single 
XML node", "@@attributes_markup");
-        assertErrorContains("${doc.r.multipleMatches.@@attributes_markup}", 
"single XML node", "@@attributes_markup");
-    }
-    
-    @Test
-    public void testAtAtSiblingElement() throws Exception {
-        addDocToDataModel("<r><a/><b/></r>");
-        assertOutput("${doc.r.@@previous_sibling_element?size}", "0");
-        assertOutput("${doc.r.@@next_sibling_element?size}", "0");
-        assertOutput("${doc.r.a.@@previous_sibling_element?size}", "0");
-        assertOutput("${doc.r.a.@@next_sibling_element.@@qname}", "b");
-        assertOutput("${doc.r.b.@@previous_sibling_element.@@qname}", "a");
-        assertOutput("${doc.r.b.@@next_sibling_element?size}", "0");
-        
-        addDocToDataModel("<r>\r\n\t <a/>\r\n\t <b/>\r\n\t </r>");
-        assertOutput("${doc.r.@@previous_sibling_element?size}", "0");
-        assertOutput("${doc.r.@@next_sibling_element?size}", "0");
-        assertOutput("${doc.r.a.@@previous_sibling_element?size}", "0");
-        assertOutput("${doc.r.a.@@next_sibling_element.@@qname}", "b");
-        assertOutput("${doc.r.b.@@previous_sibling_element.@@qname}", "a");
-        assertOutput("${doc.r.b.@@next_sibling_element?size}", "0");
-        
-        addDocToDataModel("<r>t<a/>t<b/>t</r>");
-        assertOutput("${doc.r.a.@@previous_sibling_element?size}", "0");
-        assertOutput("${doc.r.a.@@next_sibling_element?size}", "0");
-        assertOutput("${doc.r.b.@@previous_sibling_element?size}", "0");
-        assertOutput("${doc.r.b.@@next_sibling_element?size}", "0");
-        
-        addDocToDataModelNoSimplification("<r><a/> <!-- 
--><?pi?>&#x20;<b/></r>");
-        assertOutput("${doc.r.a.@@next_sibling_element.@@qname}", "b");
-        assertOutput("${doc.r.b.@@previous_sibling_element.@@qname}", "a");
-        
-        addDocToDataModelNoSimplification("<r><a/> <!-- -->t<!-- --> 
<b/></r>");
-        assertOutput("${doc.r.a.@@next_sibling_element?size}", "0");
-        assertOutput("${doc.r.b.@@previous_sibling_element?size}", "0");
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59412b29/src/test/java/org/apache/freemarker/dom/DOMSiblingTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/dom/DOMSiblingTest.java 
b/src/test/java/org/apache/freemarker/dom/DOMSiblingTest.java
new file mode 100644
index 0000000..aa0a3f8
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/dom/DOMSiblingTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.freemarker.dom;
+
+import java.io.IOException;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.util.XMLLoader;
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+public class DOMSiblingTest extends TemplateTest {
+
+    @Before
+    public void setUp() throws SAXException, IOException, 
ParserConfigurationException {
+        InputSource is = new 
InputSource(getClass().getResourceAsStream("DOMSiblingTest.xml"));
+        addToDataModel("doc", XMLLoader.toModel(is));
+    }
+
+    @Test
+    public void testBlankPreviousSibling() throws IOException, 
TemplateException {
+        assertOutput("${doc.person.name?previousSibling}", "\n    ");
+        assertOutput("${doc.person.name?previous_sibling}", "\n    ");
+    }
+
+    @Test
+    public void testNonBlankPreviousSibling() throws IOException, 
TemplateException {
+        assertOutput("${doc.person.address?previousSibling}", "12th August");
+    }
+
+    @Test
+    public void testBlankNextSibling() throws IOException, TemplateException {
+        assertOutput("${doc.person.name?nextSibling}", "\n    ");
+        assertOutput("${doc.person.name?next_sibling}", "\n    ");
+    }
+
+    @Test
+    public void testNonBlankNextSibling() throws IOException, 
TemplateException {
+        assertOutput("${doc.person.dob?nextSibling}", "Chennai, India");
+    }
+
+    @Test
+    public void testNullPreviousSibling() throws IOException, 
TemplateException {
+        assertOutput("${doc.person?previousSibling?? ?c}", "false");
+    }
+
+    @Test
+    public void testSignificantPreviousSibling() throws IOException, 
TemplateException {
+        assertOutput("${doc.person.name.@@previous_sibling_element}", "male");
+    }
+
+    @Test
+    public void testSignificantNextSibling() throws IOException, 
TemplateException {
+        assertOutput("${doc.person.name.@@next_sibling_element}", "12th 
August");
+    }
+
+    @Test
+    public void testNullSignificantPreviousSibling() throws IOException, 
TemplateException {
+        assertOutput("${doc.person.phone.@@next_sibling_element?size}", "0");
+    }
+
+    @Test
+    public void testSkippingCommentNode() throws IOException, 
TemplateException {
+        assertOutput("${doc.person.profession.@@previous_sibling_element}", 
"Chennai, India");
+    }
+
+    @Test
+    public void testSkippingEmptyCDataNode() throws IOException, 
TemplateException {
+        assertOutput("${doc.person.hobby.@@previous_sibling_element}", 
"Software Engineer");
+    }
+
+    @Test
+    public void testValidCDataNode() throws IOException, TemplateException {
+        assertOutput("${doc.person.phone.@@previous_sibling_element?size}", 
"0");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59412b29/src/test/java/org/apache/freemarker/dom/DOMSimplifiersTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/dom/DOMSimplifiersTest.java 
b/src/test/java/org/apache/freemarker/dom/DOMSimplifiersTest.java
new file mode 100644
index 0000000..103ceb4
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/dom/DOMSimplifiersTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.freemarker.dom;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.freemarker.test.util.XMLLoader;
+import org.junit.Test;
+import org.w3c.dom.CDATASection;
+import org.w3c.dom.Comment;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentType;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.ProcessingInstruction;
+import org.w3c.dom.Text;
+import org.xml.sax.SAXException;
+
+public class DOMSimplifiersTest {
+
+    private static final String COMMON_TEST_XML
+            = "<!DOCTYPE a 
[]><?p?><a>x<![CDATA[y]]><!--c--><?p?>z<?p?><b><!--c--></b><c></c>"
+              + "<d>a<e>c</e>b<!--c--><!--c--><!--c--><?p?><?p?><?p?></d>"
+              + "<f><![CDATA[1]]>2</f></a><!--c-->";
+
+    private static final String TEXT_MERGE_CONTENT =
+            "<a>"
+            + "a<!--c--><s/>"
+            + "<!--c-->a<s/>"
+            + "a<!--c-->b<s/>"
+            + "<!--c-->a<!--c-->b<!--c--><s/>"
+            + "a<b>b</b>c<s/>"
+            + "a<b>b</b><!--c-->c<s/>"
+            + "a<!--c-->1<b>b<!--c--></b>c<!--c-->1<s/>"
+            + "a<!--c-->1<b>b<!--c-->c</b>d<!--c-->1<s/>"
+            + "a<!--c-->1<b>b<!--c-->c</b>d<!--c-->1<s/>"
+            + 
"a<!--c-->1<b>b<!--c-->1<e>c<!--c-->1</e>d<!--c-->1</b>e<!--c-->1<s/>"
+            + "</a>";
+    private static final String TEXT_MERGE_EXPECTED =
+            "<a>"
+            + "%a<s/>"
+            + "%a<s/>"
+            + "%ab<s/>"
+            + "%ab<s/>"
+            + "%a<b>%b</b>%c<s/>"
+            + "%a<b>%b</b>%c<s/>"
+            + "%a1<b>%b</b>%c1<s/>"
+            + "%a1<b>%bc</b>%d1<s/>"
+            + "%a1<b>%bc</b>%d1<s/>"
+            + "%a1<b>%b1<e>%c1</e>%d1</b>%e1<s/>"
+            + "</a>";
+    
+    @Test
+    public void testTest() throws Exception {
+        String expected = "<!DOCTYPE 
...><?p?><a>%x<![CDATA[y]]><!--c--><?p?>%z<?p?><b><!--c--></b><c/>"
+                   + 
"<d>%a<e>%c</e>%b<!--c--><!--c--><!--c--><?p?><?p?><?p?></d>"
+                   + "<f><![CDATA[1]]>%2</f></a><!--c-->";
+        assertEquals(expected, toString(XMLLoader.toDOM(COMMON_TEST_XML)));
+    }
+
+    @Test
+    public void testMergeAdjacentText() throws Exception {
+        Document dom = XMLLoader.toDOM(COMMON_TEST_XML);
+        NodeModel.mergeAdjacentText(dom);
+        assertEquals(
+                "<!DOCTYPE 
...><?p?><a>%xy<!--c--><?p?>%z<?p?><b><!--c--></b><c/>"
+                + "<d>%a<e>%c</e>%b<!--c--><!--c--><!--c--><?p?><?p?><?p?></d>"
+                + "<f><![CDATA[12]]></f></a><!--c-->",
+                toString(dom));
+    }
+
+    @Test
+    public void testRemoveComments() throws Exception {
+        Document dom = XMLLoader.toDOM(COMMON_TEST_XML);
+        NodeModel.removeComments(dom);
+        assertEquals(
+                "<!DOCTYPE ...><?p?><a>%x<![CDATA[y]]><?p?>%z<?p?><b/><c/>"
+                + "<d>%a<e>%c</e>%b<?p?><?p?><?p?></d>"
+                + "<f><![CDATA[1]]>%2</f></a>",
+                toString(dom));
+    }
+
+    @Test
+    public void testRemovePIs() throws Exception {
+        Document dom = XMLLoader.toDOM(COMMON_TEST_XML);
+        NodeModel.removePIs(dom);
+        assertEquals(
+                "<!DOCTYPE ...><a>%x<![CDATA[y]]><!--c-->%z<b><!--c--></b><c/>"
+                + "<d>%a<e>%c</e>%b<!--c--><!--c--><!--c--></d>"
+                + "<f><![CDATA[1]]>%2</f></a><!--c-->",
+                toString(dom));
+    }
+    
+    @Test
+    public void testSimplify() throws Exception {
+        testSimplify(
+                "<!DOCTYPE ...><a>%xyz<b/><c/>"
+                + "<d>%a<e>%c</e>%b</d><f><![CDATA[12]]></f></a>",
+                COMMON_TEST_XML);
+    }
+
+    @Test
+    public void testSimplify2() throws Exception {
+        testSimplify(TEXT_MERGE_EXPECTED, TEXT_MERGE_CONTENT);
+    }
+
+    @Test
+    public void testSimplify3() throws Exception {
+        testSimplify("<a/>", "<a/>");
+    }
+    
+    private void testSimplify(String expected, String content)
+            throws SAXException, IOException, ParserConfigurationException {
+        {
+            Document dom = XMLLoader.toDOM(content);
+            NodeModel.simplify(dom);
+            assertEquals(expected, toString(dom));
+        }
+        
+        // Must be equivalent:
+        {
+            Document dom = XMLLoader.toDOM(content);
+            NodeModel.removeComments(dom);
+            NodeModel.removePIs(dom);
+            NodeModel.mergeAdjacentText(dom);
+            assertEquals(expected, toString(dom));
+        }
+        
+        // Must be equivalent:
+        {
+            Document dom = XMLLoader.toDOM(content);
+            NodeModel.removeComments(dom);
+            NodeModel.removePIs(dom);
+            NodeModel.simplify(dom);
+            assertEquals(expected, toString(dom));
+        }
+    }
+
+    private String toString(Document doc) {
+        StringBuilder sb = new StringBuilder();
+        toString(doc, sb);
+        return sb.toString();
+    }
+
+    private void toString(Node node, StringBuilder sb) {
+        if (node instanceof Document) {
+            childrenToString(node, sb);
+        } else if (node instanceof Element) {
+            if (node.hasChildNodes()) {
+                sb.append("<").append(node.getNodeName()).append(">");
+                childrenToString(node, sb);
+                sb.append("</").append(node.getNodeName()).append(">");
+            } else {
+                sb.append("<").append(node.getNodeName()).append("/>");
+            }
+        } else if (node instanceof Text) {
+            if (node instanceof CDATASection) {
+                
sb.append("<![CDATA[").append(node.getNodeValue()).append("]]>");
+            } else {
+                sb.append("%").append(node.getNodeValue());
+            }
+        } else if (node instanceof Comment) {
+            sb.append("<!--").append(node.getNodeValue()).append("-->");
+        } else if (node instanceof ProcessingInstruction) {
+            sb.append("<?").append(node.getNodeName()).append("?>");
+        } else if (node instanceof DocumentType) {
+            sb.append("<!DOCTYPE ...>");
+        } else {
+            throw new IllegalStateException("Unhandled node type: " + 
node.getClass().getName());
+        }
+    }
+
+    private void childrenToString(Node node, StringBuilder sb) {
+        Node child = node.getFirstChild();
+        while (child != null) {
+            toString(child, sb);
+            child = child.getNextSibling();
+        }
+    }
+    
+}


Reply via email to