You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/05/21 21:34:58 UTC
[3/6] incubator-freemarker git commit: Factored out freemarker-dom
from freemarker-core. Also added mechanism to "inject" DOM wrapping
capability into DefaultObjectWrapper on configuration time. See details
below.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/be556897/freemarker-dom/src/main/java/org/apache/freemarker/dom/ElementModel.java
----------------------------------------------------------------------
diff --git a/freemarker-dom/src/main/java/org/apache/freemarker/dom/ElementModel.java b/freemarker-dom/src/main/java/org/apache/freemarker/dom/ElementModel.java
new file mode 100644
index 0000000..220f414
--- /dev/null
+++ b/freemarker-dom/src/main/java/org/apache/freemarker/dom/ElementModel.java
@@ -0,0 +1,234 @@
+/*
+ * 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.Collections;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util._StringUtil;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+class ElementModel extends NodeModel implements TemplateScalarModel {
+
+ public ElementModel(Element element) {
+ super(element);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ /**
+ * An Element node supports various hash keys.
+ * Any key that corresponds to the tag name of any child elements
+ * returns a sequence of those elements. The special key "*" returns
+ * all the element's direct children.
+ * The "**" key return all the element's descendants in the order they
+ * occur in the document.
+ * Any key starting with '@' is taken to be the name of an element attribute.
+ * The special key "@@" returns a hash of all the element's attributes.
+ * The special key "/" returns the root document node associated with this element.
+ */
+ @Override
+ public TemplateModel get(String key) throws TemplateModelException {
+ if (key.equals("*")) {
+ NodeListModel ns = new NodeListModel(this);
+ TemplateSequenceModel children = getChildNodes();
+ for (int i = 0; i < children.size(); i++) {
+ NodeModel child = (NodeModel) children.get(i);
+ if (child.node.getNodeType() == Node.ELEMENT_NODE) {
+ ns.add(child);
+ }
+ }
+ return ns;
+ } else if (key.equals("**")) {
+ return new NodeListModel(((Element) node).getElementsByTagName("*"), this);
+ } else if (key.startsWith("@")) {
+ if (key.startsWith("@@")) {
+ if (key.equals(AtAtKey.ATTRIBUTES.getKey())) {
+ return new NodeListModel(node.getAttributes(), this);
+ } else if (key.equals(AtAtKey.START_TAG.getKey())) {
+ NodeOutputter nodeOutputter = new NodeOutputter(node);
+ return new SimpleScalar(nodeOutputter.getOpeningTag((Element) node));
+ } else if (key.equals(AtAtKey.END_TAG.getKey())) {
+ NodeOutputter nodeOutputter = new NodeOutputter(node);
+ return new SimpleScalar(nodeOutputter.getClosingTag((Element) node));
+ } else if (key.equals(AtAtKey.ATTRIBUTES_MARKUP.getKey())) {
+ StringBuilder buf = new StringBuilder();
+ NodeOutputter nu = new NodeOutputter(node);
+ nu.outputContent(node.getAttributes(), buf);
+ return new SimpleScalar(buf.toString().trim());
+ } else if (key.equals(AtAtKey.PREVIOUS_SIBLING_ELEMENT.getKey())) {
+ Node previousSibling = node.getPreviousSibling();
+ while (previousSibling != null && !isSignificantNode(previousSibling)) {
+ previousSibling = previousSibling.getPreviousSibling();
+ }
+ return previousSibling != null && previousSibling.getNodeType() == Node.ELEMENT_NODE
+ ? wrap(previousSibling) : new NodeListModel(Collections.emptyList(), null);
+ } else if (key.equals(AtAtKey.NEXT_SIBLING_ELEMENT.getKey())) {
+ Node nextSibling = node.getNextSibling();
+ while (nextSibling != null && !isSignificantNode(nextSibling)) {
+ nextSibling = nextSibling.getNextSibling();
+ }
+ return nextSibling != null && nextSibling.getNodeType() == Node.ELEMENT_NODE
+ ? wrap(nextSibling) : new NodeListModel(Collections.emptyList(), null);
+ } else {
+ // We don't know anything like this that's element-specific; fall back
+ return super.get(key);
+ }
+ } else { // Starts with "@", but not with "@@"
+ if (DomStringUtil.isXMLNameLike(key, 1)) {
+ Attr att = getAttribute(key.substring(1));
+ if (att == null) {
+ return new NodeListModel(this);
+ }
+ return wrap(att);
+ } else if (key.equals("@*")) {
+ return new NodeListModel(node.getAttributes(), this);
+ } else {
+ // We don't know anything like this that's element-specific; fall back
+ return super.get(key);
+ }
+ }
+ } else if (DomStringUtil.isXMLNameLike(key)) {
+ // We interpret key as an element name
+ NodeListModel result = ((NodeListModel) getChildNodes()).filterByName(key);
+ return result.size() != 1 ? result : result.get(0);
+ } else {
+ // We don't anything like this that's element-specific; fall back
+ return super.get(key);
+ }
+ }
+
+ @Override
+ public String getAsString() throws TemplateModelException {
+ NodeList nl = node.getChildNodes();
+ String result = "";
+ for (int i = 0; i < nl.getLength(); i++) {
+ Node child = nl.item(i);
+ int nodeType = child.getNodeType();
+ if (nodeType == Node.ELEMENT_NODE) {
+ String msg = "Only elements with no child elements can be processed as text."
+ + "\nThis element with name \""
+ + node.getNodeName()
+ + "\" has a child element named: " + child.getNodeName();
+ throw new TemplateModelException(msg);
+ } else if (nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE) {
+ result += child.getNodeValue();
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public String getNodeName() {
+ String result = node.getLocalName();
+ if (result == null || result.equals("")) {
+ result = node.getNodeName();
+ }
+ return result;
+ }
+
+ @Override
+ String getQualifiedName() {
+ String nodeName = getNodeName();
+ String nsURI = getNodeNamespace();
+ if (nsURI == null || nsURI.length() == 0) {
+ return nodeName;
+ }
+ Environment env = Environment.getCurrentEnvironment();
+ String defaultNS = env.getDefaultNS();
+ String prefix;
+ if (defaultNS != null && defaultNS.equals(nsURI)) {
+ prefix = "";
+ } else {
+ prefix = env.getPrefixForNamespace(nsURI);
+
+ }
+ if (prefix == null) {
+ return null; // We have no qualified name, because there is no prefix mapping
+ }
+ if (prefix.length() > 0) {
+ prefix += ":";
+ }
+ return prefix + nodeName;
+ }
+
+ private Attr getAttribute(String qname) {
+ Element element = (Element) node;
+ Attr result = element.getAttributeNode(qname);
+ if (result != null)
+ return result;
+ int colonIndex = qname.indexOf(':');
+ if (colonIndex > 0) {
+ String prefix = qname.substring(0, colonIndex);
+ String uri;
+ if (prefix.equals(Template.DEFAULT_NAMESPACE_PREFIX)) {
+ uri = Environment.getCurrentEnvironment().getDefaultNS();
+ } else {
+ uri = Environment.getCurrentEnvironment().getNamespaceForPrefix(prefix);
+ }
+ String localName = qname.substring(1 + colonIndex);
+ if (uri != null) {
+ result = element.getAttributeNodeNS(uri, localName);
+ }
+ }
+ return result;
+ }
+
+ private boolean isSignificantNode(Node node) throws TemplateModelException {
+ return (node.getNodeType() == Node.TEXT_NODE || node.getNodeType() == Node.CDATA_SECTION_NODE)
+ ? !isBlankXMLText(node.getTextContent())
+ : node.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE && node.getNodeType() != Node.COMMENT_NODE;
+ }
+
+ private boolean isBlankXMLText(String s) {
+ if (s == null) {
+ return true;
+ }
+ for (int i = 0; i < s.length(); i++) {
+ if (!isXMLWhiteSpace(s.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * White space according the XML spec.
+ */
+ private boolean isXMLWhiteSpace(char c) {
+ return c == ' ' || c == '\t' || c == '\n' | c == '\r';
+ }
+
+ boolean matchesName(String name, Environment env) {
+ return _StringUtil.matchesQName(name, getNodeName(), getNodeNamespace(), env);
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/be556897/freemarker-dom/src/main/java/org/apache/freemarker/dom/JaxenXPathSupport.java
----------------------------------------------------------------------
diff --git a/freemarker-dom/src/main/java/org/apache/freemarker/dom/JaxenXPathSupport.java b/freemarker-dom/src/main/java/org/apache/freemarker/dom/JaxenXPathSupport.java
new file mode 100644
index 0000000..3e52836
--- /dev/null
+++ b/freemarker-dom/src/main/java/org/apache/freemarker/dom/JaxenXPathSupport.java
@@ -0,0 +1,243 @@
+/*
+ * 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 java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.apache.freemarker.core.CustomStateKey;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+import org.apache.freemarker.core.util._ObjectHolder;
+import org.jaxen.BaseXPath;
+import org.jaxen.Function;
+import org.jaxen.FunctionCallException;
+import org.jaxen.FunctionContext;
+import org.jaxen.JaxenException;
+import org.jaxen.NamespaceContext;
+import org.jaxen.Navigator;
+import org.jaxen.UnresolvableException;
+import org.jaxen.VariableContext;
+import org.jaxen.XPathFunctionContext;
+import org.jaxen.dom.DocumentNavigator;
+import org.w3c.dom.Document;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+
+/**
+ */
+class JaxenXPathSupport implements XPathSupport {
+
+ private static final CustomStateKey<Map<String, BaseXPath>> XPATH_CACHE_ATTR
+ = new CustomStateKey<Map<String, BaseXPath>>() {
+ @Override
+ protected Map<String, BaseXPath> create() {
+ return new HashMap<String, BaseXPath>();
+ }
+ };
+
+ // [2.4] Can't we just use Collections.emptyList()?
+ private final static ArrayList EMPTY_ARRAYLIST = new ArrayList();
+
+ @Override
+ public TemplateModel executeQuery(Object context, String xpathQuery) throws TemplateModelException {
+ try {
+ BaseXPath xpath;
+ Map<String, BaseXPath> xpathCache = Environment.getCurrentEnvironmentNotNull().getCurrentTemplateNotNull()
+ .getCustomState(XPATH_CACHE_ATTR);
+ synchronized (xpathCache) {
+ xpath = xpathCache.get(xpathQuery);
+ if (xpath == null) {
+ xpath = new BaseXPath(xpathQuery, FM_DOM_NAVIGATOR);
+ xpath.setNamespaceContext(customNamespaceContext);
+ xpath.setFunctionContext(FM_FUNCTION_CONTEXT);
+ xpath.setVariableContext(FM_VARIABLE_CONTEXT);
+ xpathCache.put(xpathQuery, xpath);
+ }
+ }
+ List result = xpath.selectNodes(context != null ? context : EMPTY_ARRAYLIST);
+ if (result.size() == 1) {
+ return NodeQueryResultItemObjectWrapper.INSTANCE.wrap(result.get(0));
+ }
+ NodeListModel nlm = new NodeListModel(result, null);
+ nlm.xpathSupport = this;
+ return nlm;
+ } catch (UndeclaredThrowableException e) {
+ Throwable t = e.getUndeclaredThrowable();
+ if (t instanceof TemplateModelException) {
+ throw (TemplateModelException) t;
+ }
+ throw e;
+ } catch (JaxenException je) {
+ throw new TemplateModelException(je);
+ }
+ }
+
+ static private final NamespaceContext customNamespaceContext = new NamespaceContext() {
+
+ @Override
+ public String translateNamespacePrefixToUri(String prefix) {
+ if (prefix.equals(Template.DEFAULT_NAMESPACE_PREFIX)) {
+ return Environment.getCurrentEnvironment().getDefaultNS();
+ }
+ return Environment.getCurrentEnvironment().getNamespaceForPrefix(prefix);
+ }
+ };
+
+ private static final VariableContext FM_VARIABLE_CONTEXT = new VariableContext() {
+ @Override
+ public Object getVariableValue(String namespaceURI, String prefix, String localName)
+ throws UnresolvableException {
+ try {
+ TemplateModel model = Environment.getCurrentEnvironment().getVariable(localName);
+ if (model == null) {
+ throw new UnresolvableException("Variable \"" + localName + "\" not found.");
+ }
+ if (model instanceof TemplateScalarModel) {
+ return ((TemplateScalarModel) model).getAsString();
+ }
+ if (model instanceof TemplateNumberModel) {
+ return ((TemplateNumberModel) model).getAsNumber();
+ }
+ if (model instanceof TemplateDateModel) {
+ return ((TemplateDateModel) model).getAsDate();
+ }
+ if (model instanceof TemplateBooleanModel) {
+ return Boolean.valueOf(((TemplateBooleanModel) model).getAsBoolean());
+ }
+ } catch (TemplateModelException e) {
+ throw new UndeclaredThrowableException(e);
+ }
+ throw new UnresolvableException(
+ "Variable \"" + localName + "\" exists, but it's not a string, number, date, or boolean");
+ }
+ };
+
+ private static final FunctionContext FM_FUNCTION_CONTEXT = new XPathFunctionContext() {
+ @Override
+ public Function getFunction(String namespaceURI, String prefix, String localName)
+ throws UnresolvableException {
+ try {
+ return super.getFunction(namespaceURI, prefix, localName);
+ } catch (UnresolvableException e) {
+ return super.getFunction(null, null, localName);
+ }
+ }
+ };
+
+ /**
+ * Stores the the template parsed as {@link Document} in the template itself.
+ */
+ private static final CustomStateKey<_ObjectHolder<Document>> FM_DOM_NAVIAGOTOR_CACHED_DOM
+ = new CustomStateKey<_ObjectHolder<Document>>() {
+ @Override
+ protected _ObjectHolder<Document> create() {
+ return new _ObjectHolder<>(null);
+ }
+ };
+
+ private static final Navigator FM_DOM_NAVIGATOR = new DocumentNavigator() {
+ @Override
+ public Object getDocument(String uri) throws FunctionCallException {
+ try {
+ Template raw = getTemplate(uri);
+ _ObjectHolder<Document> docHolder = Environment.getCurrentEnvironmentNotNull()
+ .getCurrentTemplateNotNull().getCustomState(FM_DOM_NAVIAGOTOR_CACHED_DOM);
+ synchronized (docHolder) {
+ Document doc = docHolder.get();
+ if (doc == null) {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ FmEntityResolver er = new FmEntityResolver();
+ builder.setEntityResolver(er);
+ doc = builder.parse(createInputSource(null, raw));
+ // If the entity resolver got called 0 times, the document
+ // is standalone, so we can safely cache it
+ if (er.getCallCount() == 0) {
+ docHolder.set(doc);
+ }
+ }
+ return doc;
+ }
+ } catch (Exception e) {
+ throw new FunctionCallException("Failed to parse document for URI: " + uri, e);
+ }
+ }
+ };
+
+ // [FM3] Look into this "hidden" feature
+ static Template getTemplate(String systemId) throws IOException {
+ Environment env = Environment.getCurrentEnvironment();
+ String templatePath = env.getCurrentTemplate().getLookupName();
+ int lastSlash = templatePath.lastIndexOf('/');
+ templatePath = lastSlash == -1 ? "" : templatePath.substring(0, lastSlash + 1);
+ systemId = env.toFullTemplateName(templatePath, systemId);
+ return env.getConfiguration().getTemplate(systemId, env.getLocale());
+ }
+
+ private static InputSource createInputSource(String publicId, Template raw) throws IOException, SAXException {
+ StringWriter sw = new StringWriter();
+ try {
+ raw.process(Collections.EMPTY_MAP, sw);
+ } catch (TemplateException e) {
+ throw new SAXException(e);
+ }
+ InputSource is = new InputSource();
+ is.setPublicId(publicId);
+ is.setSystemId(raw.getLookupName());
+ is.setCharacterStream(new StringReader(sw.toString()));
+ return is;
+ }
+
+ private static class FmEntityResolver implements EntityResolver {
+ private int callCount = 0;
+
+ @Override
+ public InputSource resolveEntity(String publicId, String systemId)
+ throws SAXException, IOException {
+ ++callCount;
+ return createInputSource(publicId, getTemplate(systemId));
+ }
+
+ int getCallCount() {
+ return callCount;
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/be556897/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeListModel.java
----------------------------------------------------------------------
diff --git a/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeListModel.java b/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeListModel.java
new file mode 100644
index 0000000..333bb5c
--- /dev/null
+++ b/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeListModel.java
@@ -0,0 +1,219 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core._UnexpectedTypeErrorExplainerTemplateModel;
+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.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.model.impl.SimpleSequence;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * Used when the result set contains 0 or multiple nodes; shouldn't be used when you have exactly 1 node. For exactly 1
+ * node, use {@link NodeModel#wrap(Node)}, because {@link NodeModel} subclasses can have extra features building on that
+ * restriction, like single elements with text content can be used as FTL string-s.
+ * <p>
+ * This class is not guaranteed to be thread safe, so instances of this shouldn't be used as
+ * {@linkplain Configuration#getSharedVariables() shared variable}.
+ */
+class NodeListModel extends SimpleSequence implements TemplateHashModel, _UnexpectedTypeErrorExplainerTemplateModel {
+
+ // [2.4] make these private
+ NodeModel contextNode;
+ XPathSupport xpathSupport;
+
+ NodeListModel(Node contextNode) {
+ this(NodeModel.wrap(contextNode));
+ }
+
+ NodeListModel(NodeModel contextNode) {
+ super(NodeQueryResultItemObjectWrapper.INSTANCE);
+ this.contextNode = contextNode;
+ }
+
+ NodeListModel(NodeList nodeList, NodeModel contextNode) {
+ super(NodeQueryResultItemObjectWrapper.INSTANCE);
+ for (int i = 0; i < nodeList.getLength(); i++) {
+ list.add(nodeList.item(i));
+ }
+ this.contextNode = contextNode;
+ }
+
+ NodeListModel(NamedNodeMap nodeList, NodeModel contextNode) {
+ super(NodeQueryResultItemObjectWrapper.INSTANCE);
+ for (int i = 0; i < nodeList.getLength(); i++) {
+ list.add(nodeList.item(i));
+ }
+ this.contextNode = contextNode;
+ }
+
+ NodeListModel(List list, NodeModel contextNode) {
+ super(list, NodeQueryResultItemObjectWrapper.INSTANCE);
+ this.contextNode = contextNode;
+ }
+
+ NodeListModel filterByName(String name) throws TemplateModelException {
+ NodeListModel result = new NodeListModel(contextNode);
+ int size = size();
+ if (size == 0) {
+ return result;
+ }
+ Environment env = Environment.getCurrentEnvironment();
+ for (int i = 0; i < size; i++) {
+ NodeModel nm = (NodeModel) get(i);
+ if (nm instanceof ElementModel) {
+ if (((ElementModel) nm).matchesName(name, env)) {
+ result.add(nm);
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return size() == 0;
+ }
+
+ @Override
+ public TemplateModel get(String key) throws TemplateModelException {
+ if (size() == 1) {
+ NodeModel nm = (NodeModel) get(0);
+ return nm.get(key);
+ }
+ if (key.startsWith("@@")) {
+ if (key.equals(AtAtKey.MARKUP.getKey())
+ || key.equals(AtAtKey.NESTED_MARKUP.getKey())
+ || key.equals(AtAtKey.TEXT.getKey())) {
+ StringBuilder result = new StringBuilder();
+ for (int i = 0; i < size(); i++) {
+ NodeModel nm = (NodeModel) get(i);
+ TemplateScalarModel textModel = (TemplateScalarModel) nm.get(key);
+ result.append(textModel.getAsString());
+ }
+ return new SimpleScalar(result.toString());
+ } else if (key.length() != 2 /* to allow "@@" to fall through */) {
+ // As @@... would cause exception in the XPath engine, we throw a nicer exception now.
+ if (AtAtKey.containsKey(key)) {
+ throw new TemplateModelException(
+ "\"" + key + "\" is only applicable to a single XML node, but it was applied on "
+ + (size() != 0
+ ? size() + " XML nodes (multiple matches)."
+ : "an empty list of XML nodes (no matches)."));
+ } else {
+ throw new TemplateModelException("Unsupported @@ key: " + key);
+ }
+ }
+ }
+ if (DomStringUtil.isXMLNameLike(key)
+ || ((key.startsWith("@")
+ && (DomStringUtil.isXMLNameLike(key, 1) || key.equals("@@") || key.equals("@*"))))
+ || key.equals("*") || key.equals("**")) {
+ NodeListModel result = new NodeListModel(contextNode);
+ for (int i = 0; i < size(); i++) {
+ NodeModel nm = (NodeModel) get(i);
+ if (nm instanceof ElementModel) {
+ TemplateSequenceModel tsm = (TemplateSequenceModel) nm.get(key);
+ if (tsm != null) {
+ int size = tsm.size();
+ for (int j = 0; j < size; j++) {
+ result.add(tsm.get(j));
+ }
+ }
+ }
+ }
+ if (result.size() == 1) {
+ return result.get(0);
+ }
+ return result;
+ }
+ XPathSupport xps = getXPathSupport();
+ if (xps != null) {
+ Object context = (size() == 0) ? null : rawNodeList();
+ return xps.executeQuery(context, 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);
+ }
+ }
+
+ private List rawNodeList() throws TemplateModelException {
+ int size = size();
+ ArrayList al = new ArrayList(size);
+ for (int i = 0; i < size; i++) {
+ al.add(((NodeModel) get(i)).node);
+ }
+ return al;
+ }
+
+ XPathSupport getXPathSupport() throws TemplateModelException {
+ if (xpathSupport == null) {
+ if (contextNode != null) {
+ xpathSupport = contextNode.getXPathSupport();
+ } else if (size() > 0) {
+ xpathSupport = ((NodeModel) get(0)).getXPathSupport();
+ }
+ }
+ return xpathSupport;
+ }
+
+ @Override
+ public Object[] explainTypeError(Class[] expectedClasses) {
+ for (Class expectedClass : expectedClasses) {
+ if (TemplateScalarModel.class.isAssignableFrom(expectedClass)
+ || TemplateDateModel.class.isAssignableFrom(expectedClass)
+ || TemplateNumberModel.class.isAssignableFrom(expectedClass)
+ || TemplateBooleanModel.class.isAssignableFrom(expectedClass)) {
+ return newTypeErrorExplanation("string");
+ } else if (TemplateNodeModel.class.isAssignableFrom(expectedClass)) {
+ return newTypeErrorExplanation("node");
+ }
+ }
+ return null;
+ }
+
+ private Object[] newTypeErrorExplanation(String type) {
+ return new Object[] {
+ "This XML query result can't be used as ", type, " because for that it had to contain exactly "
+ + "1 XML node, but it contains ", Integer.valueOf(size()), " nodes. "
+ + "That is, the constructing XML query has found ",
+ isEmpty()
+ ? "no matches."
+ : "multiple matches."
+ };
+ }
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/be556897/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeModel.java
----------------------------------------------------------------------
diff --git a/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeModel.java b/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeModel.java
new file mode 100644
index 0000000..37a5c7d
--- /dev/null
+++ b/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeModel.java
@@ -0,0 +1,613 @@
+/*
+ * 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._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
+ * {@linkplain Configuration#getSharedVariables() shared variable}.
+ * <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/be556897/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeOutputter.java
----------------------------------------------------------------------
diff --git a/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeOutputter.java b/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeOutputter.java
new file mode 100644
index 0000000..bda38ac
--- /dev/null
+++ b/freemarker-dom/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/be556897/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeQueryResultItemObjectWrapper.java
----------------------------------------------------------------------
diff --git a/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeQueryResultItemObjectWrapper.java b/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeQueryResultItemObjectWrapper.java
new file mode 100644
index 0000000..e84e977
--- /dev/null
+++ b/freemarker-dom/src/main/java/org/apache/freemarker/dom/NodeQueryResultItemObjectWrapper.java
@@ -0,0 +1,92 @@
+/*
+ * 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.Environment;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelAdapter;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+import org.apache.freemarker.core.model.impl.SimpleDate;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.w3c.dom.Node;
+
+/**
+ * Used for wrapping query result items (such as XPath query result items). Because {@link NodeModel} and such aren't
+ * {@link WrappingTemplateModel}-s, we can't use the actual {@link ObjectWrapper} from the {@link Environment}, also,
+ * even if we could, it might not be the right thing to do, because that {@link ObjectWrapper} might not even wrap
+ * {@link Node}-s via {@link NodeModel}.
+ */
+class NodeQueryResultItemObjectWrapper implements ObjectWrapper {
+
+ static final NodeQueryResultItemObjectWrapper INSTANCE = new NodeQueryResultItemObjectWrapper();
+
+ private NodeQueryResultItemObjectWrapper() {
+ //
+ }
+
+ @Override
+ public TemplateModel wrap(Object obj) throws TemplateModelException {
+ if (obj instanceof NodeModel) {
+ return (NodeModel) obj;
+ }
+ if (obj instanceof Node) {
+ return NodeModel.wrap((Node) obj);
+ } else {
+ if (obj == null) {
+ return null;
+ }
+ if (obj instanceof TemplateModel) {
+ return (TemplateModel) obj;
+ }
+ if (obj instanceof TemplateModelAdapter) {
+ return ((TemplateModelAdapter) obj).getTemplateModel();
+ }
+
+ if (obj instanceof String) {
+ return new SimpleScalar((String) obj);
+ }
+ if (obj instanceof Number) {
+ return new SimpleNumber((Number) obj);
+ }
+ if (obj instanceof Boolean) {
+ return obj.equals(Boolean.TRUE) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+ }
+ if (obj instanceof java.util.Date) {
+ if (obj instanceof java.sql.Date) {
+ return new SimpleDate((java.sql.Date) obj);
+ }
+ if (obj instanceof java.sql.Time) {
+ return new SimpleDate((java.sql.Time) obj);
+ }
+ if (obj instanceof java.sql.Timestamp) {
+ return new SimpleDate((java.sql.Timestamp) obj);
+ }
+ return new SimpleDate((java.util.Date) obj, TemplateDateModel.UNKNOWN);
+ }
+ throw new TemplateModelException("Don't know how to wrap a W3C DOM query result item of this type: "
+ + obj.getClass().getName());
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/be556897/freemarker-dom/src/main/java/org/apache/freemarker/dom/PINodeModel.java
----------------------------------------------------------------------
diff --git a/freemarker-dom/src/main/java/org/apache/freemarker/dom/PINodeModel.java b/freemarker-dom/src/main/java/org/apache/freemarker/dom/PINodeModel.java
new file mode 100644
index 0000000..381d4d6
--- /dev/null
+++ b/freemarker-dom/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/be556897/freemarker-dom/src/main/java/org/apache/freemarker/dom/SunInternalXalanXPathSupport.java
----------------------------------------------------------------------
diff --git a/freemarker-dom/src/main/java/org/apache/freemarker/dom/SunInternalXalanXPathSupport.java b/freemarker-dom/src/main/java/org/apache/freemarker/dom/SunInternalXalanXPathSupport.java
new file mode 100644
index 0000000..991c93f
--- /dev/null
+++ b/freemarker-dom/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/be556897/freemarker-dom/src/main/java/org/apache/freemarker/dom/XPathSupport.java
----------------------------------------------------------------------
diff --git a/freemarker-dom/src/main/java/org/apache/freemarker/dom/XPathSupport.java b/freemarker-dom/src/main/java/org/apache/freemarker/dom/XPathSupport.java
new file mode 100644
index 0000000..e94d391
--- /dev/null
+++ b/freemarker-dom/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/be556897/freemarker-dom/src/main/java/org/apache/freemarker/dom/XalanXPathSupport.java
----------------------------------------------------------------------
diff --git a/freemarker-dom/src/main/java/org/apache/freemarker/dom/XalanXPathSupport.java b/freemarker-dom/src/main/java/org/apache/freemarker/dom/XalanXPathSupport.java
new file mode 100644
index 0000000..99a4249
--- /dev/null
+++ b/freemarker-dom/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