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?> <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(); + } + } + +}
