/*
* 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 freemarker.ext.dom;
import java.io.
File;
import java.io.
IOException;
import java.io.
InputStream;
import java.lang.ref.
WeakReference;
import java.net.
MalformedURLException;
import java.util.
Collections;
import java.util.
List;
import java.util.
Map;
import java.util.
WeakHashMap;
import javax.xml.parsers.
DocumentBuilder;
import javax.xml.parsers.
DocumentBuilderFactory;
import javax.xml.parsers.
ParserConfigurationException;
import org.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;
import org.xml.sax.
ErrorHandler;
import org.xml.sax.
InputSource;
import org.xml.sax.
SAXException;
import freemarker.core.
_UnexpectedTypeErrorExplainerTemplateModel;
import freemarker.ext.util.
WrapperTemplateModel;
import freemarker.log.
Logger;
import freemarker.template.
AdapterTemplateModel;
import freemarker.template.
Configuration;
import freemarker.template.
DefaultObjectWrapper;
import freemarker.template.
SimpleScalar;
import freemarker.template.
TemplateBooleanModel;
import freemarker.template.
TemplateDateModel;
import freemarker.template.
TemplateHashModel;
import freemarker.template.
TemplateModel;
import freemarker.template.
TemplateModelException;
import freemarker.template.
TemplateNodeModel;
import freemarker.template.
TemplateNodeModelEx;
import freemarker.template.
TemplateNumberModel;
import freemarker.template.
TemplateSequenceModel;
/**
* A base class for wrapping a single W3C DOM Node as a FreeMarker template model.
*
* <p>
* Note that {@link DefaultObjectWrapper} automatically wraps W3C DOM {@link Node}-s into this, so you may need do that
* with this class manually. However, before dropping the {@link Node}-s into the data-model, you certainly want to
* apply {@link NodeModel#simplify(Node)} on them.
*
* <p>
* This class is not guaranteed to be thread safe, so instances of this shouldn't be used as shared variable (
* {@link Configuration#setSharedVariable(String, Object)}).
*
* <p>
* To represent a node sequence (such as a query result) of exactly 1 nodes, this class should be used instead of
* {@link NodeListModel}, as it adds extra capabilities by utilizing that we have exactly 1 node. If you need to wrap a
* node sequence of 0 or multiple nodes, you must use {@link NodeListModel}.
*/
abstract public class
NodeModel
implements
TemplateNodeModelEx,
TemplateHashModel,
TemplateSequenceModel,
AdapterTemplateModel,
WrapperTemplateModel,
_UnexpectedTypeErrorExplainerTemplateModel {
static private final
Logger LOG =
Logger.
getLogger("freemarker.dom");
private static final
Object STATIC_LOCK = new
Object();
static private
DocumentBuilderFactory docBuilderFactory;
static private final
Map xpathSupportMap =
Collections.
synchronizedMap(new
WeakHashMap());
static private
XPathSupport jaxenXPathSupport;
static private
ErrorHandler errorHandler;
static
Class xpathSupportClass;
static {
try {
useDefaultXPathSupport();
} catch (
Exception e) {
// do nothing
}
if (
xpathSupportClass == null &&
LOG.
isWarnEnabled()) {
LOG.
warn("No XPath support is available.");
}
}
/**
* The W3C DOM Node being wrapped.
*/
final
Node node;
private
TemplateSequenceModel children;
private
NodeModel parent;
/**
* Sets the DOM parser implementation to be used when building {@link NodeModel} objects from XML files or from
* {@link InputStream} with the static convenience methods of {@link NodeModel}. Otherwise FreeMarker itself doesn't
* use this.
*
* @see #getDocumentBuilderFactory()
*
* @deprecated It's a bad practice to change static fields, as if multiple independent components do that in the
* same JVM, they unintentionally affect each other. Therefore it's recommended to leave this static
* value at its default.
*/
@
Deprecated
static public void
setDocumentBuilderFactory(
DocumentBuilderFactory docBuilderFactory) {
synchronized (
STATIC_LOCK) {
NodeModel.
docBuilderFactory =
docBuilderFactory;
}
}
/**
* Returns the DOM parser implementation that is used when building {@link NodeModel} objects from XML files or from
* {@link InputStream} with the static convenience methods of {@link NodeModel}. Otherwise FreeMarker itself doesn't
* use this.
*
* @see #setDocumentBuilderFactory(DocumentBuilderFactory)
*/
static public
DocumentBuilderFactory getDocumentBuilderFactory() {
synchronized (
STATIC_LOCK) {
if (
docBuilderFactory == null) {
DocumentBuilderFactory newFactory =
DocumentBuilderFactory.
newInstance();
newFactory.
setNamespaceAware(true);
newFactory.
setIgnoringElementContentWhitespace(true);
docBuilderFactory =
newFactory; // We only write it out when the initialization was full
}
return
docBuilderFactory;
}
}
/**
* Sets the error handler to use when parsing the document with the static convenience methods of {@link NodeModel}.
*
* @deprecated It's a bad practice to change static fields, as if multiple independent components do that in the
* same JVM, they unintentionally affect each other. Therefore it's recommended to leave this static
* value at its default.
*
* @see #getErrorHandler()
*/
@
Deprecated
static public void
setErrorHandler(
ErrorHandler errorHandler) {
synchronized (
STATIC_LOCK) {
NodeModel.
errorHandler =
errorHandler;
}
}
/**
* @since 2.3.20
*
* @see #setErrorHandler(ErrorHandler)
*/
static public
ErrorHandler getErrorHandler() {
synchronized (
STATIC_LOCK) {
return
NodeModel.
errorHandler;
}
}
/**
* Convenience method to create a {@link NodeModel} from a SAX {@link InputSource}; please see the security warning
* further down. Adjacent text nodes will be merged (and CDATA sections are considered as text nodes) as with
* {@link #mergeAdjacentText(Node)}. Further simplifications are applied depending on the parameters. If all
* simplifications are turned on, then it applies {@link #simplify(Node)} on the loaded DOM.
*
* <p>
* Note that {@code parse(...)} is only a convenience method, and FreeMarker itself doesn't use it (except when you
* call the other similar static convenience methods, as they may build on each other). In particular, if you want
* full control over the {@link DocumentBuilderFactory} used, create the {@link Node} with your own
* {@link DocumentBuilderFactory}, apply {@link #simplify(Node)} (or such) on it, then call
* {@link NodeModel#wrap(Node)}.
*
* <p>
* <b>Security warning:</b> If the XML to load is coming from a source that you can't fully trust, you shouldn't use
* this method, as the {@link DocumentBuilderFactory} it uses by default supports external entities, and so it
* doesn't prevent XML External Entity (XXE) attacks. Note that XXE attacks are not specific to FreeMarker, they
* affect all XML parsers in general. If that's a problem in your application, OWASP has a cheat sheet to set up a
* {@link DocumentBuilderFactory} that has limited functionality but is immune to XXE attacks. Because it's just a
* convenience method, you can just use your own {@link DocumentBuilderFactory} and do a few extra steps instead
* (see earlier).
*
* @param removeComments
* Whether to remove all comment nodes (recursively); this is like calling {@link #removeComments(Node)}
* @param removePIs
* Whether to remove all processing instruction nodes (recursively); this is like calling
* {@link #removePIs(Node)}
*/
static public
NodeModel parse(
InputSource is, boolean
removeComments, boolean
removePIs)
throws
SAXException,
IOException,
ParserConfigurationException {
DocumentBuilder builder =
getDocumentBuilderFactory().
newDocumentBuilder();
ErrorHandler errorHandler =
getErrorHandler();
if (
errorHandler != null)
builder.
setErrorHandler(
errorHandler);
final
Document doc;
try {
doc =
builder.
parse(
is);
} catch (
MalformedURLException e) {
// This typical error has an error message that is hard to understand, so let's translate it:
if (
is.
getSystemId() == null &&
is.
getCharacterStream() == null &&
is.
getByteStream() == null) {
throw new
MalformedURLException(
"The SAX InputSource has systemId == null && characterStream == null && byteStream == null. "
+ "This is often because it was created with a null InputStream or Reader, which is often because "
+ "the XML file it should point to was not found. "
+ "(The original exception was: " +
e + ")");
} else {
throw
e;
}
}
if (
removeComments &&
removePIs) {
simplify(
doc);
} else {
if (
removeComments) {
removeComments(
doc);
}
if (
removePIs) {
removePIs(
doc);
}
mergeAdjacentText(
doc);
}
return
wrap(
doc);
}
/**
* Same as {@link #parse(InputSource, boolean, boolean) parse(is, true, true)}; don't miss the security warnings
* documented there.
*/
static public
NodeModel parse(
InputSource is) throws
SAXException,
IOException,
ParserConfigurationException {
return
parse(
is, true, true);
}
/**
* Same as {@link #parse(InputSource, boolean, boolean)}, but loads from a {@link File}; don't miss the security
* warnings documented there.
*/
static public
NodeModel parse(
File f, boolean
removeComments, boolean
removePIs)
throws
SAXException,
IOException,
ParserConfigurationException {
DocumentBuilder builder =
getDocumentBuilderFactory().
newDocumentBuilder();
ErrorHandler errorHandler =
getErrorHandler();
if (
errorHandler != null)
builder.
setErrorHandler(
errorHandler);
Document doc =
builder.
parse(
f);
if (
removeComments &&
removePIs) {
simplify(
doc);
} else {
if (
removeComments) {
removeComments(
doc);
}
if (
removePIs) {
removePIs(
doc);
}
mergeAdjacentText(
doc);
}
return
wrap(
doc);
}
/**
* Same as {@link #parse(InputSource, boolean, boolean) parse(source, true, true)}, but loads from a {@link File};
* don't miss the security warnings documented there.
*/
static public
NodeModel parse(
File f) throws
SAXException,
IOException,
ParserConfigurationException {
return
parse(
f, true, true);
}
protected
NodeModel(
Node node) {
this.
node =
node;
}
/**
* @return the underling W3C DOM Node object that this TemplateNodeModel
* is wrapping.
*/
public
Node getNode() {
return
node;
}
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);
}
}
}
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;
}
public
TemplateNodeModelEx getPreviousSibling() throws
TemplateModelException {
return
wrap(
node.
getPreviousSibling());
}
public
TemplateNodeModelEx getNextSibling() throws
TemplateModelException {
return
wrap(
node.
getNextSibling());
}
public
TemplateSequenceModel getChildNodes() {
if (
children == null) {
children = new
NodeListModel(
node.
getChildNodes(), this);
}
return
children;
}
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.
*/
public final int
size() {
return 1;
}
public final
TemplateModel get(int
i) {
return
i == 0 ? this : null;
}
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() == this.
getClass()
&& ((
NodeModel)
other).
node.
equals(this.
node);
}
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;
}
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 (
ClassNotFoundException e) {
// Expected
} catch (
Exception e) {
LOG.
debug("Failed to use Xalan XPath support.",
e);
} catch (
IllegalAccessError e) {
LOG.
debug("Failed to use Xalan internal XPath support.",
e);
}
if (
xpathSupportClass == null) try {
useSunInternalXPathSupport();
} catch (
Exception e) {
LOG.
debug("Failed to use Sun internal XPath support.",
e);
} catch (
IllegalAccessError e) {
// Happens on OpenJDK 9 (but not on Oracle Java 9)
LOG.
debug("Failed to use Sun internal XPath support. "
+ "Tip: On Java 9+, you may need Xalan or Jaxen+Saxpath.",
e);
}
if (
xpathSupportClass == null) try {
useJaxenXPathSupport();
} catch (
ClassNotFoundException e) {
// Expected
} catch (
Exception e) {
LOG.
debug("Failed to use Jaxen XPath support.",
e);
} catch (
IllegalAccessError e) {
LOG.
debug("Failed to use Jaxen XPath support.",
e);
}
}
}
/**
* 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("freemarker.ext.dom.JaxenXPathSupport");
jaxenXPathSupport = (
XPathSupport)
c.
newInstance();
synchronized (
STATIC_LOCK) {
xpathSupportClass =
c;
}
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("freemarker.ext.dom.XalanXPathSupport");
synchronized (
STATIC_LOCK) {
xpathSupportClass =
c;
}
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("freemarker.ext.dom.SunInternalXalanXPathSupport");
synchronized (
STATIC_LOCK) {
xpathSupportClass =
c;
}
LOG.
debug("Using Sun's internal Xalan classes for XPath support");
}
/**
* Set an alternative implementation of freemarker.ext.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 freemarker.ext.dom.XPathSupport");
}
synchronized (
STATIC_LOCK) {
xpathSupportClass =
cl;
}
}
/**
* Get the currently used freemarker.ext.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();
}
public
Object getAdaptedObject(
Class hint) {
return
node;
}
public
Object getWrappedObject() {
return
node;
}
public
Object[]
explainTypeError(
Class[]
expectedClasses) {
for (int
i = 0;
i <
expectedClasses.length;
i++) {
Class expectedClass =
expectedClasses[
i];
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;
}
}