/*
* 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.
debug("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); // Especially as we use dummy empty DTD-s
XMLReader reader;
try {
reader =
factory.
newSAXParser().
getXMLReader();
} catch (
ParserConfigurationException e) {
// Not expected
throw new
RuntimeException("XML parser setup failed",
e);
}
reader.
setEntityResolver(new
EmptyContentEntityResolver()); // To deal with referred DTD-s
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 ends with "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) {
InputStream ins =
ClassUtil.
getReasourceAsStream(
tccl,
resourcePath, true);
if (
ins != null) {
return
ins;
}
}
return
ClassUtil.
getReasourceAsStream(
getClass(),
resourcePath, false);
}
public
String getXmlSystemId() throws
IOException {
ClassLoader tccl =
tryGetThreadContextClassLoader();
if (
tccl != null) {
final
URL url =
tccl.
getResource(
resourcePath);
if (
url != null) {
return
url.
toExternalForm();
}
}
final
URL url =
getClass().
getResource(
resourcePath);
return
url == null ? null :
url.
toExternalForm();
}
}
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<
String,
TemplateModel>
tagsAndFunctions = new
HashMap<
String,
TemplateModel>();
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<
String,
TemplateModel>
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 customTagModel;
try {
if (
Tag.class.
isAssignableFrom(
tagClass)) {
customTagModel = new
TagTransformModel(
tagNameCData,
tagClass);
} else {
customTagModel = new
SimpleTagDirectiveModel(
tagNameCData,
tagClass);
}
} catch (
IntrospectionException e) {
throw new
TldParsingSAXException(
"JavaBean introspection failed on custom tag class " +
tagClassCData,
locator,
e);
}
TemplateModel replacedTagOrFunction =
tagsAndFunctions.
put(
tagNameCData,
customTagModel);
if (
replacedTagOrFunction != null) {
if (
CustomTagAndELFunctionCombiner.
canBeCombinedAsELFunction(
replacedTagOrFunction)) {
tagsAndFunctions.
put(
tagNameCData,
CustomTagAndELFunctionCombiner.
combine(
customTagModel, (
TemplateMethodModelEx)
replacedTagOrFunction));
} else {
LOG.
warn("TLD contains multiple tags with name " +
StringUtil.
jQuote(
tagNameCData)
+ "; keeping only the last one.");
}
}
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 elFunctionModel;
try {
elFunctionModel =
beansWrapper.
wrap(null,
functionMethod);
} catch (
Exception e) {
throw new
TldParsingSAXException(
"FreeMarker object wrapping failed on method : " +
functionMethod,
locator);
}
TemplateModel replacedTagOrFunction =
tagsAndFunctions.
put(
functionNameCData,
elFunctionModel);
if (
replacedTagOrFunction != null) {
if (
CustomTagAndELFunctionCombiner.
canBeCombinedAsCustomTag(
replacedTagOrFunction)) {
tagsAndFunctions.
put(
functionNameCData,
CustomTagAndELFunctionCombiner.
combine(
replacedTagOrFunction,
elFunctionModel));
} else {
LOG.
warn("TLD contains multiple functions with name " +
StringUtil.
jQuote(
functionNameCData)
+ "; keeping only the last one.");
}
}
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);
}
}
/**
* Dummy resolver that returns 0 length content for all requests.
*/
private static final class
EmptyContentEntityResolver implements
EntityResolver {
public
InputSource resolveEntity(
String publicId,
String systemId) {
InputSource is = new
InputSource(new
ByteArrayInputStream(new byte[0]));
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);
}
}
}