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
