/* * Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky * * Licensed 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 " : "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); } } }