// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.webapp; import java.io.File; import java.io.IOException; import java.net.JarURLConnection; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.ConcurrentHashMap; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.stream.Collectors; import org.eclipse.jetty.util.PatternMatcher; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.EmptyResource; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceCollection; /** * MetaInfConfiguration * <p> * * Scan META-INF of jars to find: * <ul> * <li>tlds</li> * <li>web-fragment.xml</li> * <li>resources</li> * </ul> * * The jars which are scanned are: * <ol> * <li>those from the container classpath whose pattern matched the WebInfConfiguration.CONTAINER_JAR_PATTERN</li> * <li>those from WEB-INF/lib</li> * </ol> */ public class MetaInfConfiguration extends AbstractConfiguration { private static final Logger LOG = Log.getLogger(MetaInfConfiguration.class); public static final String USE_CONTAINER_METAINF_CACHE = "org.eclipse.jetty.metainf.useCache"; public static final boolean DEFAULT_USE_CONTAINER_METAINF_CACHE = true; public static final String CACHED_CONTAINER_TLDS = "org.eclipse.jetty.tlds.cache"; public static final String CACHED_CONTAINER_FRAGMENTS = FragmentConfiguration.FRAGMENT_RESOURCES+".cache"; public static final String CACHED_CONTAINER_RESOURCES = "org.eclipse.jetty.resources.cache"; public static final String METAINF_TLDS = "org.eclipse.jetty.tlds"; public static final String METAINF_FRAGMENTS = FragmentConfiguration.FRAGMENT_RESOURCES; public static final String METAINF_RESOURCES = "org.eclipse.jetty.resources"; public static final String CONTAINER_JAR_PATTERN = "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern"; public static final String WEBINF_JAR_PATTERN = "org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern"; public static final List<String> __allScanTypes = (List<String>) Arrays.asList(METAINF_TLDS, METAINF_RESOURCES, METAINF_FRAGMENTS); /** * If set, to a list of URLs, these resources are added to the context * resource base as a resource collection. */ public static final String RESOURCE_DIRS = "org.eclipse.jetty.resources"; /* ------------------------------------------------------------------------------- */ public MetaInfConfiguration() { addDependencies(WebXmlConfiguration.class); } /* ------------------------------------------------------------------------------- */ protected List<URI> getAllContainerJars(final WebAppContext context) throws URISyntaxException { List<URI> uris = new ArrayList<>(); if (context.getClassLoader() != null) { ClassLoader loader = context.getClassLoader().getParent(); while (loader != null) { if (loader instanceof URLClassLoader) { URL[] urls = ((URLClassLoader)loader).getURLs(); if (urls != null) for(URL url:urls) uris.add(new URI(url.toString().replaceAll(" ","%20"))); } loader = loader.getParent(); } } return uris; } /* ------------------------------------------------------------------------------- */ @Override public void preConfigure(final WebAppContext context) throws Exception { // discover matching container jars if (context.getClassLoader() != null) { List<URI> uris = getAllContainerJars(context); new PatternMatcher () { public void matched(URI uri) throws Exception { context.getMetaData().addContainerResource(Resource.newResource(uri)); } }.match((String)context.getAttribute(CONTAINER_JAR_PATTERN), uris.toArray(new URI[uris.size()]), false); } //Discover matching WEB-INF/lib jars List<Resource> jars = findJars(context); if (jars!=null) { List<URI> uris = jars.stream().map(Resource::getURI).collect(Collectors.toList()); new PatternMatcher () { @Override public void matched(URI uri) throws Exception { context.getMetaData().addWebInfJar(Resource.newResource(uri)); } }.match((String)context.getAttribute(WEBINF_JAR_PATTERN), uris.toArray(new URI[uris.size()]), true); } //No pattern to appy to classes, just add to metadata context.getMetaData().setWebInfClassesDirs(findClassDirs(context)); scanJars(context); } protected void scanJars (WebAppContext context) throws Exception { boolean useContainerCache = DEFAULT_USE_CONTAINER_METAINF_CACHE; if (context.getServer() != null) { Boolean attr = (Boolean)context.getServer().getAttribute(USE_CONTAINER_METAINF_CACHE); if (attr != null) useContainerCache = attr.booleanValue(); } if (LOG.isDebugEnabled()) LOG.debug("{} = {}", USE_CONTAINER_METAINF_CACHE, useContainerCache); //pre-emptively create empty lists for tlds, fragments and resources as context attributes //this signals that this class has been called. This differentiates the case where this class //has been called but finds no META-INF data from the case where this class was never called if (context.getAttribute(METAINF_TLDS) == null) context.setAttribute(METAINF_TLDS, new HashSet<URL>()); if (context.getAttribute(METAINF_RESOURCES) == null) context.setAttribute(METAINF_RESOURCES, new HashSet<Resource>()); if (context.getAttribute(METAINF_FRAGMENTS) == null) context.setAttribute(METAINF_FRAGMENTS, new HashMap<Resource, Resource>()); //always scan everything from the container's classpath scanJars(context, context.getMetaData().getContainerResources(), useContainerCache, __allScanTypes); //only look for fragments if web.xml is not metadata complete, or it version 3.0 or greater List<String> scanTypes = new ArrayList<>(__allScanTypes); if (context.getMetaData().isMetaDataComplete() || (context.getServletContext().getEffectiveMajorVersion() < 3) && !context.isConfigurationDiscovered()) scanTypes.remove(METAINF_FRAGMENTS); scanJars(context, context.getMetaData().getWebInfJars(), false, scanTypes); } /** * For backwards compatibility. This method will always scan for all types of data. * * @param context the context for the scan * @param jars the jars to scan * @param useCaches if true, the scanned info is cached * @throws Exception */ public void scanJars (final WebAppContext context, Collection<Resource> jars, boolean useCaches) throws Exception { scanJars(context, jars, useCaches, __allScanTypes); } @Override public void configure(WebAppContext context) throws Exception { // Look for extra resource @SuppressWarnings("unchecked") Set<Resource> resources = (Set<Resource>)context.getAttribute(RESOURCE_DIRS); if (resources!=null && !resources.isEmpty()) { Resource[] collection=new Resource[resources.size()+1]; int i=0; collection[i++]=context.getBaseResource(); for (Resource resource : resources) collection[i++]=resource; context.setBaseResource(new ResourceCollection(collection)); } } /** * Look into the jars to discover info in META-INF. If useCaches == true, then we will * cache the info discovered indexed by the jar in which it was discovered: this speeds * up subsequent context deployments. * * @param context the context for the scan * @param jars the jars resources to scan * @param useCaches if true, cache the info discovered * @param scanTypes the type of things to look for in the jars * @throws Exception if unable to scan the jars */ public void scanJars (final WebAppContext context, Collection<Resource> jars, boolean useCaches, List<String> scanTypes ) throws Exception { ConcurrentHashMap<Resource, Resource> metaInfResourceCache = null; ConcurrentHashMap<Resource, Resource> metaInfFragmentCache = null; ConcurrentHashMap<Resource, Collection<URL>> metaInfTldCache = null; if (useCaches) { metaInfResourceCache = (ConcurrentHashMap<Resource, Resource>)context.getServer().getAttribute(CACHED_CONTAINER_RESOURCES); if (metaInfResourceCache == null) { metaInfResourceCache = new ConcurrentHashMap<Resource,Resource>(); context.getServer().setAttribute(CACHED_CONTAINER_RESOURCES, metaInfResourceCache); } metaInfFragmentCache = (ConcurrentHashMap<Resource, Resource>)context.getServer().getAttribute(CACHED_CONTAINER_FRAGMENTS); if (metaInfFragmentCache == null) { metaInfFragmentCache = new ConcurrentHashMap<Resource,Resource>(); context.getServer().setAttribute(CACHED_CONTAINER_FRAGMENTS, metaInfFragmentCache); } metaInfTldCache = (ConcurrentHashMap<Resource, Collection<URL>>)context.getServer().getAttribute(CACHED_CONTAINER_TLDS); if (metaInfTldCache == null) { metaInfTldCache = new ConcurrentHashMap<Resource,Collection<URL>>(); context.getServer().setAttribute(CACHED_CONTAINER_TLDS, metaInfTldCache); } } //Scan jars for META-INF information if (jars != null) { for (Resource r : jars) { if (scanTypes.contains(METAINF_RESOURCES)) scanForResources(context, r, metaInfResourceCache); if (scanTypes.contains(METAINF_FRAGMENTS)) scanForFragment(context, r, metaInfFragmentCache); if (scanTypes.contains(METAINF_TLDS)) scanForTlds(context, r, metaInfTldCache); } } } /** * Scan for META-INF/resources dir in the given jar. * * @param context the context for the scan * @param target the target resource to scan for * @param cache the resource cache * @throws Exception if unable to scan for resources */ public void scanForResources (WebAppContext context, Resource target, ConcurrentHashMap<Resource,Resource> cache) throws Exception { Resource resourcesDir = null; if (cache != null && cache.containsKey(target)) { resourcesDir = cache.get(target); if (resourcesDir == EmptyResource.INSTANCE) { if (LOG.isDebugEnabled()) LOG.debug(target+" cached as containing no META-INF/resources"); return; } else if (LOG.isDebugEnabled()) LOG.debug(target+" META-INF/resources found in cache "); } else { //not using caches or not in the cache so check for the resources dir if (LOG.isDebugEnabled()) LOG.debug(target+" META-INF/resources checked"); if (target.isDirectory()) { //TODO think how to handle an unpacked jar file (eg for osgi) resourcesDir = target.addPath("/META-INF/resources"); } else { //Resource represents a packed jar URI uri = target.getURI(); resourcesDir = Resource.newResource(uriJarPrefix(uri,"!/META-INF/resources")); } if (!resourcesDir.exists() || !resourcesDir.isDirectory()) { resourcesDir.close(); resourcesDir = EmptyResource.INSTANCE; } if (cache != null) { Resource old = cache.putIfAbsent(target, resourcesDir); if (old != null) resourcesDir = old; else if (LOG.isDebugEnabled()) LOG.debug(target+" META-INF/resources cache updated"); } if (resourcesDir == EmptyResource.INSTANCE) { return; } } //add it to the meta inf resources for this context Set<Resource> dirs = (Set<Resource>)context.getAttribute(METAINF_RESOURCES); if (dirs == null) { dirs = new HashSet<Resource>(); context.setAttribute(METAINF_RESOURCES, dirs); } if (LOG.isDebugEnabled()) LOG.debug(resourcesDir+" added to context"); dirs.add(resourcesDir); } /** * Scan for META-INF/web-fragment.xml file in the given jar. * * @param context the context for the scan * @param jar the jar resource to scan for fragements in * @param cache the resource cache * @throws Exception if unable to scan for fragments */ public void scanForFragment (WebAppContext context, Resource jar, ConcurrentHashMap<Resource,Resource> cache) throws Exception { Resource webFrag = null; if (cache != null && cache.containsKey(jar)) { webFrag = cache.get(jar); if (webFrag == EmptyResource.INSTANCE) { if (LOG.isDebugEnabled()) LOG.debug(jar+" cached as containing no META-INF/web-fragment.xml"); return; } else if (LOG.isDebugEnabled()) LOG.debug(jar+" META-INF/web-fragment.xml found in cache "); } else { //not using caches or not in the cache so check for the web-fragment.xml if (LOG.isDebugEnabled()) LOG.debug(jar+" META-INF/web-fragment.xml checked"); if (jar.isDirectory()) { //TODO ???? webFrag = jar.addPath("/META-INF/web-fragment.xml"); } else { URI uri = jar.getURI(); webFrag = Resource.newResource(uriJarPrefix(uri,"!/META-INF/web-fragment.xml")); } if (!webFrag.exists() || webFrag.isDirectory()) { webFrag.close(); webFrag = EmptyResource.INSTANCE; } if (cache != null) { //web-fragment.xml doesn't exist: put token in cache to signal we've seen the jar Resource old = cache.putIfAbsent(jar, webFrag); if (old != null) webFrag = old; else if (LOG.isDebugEnabled()) LOG.debug(jar+" META-INF/web-fragment.xml cache updated"); } if (webFrag == EmptyResource.INSTANCE) return; } Map<Resource, Resource> fragments = (Map<Resource,Resource>)context.getAttribute(METAINF_FRAGMENTS); if (fragments == null) { fragments = new HashMap<Resource, Resource>(); context.setAttribute(METAINF_FRAGMENTS, fragments); } fragments.put(jar, webFrag); if (LOG.isDebugEnabled()) LOG.debug(webFrag+" added to context"); } /** * Discover META-INF/*.tld files in the given jar * * @param context the context for the scan * @param jar the jar resources to scan tlds for * @param cache the resource cache * @throws Exception if unable to scan for tlds */ public void scanForTlds (WebAppContext context, Resource jar, ConcurrentHashMap<Resource, Collection<URL>> cache) throws Exception { Collection<URL> tlds = null; if (cache != null && cache.containsKey(jar)) { Collection<URL> tmp = cache.get(jar); if (tmp.isEmpty()) { if (LOG.isDebugEnabled()) LOG.debug(jar+" cached as containing no tlds"); return; } else { tlds = tmp; if (LOG.isDebugEnabled()) LOG.debug(jar+" tlds found in cache "); } } else { //not using caches or not in the cache so find all tlds tlds = new HashSet<URL>(); if (jar.isDirectory()) { tlds.addAll(getTlds(jar.getFile())); } else { URI uri = jar.getURI(); tlds.addAll(getTlds(uri)); } if (cache != null) { if (LOG.isDebugEnabled()) LOG.debug(jar+" tld cache updated"); Collection<URL> old = (Collection<URL>)cache.putIfAbsent(jar, tlds); if (old != null) tlds = old; } if (tlds.isEmpty()) return; } Collection<URL> metaInfTlds = (Collection<URL>)context.getAttribute(METAINF_TLDS); if (metaInfTlds == null) { metaInfTlds = new HashSet<URL>(); context.setAttribute(METAINF_TLDS, metaInfTlds); } metaInfTlds.addAll(tlds); if (LOG.isDebugEnabled()) LOG.debug("tlds added to context"); } @Override public void postConfigure(WebAppContext context) throws Exception { context.setAttribute(METAINF_RESOURCES, null); context.setAttribute(METAINF_FRAGMENTS, null); context.setAttribute(METAINF_TLDS, null); } /** * Find all .tld files in all subdirs of the given dir. * * @param dir the directory to scan * @return the list of tlds found * @throws IOException if unable to scan the directory */ public Collection<URL> getTlds (File dir) throws IOException { if (dir == null || !dir.isDirectory()) return Collections.emptySet(); HashSet<URL> tlds = new HashSet<URL>(); File[] files = dir.listFiles(); if (files != null) { for (File f:files) { if (f.isDirectory()) tlds.addAll(getTlds(f)); else { String name = f.getCanonicalPath(); if (name.contains("META-INF") && name.endsWith(".tld")) tlds.add(f.toURI().toURL()); } } } return tlds; } /** * Find all .tld files in the given jar. * * @param uri the uri to jar file * @return the collection of tlds as url references * @throws IOException if unable to scan the jar file */ public Collection<URL> getTlds (URI uri) throws IOException { HashSet<URL> tlds = new HashSet<URL>(); String jarUri = uriJarPrefix(uri, "!/"); URL url = new URL(jarUri); JarURLConnection jarConn = (JarURLConnection) url.openConnection(); jarConn.setUseCaches(Resource.getDefaultUseCaches()); JarFile jarFile = jarConn.getJarFile(); Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry e = entries.nextElement(); String name = e.getName(); if (name.startsWith("META-INF") && name.endsWith(".tld")) { tlds.add(new URL(jarUri + name)); } } if (!Resource.getDefaultUseCaches()) jarFile.close(); return tlds; } protected List<Resource> findClassDirs (WebAppContext context) throws Exception { if (context == null) return null; List<Resource> classDirs = new ArrayList<Resource>(); Resource webInfClasses = findWebInfClassesDir(context); if (webInfClasses != null) classDirs.add(webInfClasses); List<Resource> extraClassDirs = findExtraClasspathDirs(context); if (extraClassDirs != null) classDirs.addAll(extraClassDirs); return classDirs; } /** * Look for jars that should be treated as if they are in WEB-INF/lib * * @param context the context to find the jars in * @return the list of jar resources found within context * @throws Exception if unable to find the jars */ protected List<Resource> findJars (WebAppContext context) throws Exception { List<Resource> jarResources = new ArrayList<Resource>(); List<Resource> webInfLibJars = findWebInfLibJars(context); if (webInfLibJars != null) jarResources.addAll(webInfLibJars); List<Resource> extraClasspathJars = findExtraClasspathJars(context); if (extraClasspathJars != null) jarResources.addAll(extraClasspathJars); return jarResources; } /** * Look for jars in <code>WEB-INF/lib</code> * * @param context the context to find the lib jars in * @return the list of jars as {@link Resource} * @throws Exception if unable to scan for lib jars */ protected List<Resource> findWebInfLibJars(WebAppContext context) throws Exception { Resource web_inf = context.getWebInf(); if (web_inf==null || !web_inf.exists()) return null; List<Resource> jarResources = new ArrayList<Resource>(); Resource web_inf_lib = web_inf.addPath("/lib"); if (web_inf_lib.exists() && web_inf_lib.isDirectory()) { String[] files=web_inf_lib.list(); for (int f=0;files!=null && f<files.length;f++) { try { Resource file = web_inf_lib.addPath(files[f]); String fnlc = file.getName().toLowerCase(Locale.ENGLISH); int dot = fnlc.lastIndexOf('.'); String extension = (dot < 0 ? null : fnlc.substring(dot)); if (extension != null && (extension.equals(".jar") || extension.equals(".zip"))) { jarResources.add(file); } } catch (Exception ex) { LOG.warn(Log.EXCEPTION,ex); } } } return jarResources; } /** * Get jars from WebAppContext.getExtraClasspath as resources * * @param context the context to find extra classpath jars in * @return the list of Resources with the extra classpath, or null if not found * @throws Exception if unable to find the extra classpath jars */ protected List<Resource> findExtraClasspathJars(WebAppContext context) throws Exception { if (context == null || context.getExtraClasspath() == null) return null; List<Resource> jarResources = new ArrayList<Resource>(); StringTokenizer tokenizer = new StringTokenizer(context.getExtraClasspath(), ",;"); while (tokenizer.hasMoreTokens()) { Resource resource = context.newResource(tokenizer.nextToken().trim()); String fnlc = resource.getName().toLowerCase(Locale.ENGLISH); int dot = fnlc.lastIndexOf('.'); String extension = (dot < 0 ? null : fnlc.substring(dot)); if (extension != null && (extension.equals(".jar") || extension.equals(".zip"))) { jarResources.add(resource); } } return jarResources; } /** * Get <code>WEB-INF/classes</code> dir * * @param context the context to look for the <code>WEB-INF/classes</code> directory * @return the Resource for the <code>WEB-INF/classes</code> directory * @throws Exception if unable to find the <code>WEB-INF/classes</code> directory */ protected Resource findWebInfClassesDir (WebAppContext context) throws Exception { if (context == null) return null; Resource web_inf = context.getWebInf(); // Find WEB-INF/classes if (web_inf != null && web_inf.isDirectory()) { // Look for classes directory Resource classes= web_inf.addPath("classes/"); if (classes.exists()) return classes; } return null; } /** * Get class dirs from WebAppContext.getExtraClasspath as resources * * @param context the context to look for extra classpaths in * @return the list of Resources to the extra classpath * @throws Exception if unable to find the extra classpaths */ protected List<Resource> findExtraClasspathDirs(WebAppContext context) throws Exception { if (context == null || context.getExtraClasspath() == null) return null; List<Resource> dirResources = new ArrayList<Resource>(); StringTokenizer tokenizer = new StringTokenizer(context.getExtraClasspath(), ",;"); while (tokenizer.hasMoreTokens()) { Resource resource = context.newResource(tokenizer.nextToken().trim()); if (resource.exists() && resource.isDirectory()) dirResources.add(resource); } return dirResources; } private String uriJarPrefix(URI uri, String suffix) { String uriString = uri.toString(); if (uriString.startsWith("jar:")) { return uriString + suffix; } else { return "jar:" + uriString + suffix; } } }