blob: 0be4d9d7c4ba6feeb2b97d84810da311ce32b098 [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.jsp;
import java.beans.IntrospectionException;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.tagext.Tag;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import freemarker.core.BugException;
import freemarker.core.Environment;
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.servlet.FreemarkerServlet;
import freemarker.ext.servlet.HttpRequestHashModel;
import freemarker.log.Logger;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.ObjectWrapper;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateTransformModel;
import freemarker.template.utility.ClassUtil;
import freemarker.template.utility.NullArgumentException;
import freemarker.template.utility.SecurityUtilities;
import freemarker.template.utility.StringUtil;
/**
* A hash model associated with a servlet context that can load JSP tag libraries associated with that servlet context.
* An instance of this class is made available in the root data model of templates executed by
* {@link freemarker.ext.servlet.FreemarkerServlet} under key {@code JspTaglibs}. It can be added to custom servlets as
* well to enable JSP taglib integration in them as well.
*/
public class TaglibFactory implements TemplateHashModel {
/**
* The default of {@link #getClasspathTlds()}; an empty list.
*
* @since 2.3.22
*/
public static final List DEFAULT_CLASSPATH_TLDS = Collections.EMPTY_LIST;
/**
* The default of {@link #getMetaInfTldSources()}; a list that contains
* {@link WebInfPerLibJarMetaInfTldSource#INSTANCE}, which gives the behavior described in the JSP 2.2
* specification.
*
* @since 2.3.22
*/
public static final List/*<? extends MetaInfTldSource>*/ DEFAULT_META_INF_TLD_SOURCES
= Collections.singletonList(WebInfPerLibJarMetaInfTldSource.INSTANCE);
private static final Logger LOG = Logger.getLogger("freemarker.jsp");
private static final int URL_TYPE_FULL = 0;
private static final int URL_TYPE_ABSOLUTE = 1;
private static final int URL_TYPE_RELATIVE = 2;
private static final String META_INF_REL_PATH = "META-INF/";
private static final String META_INF_ABS_PATH = "/META-INF/";
private static final String DEFAULT_TLD_RESOURCE_PATH = META_INF_ABS_PATH + "taglib.tld";
private static final String JAR_URL_ENTRY_PATH_START = "!/";
private static final String PLATFORM_FILE_ENCODING = SecurityUtilities.getSystemProperty("file.encoding", "utf-8");
private final ServletContext servletContext;
private ObjectWrapper objectWrapper;
private List/*<MetaInfTldSource>*/ metaInfTldSources = DEFAULT_META_INF_TLD_SOURCES;
private List/*<String>*/ classpathTlds = DEFAULT_CLASSPATH_TLDS;
boolean test_emulateNoUrlToFileConversions = false;
boolean test_emulateNoJarURLConnections = false;
boolean test_emulateJarEntryUrlOpenStreamFails = false;
private final Object lock = new Object();
private final Map taglibs = new HashMap();
private final Map tldLocations = new HashMap();
private List/*<String>*/ failedTldLocations = new ArrayList();
private int nextTldLocationLookupPhase = 0;
/**
/**
* Creates a new JSP taglib factory that will be used to load JSP tag libraries and functions for the web
* application represented by the passed in {@link ServletContext}.
* You should at least call {@link #setObjectWrapper(ObjectWrapper)} before start using this object.
*
* <p>This object is only thread-safe after you have stopped calling its setter methods (and it was properly
* published to the other threads; see JSR 133 (Java Memory Model)).
*
* @param ctx
* The servlet context whose JSP tag libraries this factory will load.
*/
public TaglibFactory(ServletContext ctx) {
this.servletContext = ctx;
}
/**
* Retrieves a JSP tag library identified by an URI. The matching of the URI to a JSP taglib is done as described in
* the JSP 1.2 FCS specification.
*
* @param taglibUri
* The URI used in templates to refer to the taglib (like {@code <%@ taglib uri="..." ... %>} in
* JSP). It can be any of the three forms allowed by the JSP specification: absolute URI (like
* {@code http://example.com/foo}), root relative URI (like {@code /bar/foo.tld}) and non-root relative
* URI (like {@code bar/foo.tld}). Note that if a non-root relative URI is used it's resolved relative to
* the URL of the current request. In this case, the current request is obtained by looking up a
* {@link HttpRequestHashModel} object named <tt>Request</tt> in the root data model.
* {@link FreemarkerServlet} provides this object under the expected name, and custom servlets that want
* to integrate JSP taglib support should do the same.
*
* @return a {@link TemplateHashModel} representing the JSP taglib. Each element of this hash represents a single
* custom tag or EL function from the library, implemented as a {@link TemplateTransformModel} or
* {@link TemplateMethodModelEx}, respectively.
*/
public TemplateModel get(final String taglibUri) throws TemplateModelException {
synchronized (lock) {
{
final Taglib taglib = (Taglib) taglibs.get(taglibUri);
if (taglib != null) {
return taglib;
}
}
boolean failedTldListAlreadyIncluded = false;
final TldLocation tldLocation;
final String normalizedTaglibUri;
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Locating TLD for taglib URI " + StringUtil.jQuoteNoXSS(taglibUri) + ".");
}
TldLocation explicitlyMappedTldLocation = getExplicitlyMappedTldLocation(taglibUri);
if (explicitlyMappedTldLocation != null) {
tldLocation = explicitlyMappedTldLocation;
normalizedTaglibUri = taglibUri;
} else {
// Taglib URI must be directly the path (no mapping).
final int urlType;
try {
urlType = getUriType(taglibUri);
} catch (MalformedURLException e) {
throw new TaglibGettingException("Malformed taglib URI: " + StringUtil.jQuote(taglibUri), e);
}
if (urlType == URL_TYPE_RELATIVE) {
normalizedTaglibUri = resolveRelativeUri(taglibUri);
} else if (urlType == URL_TYPE_ABSOLUTE) {
normalizedTaglibUri = taglibUri;
} else if (urlType == URL_TYPE_FULL) {
// Per spec., full URI-s can only be resolved through explicit mapping
String failedTLDsList = getFailedTLDsList();
failedTldListAlreadyIncluded = true;
throw new TaglibGettingException("No TLD was found for the "
+ StringUtil.jQuoteNoXSS(taglibUri) + " JSP taglib URI. (TLD-s are searched according "
+ "the JSP 2.2 specification. In development- and embedded-servlet-container "
+ "setups you may also need the "
+ "\"" + FreemarkerServlet.INIT_PARAM_META_INF_TLD_LOCATIONS + "\" and "
+ "\"" + FreemarkerServlet.INIT_PARAM_CLASSPATH_TLDS + "\" "
+ FreemarkerServlet.class.getName() + " init-params or the similar system "
+ "properites."
+ (failedTLDsList == null
? ""
: " Also note these TLD-s were skipped earlier due to errors; "
+ "see error in the log: " + failedTLDsList
) + ")");
} else {
throw new BugException();
}
if (!normalizedTaglibUri.equals(taglibUri)) {
final Taglib taglib = (Taglib) taglibs.get(normalizedTaglibUri);
if (taglib != null) {
return taglib;
}
}
tldLocation = isJarPath(normalizedTaglibUri)
? (TldLocation) new ServletContextJarEntryTldLocation(
normalizedTaglibUri, DEFAULT_TLD_RESOURCE_PATH)
: (TldLocation) new ServletContextTldLocation(normalizedTaglibUri);
}
} catch (Exception e) {
String failedTLDsList = failedTldListAlreadyIncluded ? null : getFailedTLDsList();
throw new TemplateModelException(
"Error while looking for TLD file for " + StringUtil.jQuoteNoXSS(taglibUri)
+ "; see cause exception."
+ (failedTLDsList == null
? ""
: " (Note: These TLD-s were skipped earlier due to errors; "
+ "see errors in the log: " + failedTLDsList + ")"),
e);
}
try {
return loadTaglib(tldLocation, normalizedTaglibUri);
} catch (Exception e) {
throw new TemplateModelException("Error while loading tag library for URI "
+ StringUtil.jQuoteNoXSS(normalizedTaglibUri) + " from TLD location "
+ StringUtil.jQuoteNoXSS(tldLocation) + "; see cause exception.",
e);
}
}
}
/**
* Returns the joined list of failed TLD-s, or {@code null} if there was none.
*/
private String getFailedTLDsList() {
synchronized (failedTldLocations) {
if (failedTldLocations.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < failedTldLocations.size(); i++) {
if (i != 0) {
sb.append(", ");
}
sb.append(StringUtil.jQuote(failedTldLocations.get(i)));
}
return sb.toString();
}
}
/**
* Returns false.
*/
public boolean isEmpty() {
return false;
}
/**
* See {@link #setObjectWrapper(ObjectWrapper)}.
*
* @since 2.3.22
*/
public ObjectWrapper getObjectWrapper() {
return objectWrapper;
}
/**
* Sets the {@link ObjectWrapper} used when building the JSP tag library {@link TemplateHashModel}-s from the TLD-s.
* Usually, it should be the same {@link ObjectWrapper} that will be used inside the templates. {@code null} value
* is only supported for backward compatibility. For custom EL functions to be exposed, it must be non-{@code null}
* and an {@code intanceof} {@link BeansWrapper} (like typically, a {@link DefaultObjectWrapper}).
*
* @since 2.3.22
*/
public void setObjectWrapper(ObjectWrapper objectWrapper) {
checkNotStarted();
this.objectWrapper = objectWrapper;
}
/**
* See {@link #setMetaInfTldSources(List)}.
*
* @since 2.3.22
*/
public List/*<Pattern>*/ getMetaInfTldSources() {
return metaInfTldSources;
}
/**
* Sets the list of places where we will look for {@code META-INF/**}{@code /*.tld} files. By default this is a list
* that only contains {@link WebInfPerLibJarMetaInfTldSource#INSTANCE}. This corresponds to the behavior that the
* JSP specification describes. See the {@link MetaInfTldSource} subclasses for the possible values and their
* meanings.
*
* <p>
* This is usually set via the init-params of {@link FreemarkerServlet}.
*
* @param metaInfTldSources
* The list of {@link MetaInfTldSource} subclass instances. Their order matters if multiple TLD-s define
* a taglib with the same {@code taglib-uri}. In that case, the one found by the earlier
* {@link MetaInfTldSource} wins.
*
* @see #setClasspathTlds(List)
*
* @since 2.3.22
*/
public void setMetaInfTldSources(List/*<? extends MetaInfTldSource>*/ metaInfTldSources) {
checkNotStarted();
NullArgumentException.check("metaInfTldSources", metaInfTldSources);
this.metaInfTldSources = metaInfTldSources;
}
/**
* See {@link #setClasspathTlds(List)}.
*
* @since 2.3.22
*/
public List/*<String>*/ getClasspathTlds() {
return classpathTlds;
}
/**
* Sets the class-loader resource paths of the TLD-s that aren't inside the locations covered by
* {@link #setMetaInfTldSources(List)}, yet you want them to be discovered. They will be loaded with the class
* loader provided by the servlet container.
*
* <p>
* This is usually set via the init-params of {@link FreemarkerServlet}.
*
* @param classpathTlds
* List of {@code String}-s, maybe {@code null}. Each item is a resource path, like
* {@code "/META-INF/my.tld"}. (Relative resource paths will be interpreted as root-relative.)
*
* @see #setMetaInfTldSources(List)
*
* @since 2.3.22
*/
public void setClasspathTlds(List/*<String>*/ classpathTlds) {
checkNotStarted();
NullArgumentException.check("classpathTlds", classpathTlds);
this.classpathTlds = classpathTlds;
}
private void checkNotStarted() {
synchronized (lock) {
if (nextTldLocationLookupPhase != 0) {
throw new IllegalStateException(TaglibFactory.class.getName() + " object was already in use.");
}
}
}
private TldLocation getExplicitlyMappedTldLocation(final String uri) throws SAXException, IOException,
TaglibGettingException {
while (true) {
final TldLocation tldLocation = (TldLocation) tldLocations.get(uri);
if (tldLocation != null) {
return tldLocation;
}
switch (nextTldLocationLookupPhase) {
case 0:
// Not in JSP spec.
addTldLocationsFromClasspathTlds();
break;
case 1:
// JSP 2.2 spec / JSP.7.3.3 (also JSP.3.2)
addTldLocationsFromWebXml();
break;
case 2:
// JSP 2.2 spec / JSP.7.3.4, FM-specific TLD processing order #1
addTldLocationsFromWebInfTlds();
break;
case 3:
// JSP 2.2 spec / JSP.7.3.4, FM-specific TLD processing order #2
addTldLocationsFromMetaInfTlds();
break;
case 4:
return null;
default:
throw new BugException();
}
nextTldLocationLookupPhase++;
}
}
private void addTldLocationsFromWebXml() throws SAXException, IOException {
LOG.debug("Looking for TLD locations in servletContext:/WEB-INF/web.xml");
WebXmlParser webXmlParser = new WebXmlParser();
InputStream in = servletContext.getResourceAsStream("/WEB-INF/web.xml");
if (in == null) {
LOG.debug("No web.xml was found in servlet context");
return;
}
try {
parseXml(in, servletContext.getResource("/WEB-INF/web.xml").toExternalForm(), webXmlParser);
} finally {
in.close();
}
}
private void addTldLocationsFromWebInfTlds()
throws IOException, SAXException {
LOG.debug("Looking for TLD locations in servletContext:/WEB-INF/**/*.tld");
addTldLocationsFromServletContextResourceTlds("/WEB-INF");
}
private void addTldLocationsFromServletContextResourceTlds(String basePath)
throws IOException, SAXException {
Set unsortedResourcePaths = servletContext.getResourcePaths(basePath);
if (unsortedResourcePaths != null) {
List/*<String>*/ resourcePaths = new ArrayList/*<String>*/(unsortedResourcePaths);
Collections.sort(resourcePaths);
// First process the files...
for (Iterator it = resourcePaths.iterator(); it.hasNext(); ) {
String resourcePath = (String) it.next();
if (resourcePath.endsWith(".tld")) {
addTldLocationFromTld(new ServletContextTldLocation(resourcePath));
}
}
// ... only later the directories
for (Iterator it = resourcePaths.iterator(); it.hasNext(); ) {
String resourcePath = (String) it.next();
if (resourcePath.endsWith("/")) {
addTldLocationsFromServletContextResourceTlds(resourcePath);
}
}
}
}
private void addTldLocationsFromMetaInfTlds() throws IOException, SAXException {
if (metaInfTldSources == null || metaInfTldSources.isEmpty()) {
return;
}
Set/*<URLWithExternalForm>*/ cpMetaInfDirUrlsWithEF = null;
// Skip past the last "clear":
int srcIdxStart = 0;
for (int i = metaInfTldSources.size() - 1; i >= 0; i--) {
if (metaInfTldSources.get(i) instanceof ClearMetaInfTldSource) {
srcIdxStart = i + 1;
break;
}
}
for (int srcIdx = srcIdxStart; srcIdx < metaInfTldSources.size(); srcIdx++) {
MetaInfTldSource miTldSource = (MetaInfTldSource) metaInfTldSources.get(srcIdx);
if (miTldSource == WebInfPerLibJarMetaInfTldSource.INSTANCE) {
addTldLocationsFromWebInfPerLibJarMetaInfTlds();
} else if (miTldSource instanceof ClasspathMetaInfTldSource) {
ClasspathMetaInfTldSource cpMiTldLocation = (ClasspathMetaInfTldSource) miTldSource;
if (LOG.isDebugEnabled()) {
LOG.debug("Looking for TLD-s in "
+ "classpathRoots[" + cpMiTldLocation.getRootContainerPattern() + "]"
+ META_INF_ABS_PATH + "**/*.tld");
}
if (cpMetaInfDirUrlsWithEF == null) {
cpMetaInfDirUrlsWithEF = collectMetaInfUrlsFromClassLoaders();
}
for (Iterator iterator = cpMetaInfDirUrlsWithEF.iterator(); iterator.hasNext(); ) {
URLWithExternalForm urlWithEF = (URLWithExternalForm) iterator.next();
final URL url = urlWithEF.getUrl();
final boolean isJarUrl = isJarUrl(url);
final String urlEF = urlWithEF.externalForm;
final String rootContainerUrl;
if (isJarUrl) {
int sep = urlEF.indexOf(JAR_URL_ENTRY_PATH_START);
rootContainerUrl = sep != -1 ? urlEF.substring(0, sep) : urlEF;
} else {
rootContainerUrl = urlEF.endsWith(META_INF_ABS_PATH)
? urlEF.substring(0, urlEF.length() - META_INF_REL_PATH.length())
: urlEF;
}
if (cpMiTldLocation.getRootContainerPattern().matcher(rootContainerUrl).matches()) {
final File urlAsFile = urlToFileOrNull(url);
if (urlAsFile != null) {
addTldLocationsFromFileDirectory(urlAsFile);
} else if (isJarUrl) {
addTldLocationsFromJarDirectoryEntryURL(url);
} else {
if (LOG.isDebugEnabled()) {
LOG.warn("Can't list entries under this URL; TLD-s won't be discovered here: "
+ urlWithEF.getExternalForm());
}
}
}
}
} else {
throw new BugException();
}
}
}
private void addTldLocationsFromWebInfPerLibJarMetaInfTlds() throws IOException, SAXException {
if (LOG.isDebugEnabled()) {
LOG.debug("Looking for TLD locations in servletContext:/WEB-INF/lib/*.{jar,zip}" + META_INF_ABS_PATH
+ "*.tld");
}
Set libEntPaths = servletContext.getResourcePaths("/WEB-INF/lib");
if (libEntPaths != null) {
for (Iterator iter = libEntPaths.iterator(); iter.hasNext(); ) {
final String libEntryPath = (String) iter.next();
if (isJarPath(libEntryPath)) {
addTldLocationsFromServletContextJar(libEntryPath);
}
}
}
}
private void addTldLocationsFromClasspathTlds() throws SAXException, IOException, TaglibGettingException {
if (classpathTlds == null || classpathTlds.size() == 0) {
return;
}
LOG.debug("Looking for TLD locations in TLD-s specified in cfg.classpathTlds");
for (Iterator it = classpathTlds.iterator(); it.hasNext(); ) {
String tldResourcePath = (String) it.next();
if (tldResourcePath.trim().length() == 0) {
throw new TaglibGettingException("classpathTlds can't contain empty item");
}
if (!tldResourcePath.startsWith("/")) {
tldResourcePath = "/" + tldResourcePath;
}
if (tldResourcePath.endsWith("/")) {
throw new TaglibGettingException("classpathTlds can't specify a directory: " + tldResourcePath);
}
ClasspathTldLocation tldLocation = new ClasspathTldLocation(tldResourcePath);
InputStream in;
try {
in = tldLocation.getInputStream();
} catch (IOException e) {
if (LOG.isWarnEnabled()) {
LOG.warn("Ignored classpath TLD location " + StringUtil.jQuoteNoXSS(tldResourcePath)
+ " because of error", e);
}
in = null;
}
if (in != null) {
try {
addTldLocationFromTld(in, tldLocation);
} finally {
in.close();
}
}
}
}
/**
* Finds and processes *.tld inside a jar in the servet context.
*/
private void addTldLocationsFromServletContextJar(
final String jarResourcePath)
throws IOException, MalformedURLException, SAXException {
final String metaInfEntryPath = normalizeJarEntryPath(META_INF_ABS_PATH, true);
// Null for non-random-access backing resource:
final JarFile jarFile = servletContextResourceToFileOrNull(jarResourcePath);
if (jarFile != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Scanning for " + META_INF_ABS_PATH + "*.tld-s in JarFile: servletContext:"
+ jarResourcePath);
}
for (Enumeration/*<JarEntry>*/ entries = jarFile.entries(); entries.hasMoreElements(); ) {
final JarEntry curEntry = (JarEntry) entries.nextElement();
final String curEntryPath = normalizeJarEntryPath(curEntry.getName(), false);
if (curEntryPath.startsWith(metaInfEntryPath) && curEntryPath.endsWith(".tld")) {
addTldLocationFromTld(new ServletContextJarEntryTldLocation(jarResourcePath, curEntryPath));
}
}
} else { // jarFile == null => fall back to streamed access
if (LOG.isDebugEnabled()) {
LOG.debug("Scanning for " + META_INF_ABS_PATH
+ "*.tld-s in ZipInputStream (slow): servletContext:" + jarResourcePath);
}
final InputStream in = servletContext.getResourceAsStream(jarResourcePath);
if (in == null) {
throw new IOException("ServletContext resource not found: " + jarResourcePath);
}
try {
ZipInputStream zipIn = new ZipInputStream(in);
try {
while (true) {
ZipEntry curEntry = zipIn.getNextEntry();
if (curEntry == null) break;
String curEntryPath = normalizeJarEntryPath(curEntry.getName(), false);
if (curEntryPath.startsWith(metaInfEntryPath) && curEntryPath.endsWith(".tld")) {
addTldLocationFromTld(zipIn,
new ServletContextJarEntryTldLocation(jarResourcePath, curEntryPath));
}
}
} finally {
zipIn.close();
}
} finally {
in.close();
}
}
}
/**
* Finds and processes *.tld inside a directory in a jar.
*
* @param jarBaseEntryUrl
* Something like "jar:file:/C:/foo%20bar/baaz.jar!/META-INF/". If this is not a jar(-like) URL, the
* behavior is undefined.
*/
private void addTldLocationsFromJarDirectoryEntryURL(final URL jarBaseEntryUrl)
throws IOException, MalformedURLException, SAXException {
// Null for non-random-access backing resource:
final JarFile jarFile;
// Not null; the path of the directory *inside* the JAR where we will search
// (like "/META-INF/" in "jar:file:/C:/foo%20bar/baaz.jar!/META-INF/"):
final String baseEntryPath;
// Null when URLConnection is used
// (like "file:/C:/foo%20bar/baaz.jar" in "jar:file:/C:/foo%20bar/baaz.jar!/META-INF/"):
final String rawJarContentUrlEF;
{
final URLConnection urlCon = jarBaseEntryUrl.openConnection();
if (!test_emulateNoJarURLConnections && urlCon instanceof JarURLConnection) {
final JarURLConnection jarCon = (JarURLConnection) urlCon;
jarFile = jarCon.getJarFile();
rawJarContentUrlEF = null; // Not used as we have a JarURLConnection
baseEntryPath = normalizeJarEntryPath(jarCon.getEntryName(), true);
if (baseEntryPath == null) {
throw newFailedToExtractEntryPathException(jarBaseEntryUrl);
}
} else {
final String jarBaseEntryUrlEF = jarBaseEntryUrl.toExternalForm();
final int jarEntrySepIdx = jarBaseEntryUrlEF.indexOf(JAR_URL_ENTRY_PATH_START);
if (jarEntrySepIdx == -1) {
throw newFailedToExtractEntryPathException(jarBaseEntryUrl);
}
rawJarContentUrlEF = jarBaseEntryUrlEF.substring(jarBaseEntryUrlEF.indexOf(':') + 1, jarEntrySepIdx);
baseEntryPath = normalizeJarEntryPath(
jarBaseEntryUrlEF.substring(jarEntrySepIdx + JAR_URL_ENTRY_PATH_START.length()), true);
File rawJarContentAsFile = urlToFileOrNull(new URL(rawJarContentUrlEF));
jarFile = rawJarContentAsFile != null ? new JarFile(rawJarContentAsFile) : null;
}
}
if (jarFile != null) { // jarFile == null => fall back to streamed access
if (LOG.isDebugEnabled()) {
LOG.debug("Scanning for " + META_INF_ABS_PATH + "**/*.tld-s in random access mode: "
+ jarBaseEntryUrl);
}
for (Enumeration/*<JarEntry>*/ entries = jarFile.entries(); entries.hasMoreElements(); ) {
final JarEntry curEntry = (JarEntry) entries.nextElement();
final String curEntryPath = normalizeJarEntryPath(curEntry.getName(), false);
if (curEntryPath.startsWith(baseEntryPath) && curEntryPath.endsWith(".tld")) {
final String curEntryBaseRelativePath = curEntryPath.substring(baseEntryPath.length());
final URL tldUrl = createJarEntryUrl(jarBaseEntryUrl, curEntryBaseRelativePath);
addTldLocationFromTld(new JarEntryUrlTldLocation(tldUrl, null));
}
}
} else {
// Not a random-access file, so we fall back to the slower ZipInputStream approach.
if (LOG.isDebugEnabled()) {
LOG.debug("Scanning for " + META_INF_ABS_PATH + "**/*.tld-s in stream mode (slow): "
+ rawJarContentUrlEF);
}
final InputStream in = new URL(rawJarContentUrlEF).openStream();
try {
ZipInputStream zipIn = new ZipInputStream(in);
try {
while (true) {
ZipEntry curEntry = zipIn.getNextEntry();
if (curEntry == null) break;
String curEntryPath = normalizeJarEntryPath(curEntry.getName(), false);
if (curEntryPath.startsWith(baseEntryPath) && curEntryPath.endsWith(".tld")) {
final String curEntryBaseRelativePath = curEntryPath.substring(baseEntryPath.length());
final URL tldUrl = createJarEntryUrl(jarBaseEntryUrl, curEntryBaseRelativePath);
addTldLocationFromTld(zipIn, new JarEntryUrlTldLocation(tldUrl, null));
}
}
} finally {
zipIn.close();
}
} catch (ZipException e) {
// ZipException messages miss the zip URL
IOException ioe = new IOException("Error reading ZIP (see cause excepetion) from: "
+ rawJarContentUrlEF);
try {
ioe.initCause(e);
} catch (Exception e2) {
throw e;
}
throw ioe;
} finally {
in.close();
}
}
}
private void addTldLocationsFromFileDirectory(final File dir) throws IOException, SAXException {
if (dir.isDirectory()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Scanning for *.tld-s in File directory: " + StringUtil.jQuoteNoXSS(dir));
}
File[] tldFiles = dir.listFiles(new FilenameFilter() {
public boolean accept(File urlAsFile, String name) {
return isTldFileNameIgnoreCase(name);
}
});
if (tldFiles == null) {
throw new IOException("Can't list this directory for some reason: " + dir);
}
for (int i = 0; i < tldFiles.length; i++) {
final File file = tldFiles[i];
addTldLocationFromTld(new FileTldLocation(file));
}
} else {
LOG.warn("Skipped scanning for *.tld for non-existent directory: " + StringUtil.jQuoteNoXSS(dir));
}
}
/**
* Adds the TLD location mapping from the TLD itself.
*/
private void addTldLocationFromTld(TldLocation tldLocation) throws IOException, SAXException {
InputStream in = tldLocation.getInputStream();
try {
addTldLocationFromTld(in, tldLocation);
} finally {
in.close();
}
}
/**
* Use this overload only if you already have the {@link InputStream} for some reason, otherwise use
* {@link #addTldLocationFromTld(TldLocation)}.
*
* @param reusedIn
* The stream that we already had (so we don't have to open a new one from the {@code tldLocation}).
*/
private void addTldLocationFromTld(InputStream reusedIn, TldLocation tldLocation) throws SAXException,
IOException {
String taglibUri;
try {
taglibUri = getTaglibUriFromTld(reusedIn, tldLocation.getXmlSystemId());
} catch (SAXException e) {
LOG.error("Error while parsing TLD; skipping: " + tldLocation, e);
synchronized (failedTldLocations) {
failedTldLocations.add(tldLocation.toString());
}
taglibUri = null;
}
if (taglibUri != null) {
addTldLocation(tldLocation, taglibUri);
}
}
private void addTldLocation(TldLocation tldLocation, String taglibUri) {
if (tldLocations.containsKey(taglibUri)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Ignored duplicate mapping of taglib URI " + StringUtil.jQuoteNoXSS(taglibUri)
+ " to TLD location " + StringUtil.jQuoteNoXSS(tldLocation));
}
} else {
tldLocations.put(taglibUri, tldLocation);
if (LOG.isDebugEnabled()) {
LOG.debug("Mapped taglib URI " + StringUtil.jQuoteNoXSS(taglibUri)
+ " to TLD location " + StringUtil.jQuoteNoXSS(tldLocation));
}
}
}
private static Set/*<URLWithExternalForm>*/ collectMetaInfUrlsFromClassLoaders() throws IOException {
final Set/*<URLWithExternalForm>*/ metainfDirUrls = new TreeSet();
final ClassLoader tccl = tryGetThreadContextClassLoader();
if (tccl != null) {
collectMetaInfUrlsFromClassLoader(tccl, metainfDirUrls);
}
final ClassLoader cccl = TaglibFactory.class.getClassLoader();
if (!isDescendantOfOrSameAs(tccl, cccl)) {
collectMetaInfUrlsFromClassLoader(cccl, metainfDirUrls);
}
return metainfDirUrls;
}
private static void collectMetaInfUrlsFromClassLoader(ClassLoader cl, Set/* <URLWithExternalForm> */metainfDirUrls)
throws IOException {
Enumeration/*<URL>*/ urls = cl.getResources(META_INF_REL_PATH);
if (urls != null) {
while (urls.hasMoreElements()) {
metainfDirUrls.add(new URLWithExternalForm((URL) urls.nextElement()));
}
}
}
private String getTaglibUriFromTld(InputStream tldFileIn, String tldFileXmlSystemId) throws SAXException, IOException {
TldParserForTaglibUriExtraction tldParser = new TldParserForTaglibUriExtraction();
parseXml(tldFileIn, tldFileXmlSystemId, tldParser);
return tldParser.getTaglibUri();
}
/**
* @param tldLocation
* The physical location of the TLD file
* @param taglibUri
* The URI used in templates to refer to the taglib (like {@code <%@ taglib uri="..." ... %>} in JSP).
*/
private TemplateHashModel loadTaglib(TldLocation tldLocation, String taglibUri) throws IOException, SAXException {
if (LOG.isDebugEnabled()) {
LOG.debug("Loading taglib for URI " + StringUtil.jQuoteNoXSS(taglibUri)
+ " from TLD location " + StringUtil.jQuoteNoXSS(tldLocation));
}
final Taglib taglib = new Taglib(servletContext, tldLocation, objectWrapper);
taglibs.put(taglibUri, taglib);
tldLocations.remove(taglibUri);
return taglib;
}
private static void parseXml(InputStream in, String systemId, DefaultHandler handler)
throws SAXException, IOException {
InputSource inSrc = new InputSource();
inSrc.setSystemId(systemId);
inSrc.setByteStream(toCloseIgnoring(in));
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(false);
factory.setValidating(false);
XMLReader reader;
try {
reader = factory.newSAXParser().getXMLReader();
} catch (ParserConfigurationException e) {
// Not expected
throw new RuntimeException("XML parser setup failed", e);
}
reader.setEntityResolver(new LocalDtdEntityResolver());
reader.setContentHandler(handler);
reader.setErrorHandler(handler);
reader.parse(inSrc);
}
private static String resolveRelativeUri(String uri) throws TaglibGettingException {
TemplateModel reqHash;
try {
reqHash = Environment.getCurrentEnvironment().getVariable(
FreemarkerServlet.KEY_REQUEST_PRIVATE);
} catch (TemplateModelException e) {
throw new TaglibGettingException("Failed to get FreemarkerServlet request information", e);
}
if (reqHash instanceof HttpRequestHashModel) {
HttpServletRequest req =
((HttpRequestHashModel) reqHash).getRequest();
String pi = req.getPathInfo();
String reqPath = req.getServletPath();
if (reqPath == null) {
reqPath = "";
}
reqPath += (pi == null ? "" : pi);
// We don't care about paths with ".." in them. If the container
// wishes to resolve them on its own, let it be.
int lastSlash = reqPath.lastIndexOf('/');
if (lastSlash != -1) {
return reqPath.substring(0, lastSlash + 1) + uri;
} else {
return '/' + uri;
}
}
throw new TaglibGettingException(
"Can't resolve relative URI " + uri + " as request URL information is unavailable.");
}
/**
* Ignores attempts to close the stream.
*/
private static FilterInputStream toCloseIgnoring(InputStream in) {
return new FilterInputStream(in) {
@Override
public void close() {
// Do nothing
}
};
}
private static int getUriType(String uri) throws MalformedURLException {
if (uri == null) {
throw new IllegalArgumentException("null is not a valid URI");
}
if (uri.length() == 0) {
throw new MalformedURLException("empty string is not a valid URI");
}
final char c0 = uri.charAt(0);
if (c0 == '/') {
return URL_TYPE_ABSOLUTE;
}
// Check if it conforms to RFC 3986 3.1 in order to qualify as ABS_URI
if (c0 < 'a' || c0 > 'z') { // First char of scheme must be alpha
return URL_TYPE_RELATIVE;
}
final int colon = uri.indexOf(':');
if (colon == -1) { // Must have a colon
return URL_TYPE_RELATIVE;
}
// Subsequent chars must be [a-z,0-9,+,-,.]
for (int i = 1; i < colon; ++i) {
final char c = uri.charAt(i);
if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '+' && c != '-' && c != '.') {
return URL_TYPE_RELATIVE;
}
}
return URL_TYPE_FULL;
}
private static boolean isJarPath(final String uriPath) {
return uriPath.endsWith(".jar") || uriPath.endsWith(".zip");
}
private static boolean isJarUrl(URL url) {
final String scheme = url.getProtocol();
return "jar".equals(scheme) || "zip".equals(scheme)
|| "vfszip".equals(scheme) // JBoss AS
|| "wsjar".equals(scheme); // WebSphere
}
private static URL createJarEntryUrl(final URL jarBaseEntryUrl, String relativeEntryPath)
throws MalformedURLException {
if (relativeEntryPath.startsWith("/")) {
relativeEntryPath = relativeEntryPath.substring(1);
}
try {
return new URL(jarBaseEntryUrl, StringUtil.URLPathEnc(relativeEntryPath, PLATFORM_FILE_ENCODING));
} catch (UnsupportedEncodingException e) {
throw new BugException();
}
}
/**
* Trying to hide any JarFile implementation inconsistencies.
*/
private static String normalizeJarEntryPath(String jarEntryDirPath, boolean directory) {
// Not know to be a problem, but to be in the safe side:
if (!jarEntryDirPath.startsWith("/")) {
jarEntryDirPath = "/" + jarEntryDirPath;
}
// Known to be a problem:
if (directory && !jarEntryDirPath.endsWith("/")) {
jarEntryDirPath = jarEntryDirPath + "/";
}
return jarEntryDirPath;
}
private static MalformedURLException newFailedToExtractEntryPathException(final URL url) {
return new MalformedURLException("Failed to extract jar entry path from: " + url);
}
/**
* Converts an URL to a {@code File} object, if the URL format (scheme) makes is possible.
*/
private File urlToFileOrNull(URL url) {
if (test_emulateNoUrlToFileConversions) {
return null;
}
if (!"file".equals(url.getProtocol())) {
return null;
}
String filePath;
try {
// Using URI instead of URL, so we get an URL-decoded path.
filePath = url.toURI().getSchemeSpecificPart();
} catch (URISyntaxException e) { // Can happen, as URI-s are stricter than legacy URL-s.
// URL.getFile() doesn't decode %XX-s (used for spaces and non-US-ASCII letters usually), so we do.
// As it was originally created for a file somewhere, we hope that it uses the platform default encoding.
try {
filePath = URLDecoder.decode(url.getFile(), PLATFORM_FILE_ENCODING);
} catch (UnsupportedEncodingException e2) {
throw new BugException(e2);
}
}
return new File(filePath);
}
/**
* Gets a servlet context resource as a {@link JarFile} if possible, return {@code null} otherwise.
* For BC only, we try to get over errors during URL/JarFile construction, so then the caller can fall back to the
* legacy ZipInputStream-based approach.
*/
private JarFile servletContextResourceToFileOrNull(final String jarResourcePath) throws MalformedURLException,
IOException {
URL jarResourceUrl = servletContext.getResource(jarResourcePath);
if (jarResourceUrl == null) {
LOG.error("ServletContext resource URL was null (missing resource?): " + jarResourcePath);
return null;
}
File jarResourceAsFile = urlToFileOrNull(jarResourceUrl);
if (jarResourceAsFile == null) {
// Expected - it's just not File
return null;
}
if (!jarResourceAsFile.isFile()) {
LOG.error("Jar file doesn't exist - falling back to stream mode: " + jarResourceAsFile);
return null;
}
return new JarFile(jarResourceAsFile);
}
private static URL tryCreateServletContextJarEntryUrl(
ServletContext servletContext, final String servletContextJarFilePath, final String entryPath) {
try {
final URL jarFileUrl = servletContext.getResource(servletContextJarFilePath);
if (jarFileUrl == null) {
throw new IOException("Servlet context resource not found: " + servletContextJarFilePath);
}
return new URL(
"jar:"
+ jarFileUrl.toURI()
+ JAR_URL_ENTRY_PATH_START
+ URLEncoder.encode(
entryPath.startsWith("/") ? entryPath.substring(1) : entryPath,
PLATFORM_FILE_ENCODING));
} catch (Exception e) {
LOG.error("Couldn't get URL for serlvetContext resource "
+ StringUtil.jQuoteNoXSS(servletContextJarFilePath)
+ " / jar entry " + StringUtil.jQuoteNoXSS(entryPath),
e);
return null;
}
}
private static boolean isTldFileNameIgnoreCase(String name) {
final int dotIdx = name.lastIndexOf('.');
if (dotIdx < 0) return false;
final String extension = name.substring(dotIdx + 1).toLowerCase();
return extension.equalsIgnoreCase("tld");
}
private static ClassLoader tryGetThreadContextClassLoader() {
ClassLoader tccl;
try {
tccl = Thread.currentThread().getContextClassLoader();
} catch (SecurityException e) {
// Suppress
tccl = null;
LOG.warn("Can't access Thread Context ClassLoader", e);
}
return tccl;
}
private static boolean isDescendantOfOrSameAs(ClassLoader descendant, ClassLoader parent) {
while (true) {
if (descendant == null) {
return false;
}
if (descendant == parent) {
return true;
}
descendant = descendant.getParent();
}
}
/**
* A location within which we will look for {@code META-INF/**}{@code /*.tld}-s. Used in the parameter to
* {@link #setMetaInfTldSources}. See concrete subclasses for more.
*
* @since 2.3.22
*/
public static abstract class MetaInfTldSource {
private MetaInfTldSource() { }
}
/**
* To search TLD-s under <tt>sevletContext:/WEB-INF/lib/*.{jar,zip}/META-INF/**</tt><tt>/*.tld</tt>, as requested by
* the JSP specification. Note that these also used to be in the classpath, so it's redundant to use this together
* with a sufficiently permissive {@link ClasspathMetaInfTldSource}.
*
* @since 2.3.22
*/
public static final class WebInfPerLibJarMetaInfTldSource extends MetaInfTldSource {
public final static WebInfPerLibJarMetaInfTldSource INSTANCE = new WebInfPerLibJarMetaInfTldSource();
private WebInfPerLibJarMetaInfTldSource() { };
}
/**
* To search TLD-s under {@code META-INF/**}{@code /*.tld} inside classpath root containers, that is, in directories
* and jar-s that are in the classpath (or are visible for the class loader otherwise). It will only search inside
* those roots whose URL matches the pattern specified in the constructor. It correctly handles when multiple roots
* contain a TLD with the same name (typically, {@code META-INF/taglib.tld}), that is, those TLD-s won't shadow each
* other, all of them will be loaded independently.
*
* <p>
* Note that this TLD discovery mechanism is not part of the JSP specification.
*
* @since 2.3.22
*/
public static final class ClasspathMetaInfTldSource extends MetaInfTldSource {
private final Pattern rootContainerPattern;
/**
* @param rootContainerPattern
* The pattern against which the classpath root container URL-s will be matched. For example, to only
* search in jar-s whose name contains "taglib", the patter should be {@code ".*taglib\.jar$"}. To
* search everywhere, the pattern should be {@code ".*"}. The pattern need to match the whole URL,
* not just part of it.
*/
public ClasspathMetaInfTldSource(Pattern rootContainerPattern) {
this.rootContainerPattern = rootContainerPattern;
}
/**
* See constructor argument: {@link #ClasspathMetaInfTldSource(Pattern)}.
*/
public Pattern getRootContainerPattern() {
return rootContainerPattern;
};
}
/**
* When it occurs in the {@link MetaInfTldSource} list, all {@link MetaInfTldSource}-s before it will be disabled.
* This is useful when the list is assembled from multiple sources, and some want to re-start it, rather than append
* to the end of it.
*
* @see FreemarkerServlet#SYSTEM_PROPERTY_META_INF_TLD_SOURCES
* @see TaglibFactory#setMetaInfTldSources(List)
*/
public static final class ClearMetaInfTldSource extends MetaInfTldSource {
public final static ClearMetaInfTldSource INSTANCE = new ClearMetaInfTldSource();
private ClearMetaInfTldSource() { };
}
private interface TldLocation {
/**
* Reads the TLD file.
* @return Not {@code null}
*/
public abstract InputStream getInputStream() throws IOException;
/**
* The absolute URL of the TLD file.
* @return Not {@code null}
*/
public abstract String getXmlSystemId() throws IOException;
}
private interface InputStreamFactory {
InputStream getInputStream();
}
private class ServletContextTldLocation implements TldLocation {
private final String fileResourcePath;
public ServletContextTldLocation(String fileResourcePath) {
this.fileResourcePath = fileResourcePath;
}
public InputStream getInputStream() throws IOException {
final InputStream in = servletContext.getResourceAsStream(fileResourcePath);
if (in == null) {
throw newResourceNotFoundException();
}
return in;
}
public String getXmlSystemId() throws IOException {
final URL url = servletContext.getResource(fileResourcePath);
return url != null ? url.toExternalForm() : null;
}
private IOException newResourceNotFoundException() {
return new IOException("Resource not found: servletContext:" + fileResourcePath);
}
@Override
public final String toString() {
return "servletContext:" + fileResourcePath;
}
}
/**
* Points to plain class loader resource (regardless of if in what classpath root container it's in).
*/
private static class ClasspathTldLocation implements TldLocation {
private final String resourcePath;
public ClasspathTldLocation(String resourcePath) {
if (!resourcePath.startsWith("/")) {
throw new IllegalArgumentException("\"resourcePath\" must start with /");
}
this.resourcePath = resourcePath;
}
@Override
public String toString() {
return "classpath:" + resourcePath;
}
public InputStream getInputStream() throws IOException {
ClassLoader tccl = tryGetThreadContextClassLoader();
if (tccl != null) {
final InputStream in = getClass().getResourceAsStream(resourcePath);
if (in != null) {
return in;
}
}
final InputStream in = getClass().getResourceAsStream(resourcePath);
if (in == null) {
throw newResourceNotFoundException();
}
return in;
}
public String getXmlSystemId() throws IOException {
ClassLoader tccl = tryGetThreadContextClassLoader();
if (tccl != null) {
final URL url = getClass().getResource(resourcePath);
if (url != null) {
return url.toExternalForm();
}
}
final URL url = getClass().getResource(resourcePath);
return url == null ? null : url.toExternalForm();
}
private IOException newResourceNotFoundException() {
return new IOException("Resource not found: classpath:" + resourcePath);
}
}
private abstract class JarEntryTldLocation implements TldLocation {
/**
* Can be {@code null} if there was some technical problem, but then
* {@link #fallbackRawJarContentInputStreamFactory} and {@link #entryPath} will be non-{@code null}
*/
private final URL entryUrl;
private final InputStreamFactory fallbackRawJarContentInputStreamFactory;
private final String entryPath;
public JarEntryTldLocation(URL entryUrl, InputStreamFactory fallbackRawJarContentInputStreamFactory,
String entryPath) {
if (entryUrl == null) {
NullArgumentException.check(fallbackRawJarContentInputStreamFactory);
NullArgumentException.check(entryPath);
}
this.entryUrl = entryUrl;
this.fallbackRawJarContentInputStreamFactory = fallbackRawJarContentInputStreamFactory;
this.entryPath = entryPath != null ? normalizeJarEntryPath(entryPath, false) : null;
}
public InputStream getInputStream() throws IOException {
if (entryUrl != null) {
try {
if (test_emulateJarEntryUrlOpenStreamFails) {
throw new RuntimeException("Test only");
}
return entryUrl.openStream();
} catch (Exception e) {
if (fallbackRawJarContentInputStreamFactory == null) {
// Java 7 (Java 6?): We could just re-throw `e`
if (e instanceof IOException) {
throw (IOException) e;
}
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw new RuntimeException(e);
}
LOG.error("Failed to open InputStream for URL (will try fallback stream): " + entryUrl);
}
// Retry with the fallbackRawJarContentInputStreamFactory comes.
}
final String entryPath;
if (this.entryPath != null) {
entryPath = this.entryPath;
} else {
if (entryUrl == null) {
throw new IOException("Nothing to deduce jar entry path from.");
}
String urlEF = entryUrl.toExternalForm();
int sepIdx = urlEF.indexOf(JAR_URL_ENTRY_PATH_START);
if (sepIdx == -1) {
throw new IOException("Couldn't extract jar entry path from: " + urlEF);
}
entryPath = normalizeJarEntryPath(
URLDecoder.decode(
urlEF.substring(sepIdx + JAR_URL_ENTRY_PATH_START.length()),
PLATFORM_FILE_ENCODING),
false);
}
InputStream rawIn = null;
ZipInputStream zipIn = null;
boolean returnedZipIn = false;
try {
rawIn = fallbackRawJarContentInputStreamFactory.getInputStream();
if (rawIn == null) {
throw new IOException("Jar's InputStreamFactory (" + fallbackRawJarContentInputStreamFactory
+ ") says the resource doesn't exist.");
}
zipIn = new ZipInputStream(rawIn);
while (true) {
final ZipEntry macthedJarEntry = zipIn.getNextEntry();
if (macthedJarEntry == null) {
throw new IOException("Could not find JAR entry " + StringUtil.jQuoteNoXSS(entryPath) + ".");
}
if (entryPath.equals(normalizeJarEntryPath(macthedJarEntry.getName(), false))) {
returnedZipIn = true;
return zipIn;
}
}
} finally {
if (!returnedZipIn) {
if (zipIn != null) {
zipIn.close();
}
if (rawIn != null) {
rawIn.close();
}
}
}
}
public String getXmlSystemId() {
return entryUrl != null ? entryUrl.toExternalForm() : null;
}
@Override
public String toString() {
return entryUrl != null
? entryUrl.toExternalForm()
: "jar:{" + fallbackRawJarContentInputStreamFactory + "}!" + entryPath;
}
}
private class JarEntryUrlTldLocation extends JarEntryTldLocation {
private JarEntryUrlTldLocation(URL entryUrl, InputStreamFactory fallbackRawJarContentInputStreamFactory) {
super(entryUrl, fallbackRawJarContentInputStreamFactory, null);
}
}
/**
* Points to a file entry inside a jar, with optional {@link ZipInputStream} fallback.
*/
private class ServletContextJarEntryTldLocation extends JarEntryTldLocation {
/**
* For creating instance based on the servlet context resource path of a jar.
* While it tries to construct and use an URL that points directly to the target entry inside the jar, it will
* operate even if these URL-related operations fail.
*/
private ServletContextJarEntryTldLocation(final String servletContextJarFilePath, final String entryPath) {
super(
tryCreateServletContextJarEntryUrl(servletContext, servletContextJarFilePath, entryPath),
new InputStreamFactory() {
public InputStream getInputStream() {
return servletContext.getResourceAsStream(servletContextJarFilePath);
}
@Override
public String toString() {
return "servletContext:" + servletContextJarFilePath;
}
},
entryPath);
}
}
private static class FileTldLocation implements TldLocation {
private final File file;
public FileTldLocation(File file) {
this.file = file;
}
public InputStream getInputStream() throws IOException {
return new FileInputStream(file);
}
public String getXmlSystemId() throws IOException {
return file.toURI().toURL().toExternalForm();
}
@Override
public String toString() {
return file.toString();
}
}
private static final class Taglib implements TemplateHashModel {
private final Map tagsAndFunctions;
Taglib(ServletContext ctx, TldLocation tldPath, ObjectWrapper wrapper) throws IOException, SAXException {
tagsAndFunctions = parseToTagsAndFunctions(ctx, tldPath, wrapper);
}
public TemplateModel get(String key) {
return (TemplateModel) tagsAndFunctions.get(key);
}
public boolean isEmpty() {
return tagsAndFunctions.isEmpty();
}
private static final Map parseToTagsAndFunctions(
ServletContext ctx, TldLocation tldLocation, ObjectWrapper objectWrapper) throws IOException, SAXException {
final TldParserForTaglibBuilding tldParser = new TldParserForTaglibBuilding(objectWrapper);
InputStream in = tldLocation.getInputStream();
try {
parseXml(in, tldLocation.getXmlSystemId(), tldParser);
} finally {
in.close();
}
EventForwarding eventForwarding = EventForwarding.getInstance(ctx);
if (eventForwarding != null) {
eventForwarding.addListeners(tldParser.getListeners());
} else if (tldParser.getListeners().size() > 0) {
throw new TldParsingSAXException(
"Event listeners specified in the TLD could not be " +
" registered since the web application doesn't have a" +
" listener of class " + EventForwarding.class.getName() +
". To remedy this, add this element to web.xml:\n" +
"| <listener>\n" +
"| <listener-class>" + EventForwarding.class.getName() + "</listener-class>\n" +
"| </listener>", null);
}
return tldParser.getTagsAndFunctions();
}
}
private class WebXmlParser extends DefaultHandler {
private static final String E_TAGLIB = "taglib";
private static final String E_TAGLIB_LOCATION = "taglib-location";
private static final String E_TAGLIB_URI = "taglib-uri";
private StringBuilder cDataCollector;
private String taglibUriCData;
private String taglibLocationCData;
private Locator locator;
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
public void startElement(
String nsuri,
String localName,
String qName,
Attributes atts) {
if (E_TAGLIB_URI.equals(qName) || E_TAGLIB_LOCATION.equals(qName)) {
cDataCollector = new StringBuilder();
}
}
@Override
public void characters(char[] chars, int off, int len) {
if (cDataCollector != null) {
cDataCollector.append(chars, off, len);
}
}
@Override
public void endElement(String nsUri, String localName, String qName) throws TldParsingSAXException {
if (E_TAGLIB_URI.equals(qName)) {
taglibUriCData = cDataCollector.toString().trim();
cDataCollector = null;
} else if (E_TAGLIB_LOCATION.equals(qName)) {
taglibLocationCData = cDataCollector.toString().trim();
if (taglibLocationCData.length() == 0) {
throw new TldParsingSAXException("Required \"" + E_TAGLIB_URI + "\" element was missing or empty",
locator);
}
try {
if (getUriType(taglibLocationCData) == URL_TYPE_RELATIVE) {
taglibLocationCData = "/WEB-INF/" + taglibLocationCData;
}
} catch (MalformedURLException e) {
throw new TldParsingSAXException("Failed to detect URI type for: " + taglibLocationCData, locator, e);
}
cDataCollector = null;
} else if (E_TAGLIB.equals(qName)) {
addTldLocation(
isJarPath(taglibLocationCData)
? (TldLocation) new ServletContextJarEntryTldLocation(
taglibLocationCData, DEFAULT_TLD_RESOURCE_PATH)
: (TldLocation) new ServletContextTldLocation(taglibLocationCData),
taglibUriCData);
}
}
}
private static class TldParserForTaglibUriExtraction extends DefaultHandler {
private static final String E_URI = "uri";
private StringBuilder cDataCollector;
private String uri;
TldParserForTaglibUriExtraction() {
}
String getTaglibUri() {
return uri;
}
@Override
public void startElement(
String nsuri,
String localName,
String qName,
Attributes atts) {
if (E_URI.equals(qName)) {
cDataCollector = new StringBuilder();
}
}
@Override
public void characters(char[] chars, int off, int len) {
if (cDataCollector != null) {
cDataCollector.append(chars, off, len);
}
}
@Override
public void endElement(String nsuri, String localName, String qName) {
if (E_URI.equals(qName)) {
uri = cDataCollector.toString().trim();
cDataCollector = null;
}
}
}
static final class TldParserForTaglibBuilding extends DefaultHandler {
private static final String E_TAG = "tag";
private static final String E_NAME = "name";
private static final String E_TAG_CLASS = "tag-class";
private static final String E_TAG_CLASS_LEGACY = "tagclass";
private static final String E_FUNCTION = "function";
private static final String E_FUNCTION_CLASS = "function-class";
private static final String E_FUNCTION_SIGNATURE = "function-signature";
private static final String E_LISTENER = "listener";
private static final String E_LISTENER_CLASS = "listener-class";
private final BeansWrapper beansWrapper;
private final Map tagsAndFunctions = new HashMap();
private final List listeners = new ArrayList();
private Locator locator;
private StringBuilder cDataCollector;
private Stack stack = new Stack();
private String tagNameCData;
private String tagClassCData;
private String functionNameCData;
private String functionClassCData;
private String functionSignatureCData;
private String listenerClassCData;
TldParserForTaglibBuilding(ObjectWrapper wrapper) {
if (wrapper instanceof BeansWrapper) {
beansWrapper = (BeansWrapper) wrapper;
} else {
beansWrapper = null;
if (LOG.isWarnEnabled()) {
LOG.warn("Custom EL functions won't be loaded because "
+ (wrapper == null
? "no ObjectWrapper was specified for the TaglibFactory "
+ "(via TaglibFactory.setObjectWrapper(...), exists since 2.3.22)"
: "the ObjectWrapper wasn't instance of " + BeansWrapper.class.getName())
+ ".");
}
}
}
Map getTagsAndFunctions() {
return tagsAndFunctions;
}
List getListeners() {
return listeners;
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
public void startElement(String nsUri, String localName, String qName, Attributes atts) {
stack.push(qName);
if (stack.size() == 3) {
if (E_NAME.equals(qName) || E_TAG_CLASS_LEGACY.equals(qName) || E_TAG_CLASS.equals(qName)
|| E_LISTENER_CLASS.equals(qName) || E_FUNCTION_CLASS.equals(qName)
|| E_FUNCTION_SIGNATURE.equals(qName)) {
cDataCollector = new StringBuilder();
}
}
}
@Override
public void characters(char[] chars, int off, int len) {
if (cDataCollector != null) {
cDataCollector.append(chars, off, len);
}
}
@Override
public void endElement(String nsuri, String localName, String qName) throws TldParsingSAXException {
if (!stack.peek().equals(qName)) {
throw new TldParsingSAXException("Unbalanced tag nesting at \"" + qName + "\" end-tag.", locator);
}
if (stack.size() == 3) {
if (E_NAME.equals(qName)) {
if (E_TAG.equals(stack.get(1))) {
tagNameCData = pullCData();
} else if (E_FUNCTION.equals(stack.get(1))) {
functionNameCData = pullCData();
}
} else if (E_TAG_CLASS_LEGACY.equals(qName) || E_TAG_CLASS.equals(qName)) {
tagClassCData = pullCData();
} else if (E_LISTENER_CLASS.equals(qName)) {
listenerClassCData = pullCData();
} else if (E_FUNCTION_CLASS.equals(qName)) {
functionClassCData = pullCData();
} else if (E_FUNCTION_SIGNATURE.equals(qName)) {
functionSignatureCData = pullCData();
}
} else if (stack.size() == 2) {
if (E_TAG.equals(qName)) {
checkChildElementNotNull(qName, E_NAME, tagNameCData);
checkChildElementNotNull(qName, E_TAG_CLASS, tagClassCData);
final Class tagClass = resoveClassFromTLD(tagClassCData, "custom tag", tagNameCData);
final TemplateModel impl;
try {
if (Tag.class.isAssignableFrom(tagClass)) {
impl = new TagTransformModel(tagNameCData, tagClass);
} else {
impl = new SimpleTagDirectiveModel(tagNameCData, tagClass);
}
} catch (IntrospectionException e) {
throw new TldParsingSAXException(
"JavaBean introspection failed on custom tag class " + tagClassCData,
locator,
e);
}
tagsAndFunctions.put(tagNameCData, impl);
tagNameCData = null;
tagClassCData = null;
} else if (E_FUNCTION.equals(qName) && beansWrapper != null) {
checkChildElementNotNull(qName, E_FUNCTION_CLASS, functionClassCData);
checkChildElementNotNull(qName, E_FUNCTION_SIGNATURE, functionSignatureCData);
checkChildElementNotNull(qName, E_NAME, functionNameCData);
final Class functionClass = resoveClassFromTLD(
functionClassCData, "custom EL function", functionNameCData);
final Method functionMethod;
try {
functionMethod = TaglibMethodUtil.getMethodByFunctionSignature(
functionClass, functionSignatureCData);
} catch (Exception e) {
throw new TldParsingSAXException(
"Error while trying to resolve signature " + StringUtil.jQuote(functionSignatureCData)
+ " on class " + StringUtil.jQuote(functionClass.getName())
+ " for custom EL function " + StringUtil.jQuote(functionNameCData) + ".",
locator,
e);
}
final int modifiers = functionMethod.getModifiers();
if (!Modifier.isPublic(modifiers) || !Modifier.isStatic(modifiers)) {
throw new TldParsingSAXException(
"The custom EL function method must be public and static: " + functionMethod,
locator);
}
final TemplateMethodModelEx methodModel;
try {
methodModel = beansWrapper.wrap(null, functionMethod);
} catch (Exception e) {
throw new TldParsingSAXException(
"FreeMarker object wrapping failed on method : " + functionMethod,
locator);
}
tagsAndFunctions.put(functionNameCData, methodModel);
functionNameCData = null;
functionClassCData = null;
functionSignatureCData = null;
} else if (E_LISTENER.equals(qName)) {
checkChildElementNotNull(qName, E_LISTENER_CLASS, listenerClassCData);
final Class listenerClass = resoveClassFromTLD(listenerClassCData, E_LISTENER, null);
final Object listener;
try {
listener = listenerClass.newInstance();
} catch (Exception e) {
throw new TldParsingSAXException(
"Failed to create new instantiate from listener class " + listenerClassCData,
locator,
e);
}
listeners.add(listener);
listenerClassCData = null;
}
}
stack.pop();
}
private String pullCData() {
String r = cDataCollector.toString().trim();
cDataCollector = null;
return r;
}
private void checkChildElementNotNull(String parentElementName, String childElementName, String value)
throws TldParsingSAXException {
if (value == null) {
throw new TldParsingSAXException(
"Missing required \"" + childElementName + "\" element inside the \""
+ parentElementName + "\" element.", locator);
}
}
private Class resoveClassFromTLD(String className, String entryType, String entryName)
throws TldParsingSAXException {
try {
return ClassUtil.forName(className);
} catch (LinkageError e) {
throw newTLDEntryClassLoadingException(e, className, entryType, entryName);
} catch (ClassNotFoundException e) {
throw newTLDEntryClassLoadingException(e, className, entryType, entryName);
}
}
private TldParsingSAXException newTLDEntryClassLoadingException(Throwable e, String className,
String entryType, String entryName)
throws TldParsingSAXException {
int dotIdx = className.lastIndexOf('.');
if (dotIdx != -1) {
dotIdx = className.lastIndexOf('.', dotIdx - 1);
}
boolean looksLikeNestedClass =
dotIdx != -1 && className.length() > dotIdx + 1
&& Character.isUpperCase(className.charAt(dotIdx + 1));
return new TldParsingSAXException(
(e instanceof ClassNotFoundException ? "Not found class " : "Can't load class ")
+ StringUtil.jQuote(className) + " for " + entryType
+ (entryName != null ? " " + StringUtil.jQuote(entryName) : "") + "."
+ (looksLikeNestedClass
? " Hint: Before nested classes, use \"$\", not \".\"."
: ""),
locator,
e);
}
}
private static final class LocalDtdEntityResolver implements EntityResolver {
private static final Map DTDS = new HashMap();
static
{
// JSP taglib 1.2
DTDS.put("-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN", "web-jsptaglibrary_1_2.dtd");
DTDS.put("http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd", "web-jsptaglibrary_1_2.dtd");
// JSP taglib 1.1
DTDS.put("-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN", "web-jsptaglibrary_1_1.dtd");
DTDS.put("http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd", "web-jsptaglibrary_1_1.dtd");
// Servlet 2.3
DTDS.put("-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN", "web-app_2_3.dtd");
DTDS.put("http://java.sun.com/dtd/web-app_2_3.dtd", "web-app_2_3.dtd");
// Servlet 2.2
DTDS.put("-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN", "web-app_2_2.dtd");
DTDS.put("http://java.sun.com/j2ee/dtds/web-app_2_2.dtd", "web-app_2_2.dtd");
}
public InputSource resolveEntity(String publicId, String systemId) {
String resourceName = (String) DTDS.get(publicId);
if (resourceName == null) {
resourceName = (String) DTDS.get(systemId);
}
InputStream resourceStream;
if (resourceName != null) {
resourceStream = getClass().getResourceAsStream(resourceName);
} else {
// Fake an empty stream for unknown DTDs
resourceStream = new ByteArrayInputStream(new byte[0]);
}
InputSource is = new InputSource(resourceStream);
is.setPublicId(publicId);
is.setSystemId(systemId);
return is;
}
}
/**
* Redefines {@code SAXParseException#toString()} and {@code SAXParseException#getCause()} because it's broken on
* Java 1.6 and earlier.
*/
private static class TldParsingSAXException extends SAXParseException {
private final Throwable cause;
TldParsingSAXException(String message, Locator locator) {
this(message, locator, null);
}
TldParsingSAXException(String message, Locator locator, Throwable e) {
super(message, locator, e instanceof Exception ? (Exception) e : new Exception(
"Unchecked exception; see cause", e));
cause = e;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(getClass().getName());
sb.append(": ");
int startLn = sb.length();
String systemId = getSystemId();
String publicId = getPublicId();
if (systemId != null || publicId != null) {
sb.append("In ");
if (systemId != null) {
sb.append(systemId);
}
if (publicId != null) {
if (systemId != null) {
sb.append(" (public ID: ");
}
sb.append(publicId);
if (systemId != null) {
sb.append(')');
}
}
}
int line = getLineNumber();
if (line != -1) {
sb.append(sb.length() != startLn ? ", at " : "At ");
sb.append("line ");
sb.append(line);
int col = getColumnNumber();
if (col != -1) {
sb.append(", column ");
sb.append(col);
}
}
String message = getLocalizedMessage();
if (message != null) {
if (sb.length() != startLn) {
sb.append(":\n");
}
sb.append(message);
}
return sb.toString();
}
@Override
public Throwable getCause() {
Throwable superCause = super.getCause();
return superCause == null ? this.cause : superCause;
}
}
private static class URLWithExternalForm implements Comparable {
private final URL url;
private final String externalForm;
public URLWithExternalForm(URL url) {
this.url = url;
this.externalForm = url.toExternalForm();
}
public URL getUrl() {
return url;
}
public String getExternalForm() {
return externalForm;
}
@Override
public int hashCode() {
return externalForm.hashCode();
}
@Override
public boolean equals(Object that) {
if (this == that) return true;
if (that == null) return false;
if (getClass() != that.getClass()) return false;
return !externalForm.equals(((URLWithExternalForm) that).externalForm);
}
@Override
public String toString() {
return "URLWithExternalForm(" + externalForm + ")";
}
public int compareTo(Object that) {
return this.getExternalForm().compareTo(((URLWithExternalForm) that).getExternalForm());
}
}
private static class TaglibGettingException extends Exception {
public TaglibGettingException(String message, Throwable cause) {
super(message, cause);
}
public TaglibGettingException(String message) {
super(message);
}
}
}