Removed NodeModel static utility classes dealing with parsing XML to DOM. How it's best to do that is environment and application dependent, and it has security implications. Since XML loading/parsing it's not the topic of the project, these were removed. Static methods that simplify an already loaded DOM have remained, because that's a FreeMarker-specific functionality.
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/ea3a06b5 Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/ea3a06b5 Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/ea3a06b5 Branch: refs/heads/3 Commit: ea3a06b5b085b4ce27e78b250d95a9a997d4b626 Parents: 70dff09 Author: ddekany <ddek...@apache.org> Authored: Mon Feb 20 21:23:33 2017 +0100 Committer: ddekany <ddek...@apache.org> Committed: Mon Feb 20 21:23:33 2017 +0100 ---------------------------------------------------------------------- .../core/model/impl/dom/NodeModel.java | 193 +---------------- src/manual/en_US/FM3-CHANGE-LOG.txt | 4 + .../impl/dom/DOMConvenienceStaticsTest.java | 216 ------------------- .../core/model/impl/dom/DOMSiblingTest.java | 4 +- .../core/model/impl/dom/DOMSimplifiersTest.java | 201 +++++++++++++++++ .../freemarker/core/model/impl/dom/DOMTest.java | 6 +- .../test/templatesuite/TemplateTestCase.java | 13 +- .../apache/freemarker/test/util/XMLLoader.java | 138 ++++++++++++ 8 files changed, 366 insertions(+), 409 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ea3a06b5/src/main/java/org/apache/freemarker/core/model/impl/dom/NodeModel.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/dom/NodeModel.java b/src/main/java/org/apache/freemarker/core/model/impl/dom/NodeModel.java index a1679d5..765084d 100644 --- a/src/main/java/org/apache/freemarker/core/model/impl/dom/NodeModel.java +++ b/src/main/java/org/apache/freemarker/core/model/impl/dom/NodeModel.java @@ -20,20 +20,12 @@ package org.apache.freemarker.core.model.impl.dom; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; import java.lang.ref.WeakReference; -import java.net.MalformedURLException; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.WeakHashMap; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - import org.apache.freemarker.core.Configuration; import org.apache.freemarker.core._CoreLogs; import org.apache.freemarker.core.ast._UnexpectedTypeErrorExplainerTemplateModel; @@ -61,9 +53,6 @@ import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.ProcessingInstruction; import org.w3c.dom.Text; -import org.xml.sax.ErrorHandler; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; /** * A base class for wrapping a single W3C DOM_WRAPPER Node as a FreeMarker template model. @@ -90,14 +79,10 @@ implements TemplateNodeModelEx, TemplateHashModel, TemplateSequenceModel, private static final Object STATIC_LOCK = new Object(); - static private DocumentBuilderFactory docBuilderFactory; - static private final Map xpathSupportMap = Collections.synchronizedMap(new WeakHashMap()); static private XPathSupport jaxenXPathSupport; - static private ErrorHandler errorHandler; - static Class xpathSupportClass; static { @@ -118,173 +103,6 @@ implements TemplateNodeModelEx, TemplateHashModel, TemplateSequenceModel, private TemplateSequenceModel children; private NodeModel parent; - /** - * Sets the DOM_WRAPPER parser implementation to be used when building {@link NodeModel} objects from XML files or from - * {@link InputStream} with the static convenience methods of {@link NodeModel}. Otherwise FreeMarker itself doesn't - * use this. - * - * @see #getDocumentBuilderFactory() - * - * @deprecated It's a bad practice to change static fields, as if multiple independent components do that in the - * same JVM, they unintentionally affect each other. Therefore it's recommended to leave this static - * value at its default. - */ - @Deprecated - static public void setDocumentBuilderFactory(DocumentBuilderFactory docBuilderFactory) { - synchronized (STATIC_LOCK) { - NodeModel.docBuilderFactory = docBuilderFactory; - } - } - - /** - * Returns the DOM_WRAPPER parser implementation that is used when building {@link NodeModel} objects from XML files or from - * {@link InputStream} with the static convenience methods of {@link NodeModel}. Otherwise FreeMarker itself doesn't - * use this. - * - * @see #setDocumentBuilderFactory(DocumentBuilderFactory) - */ - static public DocumentBuilderFactory getDocumentBuilderFactory() { - synchronized (STATIC_LOCK) { - if (docBuilderFactory == null) { - DocumentBuilderFactory newFactory = DocumentBuilderFactory.newInstance(); - newFactory.setNamespaceAware(true); - newFactory.setIgnoringElementContentWhitespace(true); - docBuilderFactory = newFactory; // We only write it out when the initialization was full - } - return docBuilderFactory; - } - } - - /** - * Sets the error handler to use when parsing the document with the static convenience methods of {@link NodeModel}. - * - * @deprecated It's a bad practice to change static fields, as if multiple independent components do that in the - * same JVM, they unintentionally affect each other. Therefore it's recommended to leave this static - * value at its default. - * - * @see #getErrorHandler() - */ - @Deprecated - static public void setErrorHandler(ErrorHandler errorHandler) { - synchronized (STATIC_LOCK) { - NodeModel.errorHandler = errorHandler; - } - } - - /** - * @since 2.3.20 - * - * @see #setErrorHandler(ErrorHandler) - */ - static public ErrorHandler getErrorHandler() { - synchronized (STATIC_LOCK) { - return NodeModel.errorHandler; - } - } - - /** - * Convenience method to create a {@link NodeModel} from a SAX {@link InputSource}; please see the security warning - * further down. Adjacent text nodes will be merged (and CDATA sections are considered as text nodes) as with - * {@link #mergeAdjacentText(Node)}. Further simplifications are applied depending on the parameters. If all - * simplifications are turned on, then it applies {@link #simplify(Node)} on the loaded DOM_WRAPPER. - * - * <p> - * Note that {@code parse(...)} is only a convenience method, and FreeMarker itself doesn't use it (except when you - * call the other similar static convenience methods, as they may build on each other). In particular, if you want - * full control over the {@link DocumentBuilderFactory} used, create the {@link Node} with your own - * {@link DocumentBuilderFactory}, apply {@link #simplify(Node)} (or such) on it, then call - * {@link NodeModel#wrap(Node)}. - * - * <p> - * <b>Security warning:</b> If the XML to load is coming from a source that you can't fully trust, you shouldn't use - * this method, as the {@link DocumentBuilderFactory} it uses by default supports external entities, and so it - * doesn't prevent XML External Entity (XXE) attacks. Note that XXE attacks are not specific to FreeMarker, they - * affect all XML parsers in general. If that's a problem in your application, OWASP has a cheat sheet to set up a - * {@link DocumentBuilderFactory} that has limited functionality but is immune to XXE attacks. Because it's just a - * convenience method, you can just use your own {@link DocumentBuilderFactory} and do a few extra steps instead - * (see earlier). - * - * @param removeComments - * Whether to remove all comment nodes (recursively); this is like calling {@link #removeComments(Node)} - * @param removePIs - * Whether to remove all processing instruction nodes (recursively); this is like calling - * {@link #removePIs(Node)} - */ - static public NodeModel parse(InputSource is, boolean removeComments, boolean removePIs) - throws SAXException, IOException, ParserConfigurationException { - DocumentBuilder builder = getDocumentBuilderFactory().newDocumentBuilder(); - ErrorHandler errorHandler = getErrorHandler(); - if (errorHandler != null) builder.setErrorHandler(errorHandler); - final Document doc; - try { - doc = builder.parse(is); - } catch (MalformedURLException e) { - // This typical error has an error message that is hard to understand, so let's translate it: - if (is.getSystemId() == null && is.getCharacterStream() == null && is.getByteStream() == null) { - throw new MalformedURLException( - "The SAX InputSource has systemId == null && characterStream == null && byteStream == null. " - + "This is often because it was created with a null InputStream or Reader, which is often because " - + "the XML file it should point to was not found. " - + "(The original exception was: " + e + ")"); - } else { - throw e; - } - } - if (removeComments && removePIs) { - simplify(doc); - } else { - if (removeComments) { - removeComments(doc); - } - if (removePIs) { - removePIs(doc); - } - mergeAdjacentText(doc); - } - return wrap(doc); - } - - /** - * Same as {@link #parse(InputSource, boolean, boolean) parse(is, true, true)}; don't miss the security warnings - * documented there. - */ - static public NodeModel parse(InputSource is) throws SAXException, IOException, ParserConfigurationException { - return parse(is, true, true); - } - - - /** - * Same as {@link #parse(InputSource, boolean, boolean)}, but loads from a {@link File}; don't miss the security - * warnings documented there. - */ - static public NodeModel parse(File f, boolean removeComments, boolean removePIs) - throws SAXException, IOException, ParserConfigurationException { - DocumentBuilder builder = getDocumentBuilderFactory().newDocumentBuilder(); - ErrorHandler errorHandler = getErrorHandler(); - if (errorHandler != null) builder.setErrorHandler(errorHandler); - Document doc = builder.parse(f); - if (removeComments && removePIs) { - simplify(doc); - } else { - if (removeComments) { - removeComments(doc); - } - if (removePIs) { - removePIs(doc); - } - mergeAdjacentText(doc); - } - return wrap(doc); - } - - /** - * Same as {@link #parse(InputSource, boolean, boolean) parse(source, true, true)}, but loads from a {@link File}; - * don't miss the security warnings documented there. - */ - static public NodeModel parse(File f) throws SAXException, IOException, ParserConfigurationException { - return parse(f, true, true); - } - protected NodeModel(Node node) { this.node = node; } @@ -450,6 +268,14 @@ implements TemplateNodeModelEx, TemplateHashModel, TemplateSequenceModel, && ((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; @@ -464,6 +290,9 @@ implements TemplateNodeModelEx, TemplateHashModel, TemplateSequenceModel, 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; } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ea3a06b5/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 891b5a8..68899fb 100644 --- a/src/manual/en_US/FM3-CHANGE-LOG.txt +++ b/src/manual/en_US/FM3-CHANGE-LOG.txt @@ -102,3 +102,7 @@ the FreeMarer 3 changelog here: - Removed the deprecated BeansWrapper.nullModel setting. So null is always wrapped to null now. - Removed the overridable BeansWrapper.finetuneMethodAppearance method, which was deprecated by the finetuneMethodAppearance setting (BeansWrapper.setFinetuneMethodAppearance). + - Removed NodeModel static utility classes dealing with parsing XML to DOM. How it's best to do that is environment + and application dependent, and it has security implications. Since XML loading/parsing it's not the topic of the + project, these were removed. Static methods that simplify an already loaded DOM have remained, because that's + FreeMarker-specific functionality. http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ea3a06b5/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMConvenienceStaticsTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMConvenienceStaticsTest.java b/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMConvenienceStaticsTest.java deleted file mode 100644 index 14e293b..0000000 --- a/src/test/java/org/apache/freemarker/core/model/impl/dom/DOMConvenienceStaticsTest.java +++ /dev/null @@ -1,216 +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 java.io.StringReader; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.ParserConfigurationException; - -import org.apache.freemarker.core.model.impl.dom.NodeModel; -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.ErrorHandler; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - -public class DOMConvenienceStaticsTest { - - 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(toDOM(COMMON_TEST_XML))); - } - - @Test - public void testMergeAdjacentText() throws Exception { - Document dom = 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 = 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 = 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 = toDOM(content); - NodeModel.simplify(dom); - assertEquals(expected, toString(dom)); - } - - // Must be equivalent: - { - Document dom = toDOM(content); - NodeModel.removeComments(dom); - NodeModel.removePIs(dom); - NodeModel.mergeAdjacentText(dom); - assertEquals(expected, toString(dom)); - } - - // Must be equivalent: - { - Document dom = toDOM(content); - NodeModel.removeComments(dom); - NodeModel.removePIs(dom); - NodeModel.simplify(dom); - assertEquals(expected, toString(dom)); - } - } - - private Document toDOM(String content) throws SAXException, IOException, ParserConfigurationException { - DocumentBuilder builder = NodeModel.getDocumentBuilderFactory().newDocumentBuilder(); - ErrorHandler errorHandler = NodeModel.getErrorHandler(); - if (errorHandler != null) builder.setErrorHandler(errorHandler); - return builder.parse(toInputSource(content)); - } - - private InputSource toInputSource(String content) { - return new InputSource(new StringReader(content)); - } - - 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/ea3a06b5/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 index a4b85fa..435fb4e 100644 --- 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 @@ -24,8 +24,8 @@ import java.io.IOException; import javax.xml.parsers.ParserConfigurationException; import org.apache.freemarker.core.TemplateException; -import org.apache.freemarker.core.model.impl.dom.NodeModel; 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; @@ -36,7 +36,7 @@ public class DOMSiblingTest extends TemplateTest { @Before public void setUp() throws SAXException, IOException, ParserConfigurationException { InputSource is = new InputSource(getClass().getResourceAsStream("DOMSiblingTest.xml")); - addToDataModel("doc", NodeModel.parse(is)); + addToDataModel("doc", XMLLoader.toModel(is)); } @Test http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ea3a06b5/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 new file mode 100644 index 0000000..91d0b89 --- /dev/null +++ b/src/test/java/org/apache/freemarker/core/model/impl/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.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/ea3a06b5/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 index eee0eb1..387b863 100644 --- 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 @@ -31,8 +31,8 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.freemarker.core.TemplateException; -import org.apache.freemarker.core.model.impl.dom.NodeModel; 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; @@ -98,11 +98,11 @@ public class DOMTest extends TemplateTest { } private void addDocToDataModel(String xml) throws SAXException, IOException, ParserConfigurationException { - addToDataModel("doc", NodeModel.parse(new InputSource(new StringReader(xml)))); + addToDataModel("doc", XMLLoader.toModel(new InputSource(new StringReader(xml)))); } private void addDocToDataModelNoSimplification(String xml) throws SAXException, IOException, ParserConfigurationException { - addToDataModel("doc", NodeModel.parse(new InputSource(new StringReader(xml)), false, false)); + addToDataModel("doc", XMLLoader.toModel(new InputSource(new StringReader(xml)), false)); } private void addNSUnawareDocToDataModel(String xml) throws ParserConfigurationException, SAXException, IOException { http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ea3a06b5/src/test/java/org/apache/freemarker/test/templatesuite/TemplateTestCase.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/test/templatesuite/TemplateTestCase.java b/src/test/java/org/apache/freemarker/test/templatesuite/TemplateTestCase.java index c210728..2f28dab 100644 --- a/src/test/java/org/apache/freemarker/test/templatesuite/TemplateTestCase.java +++ b/src/test/java/org/apache/freemarker/test/templatesuite/TemplateTestCase.java @@ -86,6 +86,7 @@ import org.apache.freemarker.test.util.AssertEqualsDirective; import org.apache.freemarker.test.util.AssertFailsDirective; import org.apache.freemarker.test.util.FileTestCase; import org.apache.freemarker.test.util.NoOutputDirective; +import org.apache.freemarker.test.util.XMLLoader; import org.junit.Ignore; import org.xml.sax.InputSource; @@ -319,7 +320,7 @@ public class TemplateTestCase extends FileTestCase { dataModel.put("mMixed", mMixed); } else if (simpleTestName.equals("default-xmlns")) { InputSource is = new InputSource(getClass().getResourceAsStream("models/defaultxmlns1.xml")); - NodeModel nm = NodeModel.parse(is); + NodeModel nm = XMLLoader.toModel(is); dataModel.put("doc", nm); } else if (simpleTestName.equals("multimodels")) { dataModel.put("test", "selftest"); @@ -355,23 +356,23 @@ public class TemplateTestCase extends FileTestCase { dataModel.put("node", NodeModel.wrap(doc.getDocumentElement().getFirstChild().getFirstChild())); } else if (simpleTestName.equals("xmlns1")) { InputSource is = new InputSource(getClass().getResourceAsStream("models/xmlns.xml")); - NodeModel nm = NodeModel.parse(is); + NodeModel nm = XMLLoader.toModel(is); dataModel.put("doc", nm); } else if (simpleTestName.equals("xmlns2")) { InputSource is = new InputSource(getClass().getResourceAsStream("models/xmlns2.xml")); - NodeModel nm = NodeModel.parse(is); + NodeModel nm = XMLLoader.toModel(is); dataModel.put("doc", nm); } else if (simpleTestName.equals("xmlns3") || simpleTestName.equals("xmlns4")) { InputSource is = new InputSource(getClass().getResourceAsStream("models/xmlns3.xml")); - NodeModel nm = NodeModel.parse(is); + NodeModel nm = XMLLoader.toModel(is); dataModel.put("doc", nm); } else if (simpleTestName.equals("xmlns5")) { InputSource is = new InputSource(getClass().getResourceAsStream("models/defaultxmlns1.xml")); - NodeModel nm = NodeModel.parse(is); + NodeModel nm = XMLLoader.toModel(is); dataModel.put("doc", nm); } else if (simpleTestName.equals("xml-ns_prefix-scope")) { InputSource is = new InputSource(getClass().getResourceAsStream("models/xml-ns_prefix-scope.xml")); - NodeModel nm = NodeModel.parse(is); + NodeModel nm = XMLLoader.toModel(is); dataModel.put("doc", nm); } else if (simpleTestName.startsWith("sequence-builtins")) { Set<String> abcSet = new TreeSet<>(); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ea3a06b5/src/test/java/org/apache/freemarker/test/util/XMLLoader.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/test/util/XMLLoader.java b/src/test/java/org/apache/freemarker/test/util/XMLLoader.java new file mode 100644 index 0000000..7226f97 --- /dev/null +++ b/src/test/java/org/apache/freemarker/test/util/XMLLoader.java @@ -0,0 +1,138 @@ +/* + * 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.test.util; + +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.net.MalformedURLException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.freemarker.core.model.impl.dom.NodeModel; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +public final class XMLLoader { + + private static final Object STATIC_LOCK = new Object(); + + static private DocumentBuilderFactory docBuilderFactory; + + private XMLLoader() { + // + } + + /** + * Convenience method to create a {@link NodeModel} from a SAX {@link InputSource}. + */ + static public NodeModel toModel(InputSource is, boolean simplify) + throws SAXException, IOException, ParserConfigurationException { + DocumentBuilder builder = getDocumentBuilderFactory().newDocumentBuilder(); + final Document doc; + try { + doc = builder.parse(is); + } catch (MalformedURLException e) { + // This typical error has an error message that is hard to understand, so let's translate it: + if (is.getSystemId() == null && is.getCharacterStream() == null && is.getByteStream() == null) { + throw new MalformedURLException( + "The SAX InputSource has systemId == null && characterStream == null && byteStream == null. " + + "This is often because it was created with a null InputStream or Reader, which is often because " + + "the XML file it should point to was not found. " + + "(The original exception was: " + e + ")"); + } else { + throw e; + } + } + if (simplify) { + NodeModel.simplify(doc); + } + return NodeModel.wrap(doc); + } + + /** + * Same as {@link #toModel(InputSource, boolean) parse(is, true)}. + */ + static public NodeModel toModel(InputSource is) throws SAXException, IOException, ParserConfigurationException { + return toModel(is, true); + } + + /** + * Same as {@link #toModel(InputSource, boolean)}, but loads from a {@link File}; don't miss the security + * warnings documented there. + */ + static public NodeModel toModel(File f, boolean simplify) + throws SAXException, IOException, ParserConfigurationException { + DocumentBuilder builder = getDocumentBuilderFactory().newDocumentBuilder(); + Document doc = builder.parse(f); + if (simplify) { + NodeModel.simplify(doc); + } + return NodeModel.wrap(doc); + } + + /** + * Same as {@link #toModel(InputSource, boolean) parse(source, true)}, but loads from a {@link String}. + */ + static public NodeModel toModel(File f) throws SAXException, IOException, ParserConfigurationException { + return toModel(f, true); + } + + /** + * Same as {@link #toModel(InputSource, boolean)}, but loads from a {@link File}; don't miss the security + * warnings documented there. + */ + static public NodeModel toModel(String content, boolean simplify) + throws SAXException, IOException, ParserConfigurationException { + return toModel(toInputSource(content)); + } + + /** + * Same as {@link #toModel(InputSource, boolean) parse(source, true)}, but loads from a {@link String}. + */ + static public NodeModel toModel(String content) throws SAXException, IOException, ParserConfigurationException { + return toModel(content, true); + } + + public static Document toDOM(String content) throws SAXException, IOException, ParserConfigurationException { + DocumentBuilder builder = getDocumentBuilderFactory().newDocumentBuilder(); + return builder.parse(toInputSource(content)); + } + + static private DocumentBuilderFactory getDocumentBuilderFactory() { + synchronized (STATIC_LOCK) { + if (docBuilderFactory == null) { + DocumentBuilderFactory newFactory = DocumentBuilderFactory.newInstance(); + newFactory.setNamespaceAware(true); + newFactory.setIgnoringElementContentWhitespace(true); + docBuilderFactory = newFactory; // We only write it out when the initialization was full + } + return docBuilderFactory; + } + } + + private static InputSource toInputSource(String content) { + return new InputSource(new StringReader(content)); + } + +}