TldScanner.java
/*
* 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 org.apache.jasper.servlet;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.ServletContext;
import jakarta.servlet.descriptor.JspConfigDescriptor;
import jakarta.servlet.descriptor.TaglibDescriptor;
import org.apache.jasper.compiler.JarScannerFactory;
import org.apache.jasper.compiler.Localizer;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.Jar;
import org.apache.tomcat.JarScanType;
import org.apache.tomcat.JarScanner;
import org.apache.tomcat.JarScannerCallback;
import org.apache.tomcat.util.descriptor.tld.TaglibXml;
import org.apache.tomcat.util.descriptor.tld.TldParser;
import org.apache.tomcat.util.descriptor.tld.TldResourcePath;
import org.xml.sax.SAXException;
/**
* Scans for and loads Tag Library Descriptors contained in a web application.
*/
public class TldScanner {
private final Log log = LogFactory.getLog(TldScanner.class); // must not be static
private static final String MSG = "org.apache.jasper.servlet.TldScanner";
private static final String TLD_EXT = ".tld";
private static final String WEB_INF = "/WEB-INF/";
private final ServletContext context;
private final TldParser tldParser;
private final Map<String, TldResourcePath> uriTldResourcePathMap = new HashMap<>();
private final Map<TldResourcePath, TaglibXml> tldResourcePathTaglibXmlMap = new HashMap<>();
private final List<String> listeners = new ArrayList<>();
/**
* Initialise with the application's ServletContext.
*
* @param context the application's servletContext
* @param namespaceAware should the XML parser used to parse TLD files be
* configured to be name space aware
* @param validation should the XML parser used to parse TLD files be
* configured to use validation
* @param blockExternal should the XML parser used to parse TLD files be
* configured to be block references to external
* entities
*/
public TldScanner(ServletContext context,
boolean namespaceAware,
boolean validation,
boolean blockExternal) {
this.context = context;
this.tldParser = new TldParser(namespaceAware, validation, blockExternal);
}
/**
* Scan for TLDs in all places defined by the specification:
* <ol>
* <li>Tag libraries defined by the platform</li>
* <li>Entries from <jsp-config> in web.xml</li>
* <li>A resources under /WEB-INF</li>
* <li>In jar files from /WEB-INF/lib</li>
* <li>Additional entries from the container</li>
* </ol>
*
* @throws IOException if there was a problem scanning for or loading a TLD
* @throws SAXException if there was a problem parsing a TLD
*/
public void scan() throws IOException, SAXException {
scanPlatform();
scanJspConfig();
scanResourcePaths(WEB_INF);
scanJars();
}
/**
* Returns the map of URI to TldResourcePath built by this scanner.
*
* @return the map of URI to TldResourcePath
*/
public Map<String, TldResourcePath> getUriTldResourcePathMap() {
return uriTldResourcePathMap;
}
/**
* Returns the map of TldResourcePath to parsed XML files built by this
* scanner.
*
* @return the map of TldResourcePath to parsed XML files
*/
public Map<TldResourcePath,TaglibXml> getTldResourcePathTaglibXmlMap() {
return tldResourcePathTaglibXmlMap;
}
/**
* Returns a list of all listeners declared by scanned TLDs.
*
* @return a list of listener class names
*/
public List<String> getListeners() {
return listeners;
}
/**
* Set the class loader used by the digester to create objects as a result
* of this scan. Normally this only needs to be set when using JspC.
*
* @param classLoader Class loader to use when creating new objects while
* parsing TLDs
*/
public void setClassLoader(ClassLoader classLoader) {
tldParser.setClassLoader(classLoader);
}
/**
* Scan for TLDs required by the platform specification.
*/
protected void scanPlatform() {
}
/**
* Scan for TLDs defined in <jsp-config>.
* @throws IOException Error reading resources
* @throws SAXException XML parsing error
*/
protected void scanJspConfig() throws IOException, SAXException {
JspConfigDescriptor jspConfigDescriptor = context.getJspConfigDescriptor();
if (jspConfigDescriptor == null) {
return;
}
Collection<TaglibDescriptor> descriptors = jspConfigDescriptor.getTaglibs();
for (TaglibDescriptor descriptor : descriptors) {
String taglibURI = descriptor.getTaglibURI();
String resourcePath = descriptor.getTaglibLocation();
// Note: Whilst the Servlet 2.4 DTD implies that the location must
// be a context-relative path starting with '/', JSP.7.3.6.1 states
// explicitly how paths that do not start with '/' should be
// handled.
if (!resourcePath.startsWith("/")) {
resourcePath = WEB_INF + resourcePath;
}
if (uriTldResourcePathMap.containsKey(taglibURI)) {
log.warn(Localizer.getMessage(MSG + ".webxmlSkip",
resourcePath,
taglibURI));
continue;
}
if (log.isTraceEnabled()) {
log.trace(Localizer.getMessage(MSG + ".webxmlAdd",
resourcePath,
taglibURI));
}
URL url = context.getResource(resourcePath);
if (url != null) {
TldResourcePath tldResourcePath;
if (resourcePath.endsWith(".jar")) {
// if the path points to a jar file, the TLD is presumed to be
// inside at META-INF/taglib.tld
tldResourcePath = new TldResourcePath(url, resourcePath, "META-INF/taglib.tld");
} else {
tldResourcePath = new TldResourcePath(url, resourcePath);
}
// parse TLD but store using the URI supplied in the descriptor
TaglibXml tld = tldParser.parse(tldResourcePath);
uriTldResourcePathMap.put(taglibURI, tldResourcePath);
tldResourcePathTaglibXmlMap.put(tldResourcePath, tld);
if (tld.getListeners() != null) {
listeners.addAll(tld.getListeners());
}
} else {
log.warn(Localizer.getMessage(MSG + ".webxmlFailPathDoesNotExist",
resourcePath,
taglibURI));
continue;
}
}
}
/**
* Scan web application resources for TLDs, recursively.
*
* @param startPath the directory resource to scan
* @throws IOException if there was a problem scanning for or loading a TLD
* @throws SAXException if there was a problem parsing a TLD
*/
protected void scanResourcePaths(String startPath)
throws IOException, SAXException {
boolean found = false;
Set<String> dirList = context.getResourcePaths(startPath);
if (dirList != null) {
for (String path : dirList) {
if (path.startsWith("/WEB-INF/classes/")) {
// Skip: JSP.7.3.1
} else if (path.startsWith("/WEB-INF/lib/")) {
// Skip: JSP.7.3.1
} else if (path.endsWith("/")) {
scanResourcePaths(path);
} else if (path.startsWith("/WEB-INF/tags/")) {
// JSP 7.3.1: in /WEB-INF/tags only consider implicit.tld
if (path.endsWith("/implicit.tld")) {
found = true;
parseTld(path);
}
} else if (path.endsWith(TLD_EXT)) {
found = true;
parseTld(path);
}
}
}
if (found) {
if (log.isDebugEnabled()) {
log.debug(Localizer.getMessage("jsp.tldCache.tldInResourcePath", startPath));
}
} else {
if (log.isDebugEnabled()) {
log.debug(Localizer.getMessage("jsp.tldCache.noTldInResourcePath", startPath));
}
}
}
/**
* Scan for TLDs in JARs in /WEB-INF/lib.
*/
public void scanJars() {
JarScanner scanner = JarScannerFactory.getJarScanner(context);
TldScannerCallback callback = new TldScannerCallback();
scanner.scan(JarScanType.TLD, context, callback);
if (callback.scanFoundNoTLDs()) {
log.info(Localizer.getMessage("jsp.tldCache.noTldSummary"));
}
}
protected void parseTld(String resourcePath) throws IOException, SAXException {
TldResourcePath tldResourcePath =
new TldResourcePath(context.getResource(resourcePath), resourcePath);
parseTld(tldResourcePath);
}
protected void parseTld(TldResourcePath path) throws IOException, SAXException {
TaglibXml tld = tldParser.parse(path);
String uri = tld.getUri();
if (uri != null) {
if (!uriTldResourcePathMap.containsKey(uri)) {
uriTldResourcePathMap.put(uri, path);
}
}
if (tldResourcePathTaglibXmlMap.containsKey(path)) {
// TLD has already been parsed as a result of processing web.xml
return;
}
tldResourcePathTaglibXmlMap.put(path, tld);
if (tld.getListeners() != null) {
listeners.addAll(tld.getListeners());
}
}
class TldScannerCallback implements JarScannerCallback {
private boolean foundJarWithoutTld = false;
private boolean foundFileWithoutTld = false;
@Override
public void scan(Jar jar, String webappPath, boolean isWebapp) throws IOException {
boolean found = false;
URL jarFileUrl = jar.getJarFileURL();
jar.nextEntry();
for (String entryName = jar.getEntryName();
entryName != null;
jar.nextEntry(), entryName = jar.getEntryName()) {
if (!(entryName.startsWith("META-INF/") &&
entryName.endsWith(TLD_EXT))) {
continue;
}
found = true;
TldResourcePath tldResourcePath =
new TldResourcePath(jarFileUrl, webappPath, entryName);
try {
parseTld(tldResourcePath);
} catch (SAXException e) {
throw new IOException(e);
}
}
if (found) {
if (log.isDebugEnabled()) {
log.debug(Localizer.getMessage("jsp.tldCache.tldInJar", jarFileUrl.toString()));
}
} else {
foundJarWithoutTld = true;
if (log.isDebugEnabled()) {
log.debug(Localizer.getMessage(
"jsp.tldCache.noTldInJar", jarFileUrl.toString()));
}
}
}
@Override
public void scan(File file, final String webappPath, boolean isWebapp)
throws IOException {
File metaInf = new File(file, "META-INF");
if (!metaInf.isDirectory()) {
return;
}
foundFileWithoutTld = false;
final Path filePath = file.toPath();
Files.walkFileTree(metaInf.toPath(), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file,
BasicFileAttributes attrs)
throws IOException {
Path fileName = file.getFileName();
if (fileName == null || !fileName.toString().toLowerCase(
Locale.ENGLISH).endsWith(TLD_EXT)) {
return FileVisitResult.CONTINUE;
}
foundFileWithoutTld = true;
String resourcePath;
if (webappPath == null) {
resourcePath = null;
} else {
String subPath = file.subpath(
filePath.getNameCount(), file.getNameCount()).toString();
if ('/' != File.separatorChar) {
subPath = subPath.replace(File.separatorChar, '/');
}
resourcePath = webappPath + "/" + subPath;
}
try {
URL url = file.toUri().toURL();
TldResourcePath path = new TldResourcePath(url, resourcePath);
parseTld(path);
} catch (SAXException e) {
throw new IOException(e);
}
return FileVisitResult.CONTINUE;
}
});
if (foundFileWithoutTld) {
if (log.isDebugEnabled()) {
log.debug(Localizer.getMessage("jsp.tldCache.tldInDir",
file.getAbsolutePath()));
}
} else {
if (log.isDebugEnabled()) {
log.debug(Localizer.getMessage("jsp.tldCache.noTldInDir",
file.getAbsolutePath()));
}
}
}
@Override
public void scanWebInfClasses() throws IOException {
// This is used when scanAllDirectories is enabled and one or more
// JARs have been unpacked into WEB-INF/classes as happens with some
// IDEs.
Set<String> paths = context.getResourcePaths(WEB_INF + "classes/META-INF");
if (paths == null) {
return;
}
for (String path : paths) {
if (path.endsWith(TLD_EXT)) {
try {
parseTld(path);
} catch (SAXException e) {
throw new IOException(e);
}
}
}
}
boolean scanFoundNoTLDs() {
return foundJarWithoutTld;
}
}
}