blob: 77b35ab65823724ffafb9f15814475bd03a71a99 [file] [log] [blame]
/*
* 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.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.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;
import freemarker.core.CustomAttribute;
import freemarker.core.Environment;
import freemarker.template.ObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateBooleanModel;
import freemarker.template.TemplateDateModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
import freemarker.template.TemplateScalarModel;
import freemarker.template.utility.UndeclaredThrowableException;
/**
*/
class JaxenXPathSupport implements XPathSupport {
private static final CustomAttribute XPATH_CACHE_ATTR =
new CustomAttribute(CustomAttribute.SCOPE_TEMPLATE) {
@Override
protected Object 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 = (Map<String, BaseXPath>) XPATH_CACHE_ATTR.get();
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) {
// [2.4] Use the proper object wrapper (argument in 2.4)
return ObjectWrapper.DEFAULT_WRAPPER.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 CustomAttribute FM_DOM_NAVIAGOTOR_CACHED_DOM
= new CustomAttribute(CustomAttribute.SCOPE_TEMPLATE);
private static final Navigator FM_DOM_NAVIGATOR = new DocumentNavigator() {
@Override
public Object getDocument(String uri) throws FunctionCallException {
try {
Template raw = getTemplate(uri);
Document doc = (Document) FM_DOM_NAVIAGOTOR_CACHED_DOM.get(raw);
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) {
FM_DOM_NAVIAGOTOR_CACHED_DOM.set(doc, raw);
}
}
return doc;
} catch (Exception e) {
throw new FunctionCallException("Failed to parse document for URI: " + uri, e);
}
}
};
static Template getTemplate(String systemId) throws IOException {
Environment env = Environment.getCurrentEnvironment();
String encoding = env.getTemplate().getEncoding();
if (encoding == null) {
encoding = env.getConfiguration().getEncoding(env.getLocale());
}
String templatePath = env.getTemplate().getName();
int lastSlash = templatePath.lastIndexOf('/');
templatePath = lastSlash == -1 ? "" : templatePath.substring(0, lastSlash + 1);
systemId = env.toFullTemplateName(templatePath, systemId);
Template raw = env.getConfiguration().getTemplate(systemId, env.getLocale(), encoding, false);
return raw;
}
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.getName());
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;
}
};
}