/* * 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.openejb.config; import org.apache.openejb.OpenEJBException; import org.apache.openejb.loader.SystemInstance; import org.apache.openejb.util.DaemonThreadFactory; import org.apache.openejb.util.URLs; import org.apache.xbean.finder.UrlSet; import org.apache.xbean.finder.filter.Filters; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.jar.JarEntry; import java.util.jar.JarFile; import static java.util.Arrays.asList; import static org.apache.openejb.config.NewLoaderLogic.applyBuiltinExcludes; import static org.apache.openejb.util.URLs.toFile; /** * TLD file urls cached on a per classloader basis. Helps with sharing TLD * files between webapps by placing them in a parent classloader. * <p/> * Each webapp will be able to retrieve the cached version of the URLs and * therefore only needs to scan its own libraries, the parent libraries will * already have been scanned. * <p/> * For a tiny bit of performance, we will scan the StandardClassloader at boot * in a separate thread so it should be primed in advance of any deployment. * * @version $Rev$ $Date$ */ public class TldScanner { // first cache, it is the faster one but not relevant between temp and runtime phases private static final Map<ClassLoader, Set<URL>> cache = new WeakHashMap<ClassLoader, Set<URL>>(); // tld by classloader identified by hash on urls (same hash for temp and runtime classloaders) // a bit longer to compute but let scanning be reused over temp and runtime classloaders private static final Map<Integer, Set<URL>> cacheByhashCode = new WeakHashMap<Integer, Set<URL>>(); public static Set<URL> scan(final ClassLoader classLoader) throws OpenEJBException { if (skip()) { return Collections.emptySet(); } if (classLoader == null) { return Collections.emptySet(); } final Set<URL> urls = cache.get(classLoader); if (urls != null) { return urls; } final Set<URL> result = scanClassLoaderForTagLibs(classLoader); cache.put(classLoader, result); return result; } private static boolean skip() { return !"true".equalsIgnoreCase(SystemInstance.get().getProperty("openejb.taglib.scan", "true")); } public static Set<URL> scanClassLoaderForTagLibs(final ClassLoader classLoader) throws OpenEJBException { if (skip()) { return Collections.emptySet(); } final Set<URL> tldUrls = new HashSet<URL>(); if (classLoader == null) { return tldUrls; } if (classLoader == Object.class.getClassLoader()) { return tldUrls; } final List<URL> urls = urls(classLoader); final int hashCodeForUrls = hash(urls); final Set<URL> cachedSet = cacheByhashCode.get(hashCodeForUrls); if (cachedSet != null) { return cachedSet; } tldUrls.addAll(scan(classLoader.getParent())); if (urls.size() > 0) { final ExecutorService es = Executors.newFixedThreadPool( Math.min(urls.size(), 2 * Runtime.getRuntime().availableProcessors() + 1), new DaemonThreadFactory("OpenEJB-tld-server-scanning")); final Collection<Future<Set<URL>>> futures = new ArrayList<Future<Set<URL>>>(urls.size()); for (URL url : urls) { if (url.getProtocol().equals("jar")) { try { String path = url.getPath(); if (path.endsWith("!/")) { path = path.substring(0, path.length() - 2); } url = new URL(path); } catch (final MalformedURLException e) { DeploymentLoader.logger.warning("JSP tag library location bad: " + url.toExternalForm(), e); continue; } } if (!url.getProtocol().equals("file")) { continue; } final File file; try { file = toFile(url).getCanonicalFile().getAbsoluteFile(); } catch (final IOException e) { DeploymentLoader.logger.warning("JSP tag library location bad: " + url.toExternalForm(), e); continue; } futures.add(es.submit(new Callable<Set<URL>>() { @Override public Set<URL> call() throws Exception { return scanForTagLibs(file); } })); } es.shutdown(); for (final Future<Set<URL>> set : futures) { try { tldUrls.addAll(set.get()); } catch (final Exception e) { // no-op } } } cacheByhashCode.put(hashCodeForUrls, tldUrls); return tldUrls; } static Set<URL> scanWarForTagLibs(final File war) { final Set<URL> urls = new HashSet<URL>(); final File webInfDir = new File(war, "WEB-INF"); if (!webInfDir.isDirectory()) { return urls; } // skip the lib and classes dir in WEB-INF final LinkedList<File> files = new LinkedList<File>(); final File[] list = webInfDir.listFiles(); if (list != null) { for (final File file : list) { if ("lib".equals(file.getName()) || "classes".equals(file.getName())) { continue; } files.add(file); } } final File webInfMetaInf = new File(webInfDir, "classes/META-INF"); if (webInfMetaInf.exists()) { // filter directly to let it be faster in next loop files.addAll(asList(webInfMetaInf.listFiles(new FilenameFilter() { @Override public boolean accept(final File dir, final String name) { return name.endsWith(".tld"); } }))); } if (files.isEmpty()) { return urls; } // recursively scan the directories while (!files.isEmpty()) { File file = files.removeFirst(); if (file.isDirectory()) { final File[] a = file.listFiles(); if (a != null) { files.addAll(asList(a)); } } else if (file.getName().endsWith(".tld")) { try { file = file.getCanonicalFile().getAbsoluteFile(); urls.add(file.toURI().toURL()); } catch (final IOException e) { DeploymentLoader.logger.warning("JSP tag library location bad: " + file.getAbsolutePath(), e); } } } return urls; } static Set<URL> scanForTagLibs(final File file) { final Set<URL> tldLocations = new HashSet<URL>(); try { final String location = file.toURI().toURL().toExternalForm(); if (location.endsWith(".jar")) { final Set<URL> urls = scanJarForTagLibs(file); tldLocations.addAll(urls); } else if (file.getName().endsWith(".tld")) { final URL url = file.toURI().toURL(); tldLocations.add(url); } } catch (final IOException e) { DeploymentLoader.logger.warning("Error scanning for JSP tag libraries: " + file.getAbsolutePath(), e); } return tldLocations; } static Set<URL> scanJarForTagLibs(final File file) { final Set<URL> urls = new HashSet<URL>(); if (!file.isFile()) { return urls; } JarFile jarFile = null; try { jarFile = new JarFile(file); final URL jarFileUrl = new URL("jar", "", -1, file.toURI().toURL().toExternalForm() + "!/"); for (final JarEntry entry : Collections.list(jarFile.entries())) { final String name = entry.getName(); if (!name.startsWith("META-INF/") || !name.endsWith(".tld")) { continue; } final URL url = new URL(jarFileUrl, name); urls.add(url); } } catch (final IOException e) { DeploymentLoader.logger.warning("Error scanning jar for JSP tag libraries: " + file.getAbsolutePath(), e); } finally { if (jarFile != null) { try { jarFile.close(); } catch (final IOException e) { // exception ignored } } } return urls; } // mainly used to forget a classloader (temp one generally) but keep scanning info from classloader urls public static void quickClean(final ClassLoader loader) { if (loader == null) { return; } cache.remove(loader); if (loader.getParent() != TldScanner.class.getClassLoader()) { // for ears quickClean(loader.getParent()); } } // this method clean the cacheByhash too public static void forceCompleteClean(final ClassLoader loader) { if (loader == null) { return; } quickClean(loader); cacheByhashCode.remove(hash(urls(loader))); if (loader.getParent() != TldScanner.class.getClassLoader()) { // for ears forceCompleteClean(loader.getParent()); } } private static List<URL> urls(final ClassLoader classLoader) { UrlSet urlSet = new UrlSet(); if (classLoader instanceof URLClassLoader) { final URLClassLoader urlClassLoader = (URLClassLoader) classLoader; try { urlSet = new UrlSet(urlClassLoader.getURLs()); } catch (final NullPointerException npe) { // happen for closeable classloaders like WebappClassLoader when already clean up return Collections.emptyList(); } } else { try { urlSet = new UrlSet(classLoader); } catch (final IOException e) { DeploymentLoader.logger.warning("Error scanning class loader for JSP tag libraries", e); } } try { urlSet = URLs.cullSystemJars(urlSet); urlSet = applyBuiltinExcludes( urlSet, Filters.tokens("taglibs-standard-impl", "taglibs-standard-jstlel", "javax.faces-2.", "spring-security-taglibs", "spring-webmvc"), Filters.prefixes("commons-jcs-", "myfaces-", "tomcat-websocket.jar")); // myfaces is hardcoded in tomee } catch (final IOException e) { DeploymentLoader.logger.warning("Error scanning class loader for JSP tag libraries", e); } return urlSet.getUrls(); } private static int hash(final List<URL> urls) { int hash = 0; for (final URL u : urls) { hash *= 31; if (u != null) { hash += u.toExternalForm().hashCode(); // url.hashCode() can be slow offline } } return hash; } }