blob: ef8ca88db67a92d79ec3d161ef588e4e61416f8e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2015 SAP SE
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* SAP AG - initial contribution
*******************************************************************************/
package org.eclipse.virgo.web.enterprise.services.accessor;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.osgi.framework.adaptor.BundleClassLoader;
import org.eclipse.osgi.framework.adaptor.BundleData;
import org.eclipse.osgi.framework.adaptor.ClassLoaderDelegateHook;
import org.eclipse.osgi.framework.internal.core.BundleHost;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleReference;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.wiring.BundleCapability;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.framework.wiring.BundleWire;
import org.osgi.framework.wiring.BundleWiring;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class WebAppBundleClassLoaderDelegateHook implements ClassLoaderDelegateHook {
private static final String GEMINI_WEB_TOMCAT_SYMBOLIC_NAME = "org.eclipse.gemini.web.tomcat";
private static final Logger LOGGER = LoggerFactory.getLogger(WebAppBundleClassLoaderDelegateHook.class);
private static final int MAX_API_SEARCH_DEPTH = 1;
private static final int MAX_IMPL_SEARCH_DEPTH = 2;
private static final int MAX_RESOURCE_SEARCH_DEPTH = 1;
private final ThreadLocal<AtomicInteger> delegationInProgress = new ThreadLocal<>();
private final Set<Bundle> apiBundles = new CopyOnWriteArraySet<>();
private final Set<Bundle> implBundles = new CopyOnWriteArraySet<>();
private final Map<Bundle, ClassLoader> implBundlesClassloaders = Collections.synchronizedMap(new TreeMap<>(new VirgoEEBundleComparable()));
private final Map<Bundle, Set<String>> webAppBundles = new ConcurrentHashMap<>();
private final Set<Bundle> postFindApiBundles = new CopyOnWriteArraySet<>();
private Set<String> negativeCacheClassPrefixes = new HashSet<>();
private static Map<Bundle, CacheableObject> negativeCacheClassPerAPIBundle = new ConcurrentHashMap<>();
private static Map<Bundle, CacheableObject> negativeCacheResourcePerAPIBundle = new ConcurrentHashMap<>();
private static Map<Bundle, CacheableObject> negativeCacheClassPerTCCL = new ConcurrentHashMap<>();
private static Map<Bundle, CacheableObject> negativeCacheResourcePerTCCL = new ConcurrentHashMap<>();
private long timeToLive = 20 * 60 * 1000;
static {
Thread cleaner = new Thread(new Runnable() {
@Override
public void run() {
try {
while (true) {
removeFromNegativeCache(true, true);
removeFromNegativeCache(true, false);
removeFromNegativeCache(false, true);
removeFromNegativeCache(false, false);
Thread.sleep(60000);
}
} catch (InterruptedException ignored) {
}
}
private synchronized void removeFromNegativeCache(boolean isClass, boolean isAPI) {
Collection<CacheableObject> values;
if (isClass) {
if (isAPI) {
values = negativeCacheClassPerAPIBundle.values();
} else {
values = negativeCacheClassPerTCCL.values();
}
} else {
if (isAPI) {
values = negativeCacheResourcePerAPIBundle.values();
} else {
values = negativeCacheResourcePerTCCL.values();
}
}
for (CacheableObject value : values) {
if (value.isExpired()) {
values.remove(value);
}
}
}
});
cleaner.setPriority(Thread.MIN_PRIORITY);
cleaner.start();
}
WebAppBundleClassLoaderDelegateHook() {
negativeCacheClassPrefixes.add("openwebbeans/Messages");
negativeCacheClassPrefixes.add("com.sun.faces.LogStrings");
negativeCacheClassPrefixes.add("javax.faces.LogStrings");
negativeCacheClassPrefixes.add("org.apache.catalina.loader.LocalStrings");
negativeCacheClassPrefixes.add("org.apache.tomcat.util.file.LocalStrings");
negativeCacheClassPrefixes.add("org.apache.tomcat.util.scan.LocalStrings");
negativeCacheClassPrefixes.add("org.apache.tomcat.util.http.mapper.LocalStrings");
negativeCacheClassPrefixes.add("org.apache.tomcat.util.net.res.LocalStrings");
negativeCacheClassPrefixes.add("org.apache.tomcat.util.threads.res.LocalStrings");
negativeCacheClassPrefixes.add("ValidationMessages");
negativeCacheClassPrefixes.add("org.apache.bval.jsr303.ValidationMessages");
negativeCacheClassPrefixes.add("com.sun.xml.internal.messaging.saaj.soap.LocalStrings");
negativeCacheClassPrefixes.add("org.apache.openejb.package-info");
negativeCacheClassPrefixes.add("org.apache.openejb.monitoring.package-info");
negativeCacheClassPrefixes.add("org.apache.geronimo.openejb.cdi.GeronimoWebBeansPlugin");
negativeCacheClassPrefixes.add("org.apache.openejb.server.rest.RsRegistry");
negativeCacheClassPrefixes.add("javax.faces.application.ConfigurableNavigationHandlerBeanInfo");
negativeCacheClassPrefixes.add("javax.faces.application.NavigationHandlerBeanInfo");
negativeCacheClassPrefixes.add("org.apache.webbeans.jsf.ConversationAwareViewHandlerBeanInfo");
negativeCacheClassPrefixes.add("javax.faces.application.ViewHandlerWrapperBeanInfo");
negativeCacheClassPrefixes.add("javax.faces.application.ViewHandlerBeanInfo");
negativeCacheClassPrefixes.add("javax.faces.component.UIViewRootBeanInfo");
negativeCacheClassPrefixes.add("javax.faces.component.UIComponentBaseBeanInfo");
negativeCacheClassPrefixes.add("javax.faces.component.UIComponentBeanInfo");
negativeCacheClassPrefixes.add("javax.management.MBean");
negativeCacheClassPrefixes.add("org.apache.cxf.APIMessages");
negativeCacheClassPrefixes.add("org.apache.cxf.binding.xml.interceptor.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.bus.extension.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.bus.managers.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.common.injection.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.common.logging.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.common.util.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.endpoint.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.interceptor.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.jaxrs.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.jaxrs.impl.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.jaxrs.interceptor.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.jaxrs.model.wadl.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.jaxrs.provider.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.jaxrs.servlet.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.jaxrs.utils.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.resource.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.service.factory.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.service.invoker.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.service.model.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.staxutils.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.transport.http.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.transport.servlet.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.transport.https.Messages");
negativeCacheClassPrefixes.add("org.apache.cxf.phase.Messages");
// keep javax.management.MBean out of the list:
}
@Override
public Class<?> postFindClass(String name, BundleClassLoader bcl, BundleData bd) {
if(matchesNegativeCache(name)) {
return null;
}
if (shouldEnter(MAX_IMPL_SEARCH_DEPTH)) {
try {
enter();
Bundle bundle = bd.getBundle();
if (this.implBundles.contains(bundle)) {
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
if (tccl != null && isBundleWebAppCL(tccl) && !matchesNegativeCachePerTCCL(name, tccl, true)) {
try {
return tccl.loadClass(name);
} catch (ClassNotFoundException e) {
// normal delegation should continue
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exception occurred while trying to find class [" + name + "]. Exception message: " + e.getMessage());
}
addToNegativeCachePerTCCL(name, tccl, true);
} catch (NoClassDefFoundError e) {
// normal delegation should continue
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exception occurred while trying to find class [" + name + "]. Exception message: " + e.getMessage());
}
}
}
}
} finally {
exit();
}
}
if (shouldEnter(MAX_API_SEARCH_DEPTH)) {
try {
enter();
Bundle bundle = bd.getBundle();
if (this.webAppBundles.containsKey(bundle)) {
for (Bundle postFindApiProvider : postFindApiBundles) {
try {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Post find api for class " + name
+ " from bundle "
+ bundle.getSymbolicName()
+ ". Trying to load it with bundle "
+ postFindApiProvider.getSymbolicName());
}
if (!matchesNegativeCachePerAPIBundle(name, postFindApiProvider, true)) {
return postFindApiProvider.loadClass(name);
}
} catch (ClassNotFoundException e) {
// keep moving through the bundles
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exception occurred while trying to find (post api) class ["
+ name
+ "]. Exception message: "
+ e.getMessage());
}
addToNegativeCachePerAPIBundle(name, postFindApiProvider, true);
}
}
}
} finally {
exit();
}
}
return null;
}
@Override
public String postFindLibrary(String name, BundleClassLoader bcl, BundleData bd) {
// no-op
return null;
}
@Override
public URL postFindResource(String name, BundleClassLoader bcl, BundleData bd) {
if (shouldEnter(MAX_RESOURCE_SEARCH_DEPTH)) {
try {
enter();
Bundle bundle = bd.getBundle();
if (this.webAppBundles.containsKey(bundle)) {
return doFindApiResource(name);
}
if (this.implBundles.contains(bundle)) {
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
if (tccl != null && isBundleWebAppCL(tccl) && !matchesNegativeCachePerTCCL(name, tccl, false)) {
URL resource = tccl.getResource(name);
if (resource != null) {
return resource;
} else {
addToNegativeCachePerTCCL(name, tccl, false);
}
}
}
} finally {
exit();
}
}
return null;
}
private boolean shouldEnter(int maxDepth) {
if (this.delegationInProgress.get() == null) {
return true;
}
if (this.delegationInProgress.get().get() < maxDepth) {
return true;
}
return false;
}
private void enter() {
if (this.delegationInProgress.get() == null) {
this.delegationInProgress.set(new AtomicInteger(0));
}
this.delegationInProgress.get().incrementAndGet();
}
private void exit() {
if (this.delegationInProgress.get() != null) {
if (this.delegationInProgress.get().get() > 0) {
this.delegationInProgress.get().decrementAndGet();
}
}
}
@Override
public Enumeration<URL> postFindResources(String name, BundleClassLoader bcl, BundleData bd) {
if (shouldEnter(MAX_RESOURCE_SEARCH_DEPTH)) {
try {
enter();
Bundle bundle = bd.getBundle();
if (this.webAppBundles.containsKey(bundle)) {
return doFindApiResources(name);
}
if (this.implBundles.contains(bundle)) {
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
if (tccl != null) {
try {
return tccl.getResources(name);
} catch (IOException e) {
// normal delegation should continue
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exception occurred while trying to find resources [" + name + "]. Exception message: " + e.getMessage());
}
}
}
}
} finally {
exit();
}
}
return null;
}
@Override
public Class<?> preFindClass(String name, BundleClassLoader bcl, BundleData bd) {
if(matchesNegativeCache(name)) {
return null;
}
if (shouldEnter(MAX_API_SEARCH_DEPTH)) {
try {
enter();
Bundle bundle = bd.getBundle();
if (this.webAppBundles.containsKey(bundle)) {
if (checkPackageInImport(name, this.webAppBundles.get(bundle))) {
return null;
} else {
return doFindApiClass(name);
}
}
} finally {
exit();
}
}
return null;
}
private boolean checkPackageInImport(String className, Set<String> importedPackages) {
int index = className.lastIndexOf('.');
String packageName = className;
if (index > -1) {
packageName = className.substring(0, index);
}
return importedPackages.contains(packageName);
}
@Override
public String preFindLibrary(String name, BundleClassLoader bcl, BundleData bd) {
// no-op
return null;
}
@Override
public URL preFindResource(String name, BundleClassLoader bcl, BundleData bd) {
// no-op
return null;
}
@Override
public Enumeration<URL> preFindResources(String name, BundleClassLoader bcl, BundleData bd) {
// no-op
return null;
}
void addApiBundle(Bundle bundle) {
this.apiBundles.add(bundle);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(bundle + "was added to API bundles.");
}
}
void addPostApiBundle(Bundle bundle) {
this.postFindApiBundles.add(bundle);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(bundle + "was added to post API bundles.");
}
}
void addImplBundle(Bundle bundle) {
this.implBundles.add(bundle);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(bundle + "was added to Impl bundles.");
}
ClassLoader cl = getBundleClassloader(bundle);
if (cl != null) {
this.implBundlesClassloaders.put(bundle, cl);
}
}
void removeApiBundle(Bundle bundle) {
this.apiBundles.remove(bundle);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(bundle + "was removed from API bundles.");
}
}
void removeImplBundle(Bundle bundle) {
this.implBundlesClassloaders.remove(bundle);
this.implBundles.remove(bundle);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(bundle + "was removed from Impl bundles.");
}
}
void removePostApiBundle(Bundle bundle) {
this.postFindApiBundles.remove(bundle);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(bundle + "was added to post API bundles.");
}
}
void addWebAppBundle(Bundle bundle) {
this.webAppBundles.put(bundle, cacheRequiredCapabilities(bundle));
}
void removeWebAppBundle(Bundle bundle) {
this.webAppBundles.remove(bundle);
}
private Set<String> cacheRequiredCapabilities(Bundle bundle) {
Set<String> importedPackages = new HashSet<>();
BundleWiring bundleWiring = bundle.adapt(BundleRevision.class).getWiring();
importedPackages.addAll(getImportedPackages(bundleWiring));
importedPackages.addAll(getImportedPackagesFromRequiredBundles(bundleWiring));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Deploying web bundle " + bundle.getSymbolicName() + " with import packages " + importedPackages);
}
return importedPackages;
}
private Set<String> getImportedPackagesFromRequiredBundles(BundleWiring bundleWiring) {
Set<String> importedPackages = new HashSet<>();
List<BundleWire> requiredWires = bundleWiring.getRequiredWires(BundleRevision.BUNDLE_NAMESPACE);
for (BundleWire requiredWire : requiredWires) {
List<BundleCapability> capabilities = requiredWire.getProviderWiring().getCapabilities(BundleRevision.PACKAGE_NAMESPACE);
for (BundleCapability capability : capabilities) {
importedPackages.add((String) capability.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE));
}
}
return importedPackages;
}
private Set<String> getImportedPackages(BundleWiring bundleWiring) {
Set<String> importedPackages = new HashSet<>();
List<BundleWire> requiredWires = bundleWiring.getRequiredWires(BundleRevision.PACKAGE_NAMESPACE);
for (BundleWire requiredWire : requiredWires) {
importedPackages.add((String) requiredWire.getCapability().getAttributes().get(BundleRevision.PACKAGE_NAMESPACE));
}
return importedPackages;
}
ClassLoader[] getImplBundlesClassloaders() {
return this.implBundlesClassloaders.values().toArray(new ClassLoader[0]);
}
private Class<?> doFindApiClass(String name) {
for (Bundle bundle : this.apiBundles) {
try {
if (!matchesNegativeCachePerAPIBundle(name, bundle, true)) {
return bundle.loadClass(name);
}
} catch (ClassNotFoundException e) {
// keep moving through the bundles
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exception occurred while trying to find class [" + name + "]. Exception message: " + e.getMessage());
}
addToNegativeCachePerAPIBundle(name, bundle, true);
}
}
return null;
}
private URL doFindApiResource(String name) {
for (Bundle bundle : this.apiBundles) {
if (!matchesNegativeCachePerAPIBundle(name, bundle, false)) {
URL resource = bundle.getResource(name);
if (resource != null) {
return resource;
} else {
addToNegativeCachePerAPIBundle(name, bundle, false);
}
}
}
return null;
}
private Enumeration<URL> doFindApiResources(String name) {
for (Bundle bundle : this.apiBundles) {
try {
Enumeration<URL> resources = bundle.getResources(name);
if (resources != null) {
return resources;
}
} catch (IOException e) {
// keep moving through the bundles
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Exception occurred while trying to find resources [" + name + "]. Exception message: " + e.getMessage());
}
}
}
return null;
}
private ClassLoader getBundleClassloader(Bundle bundle) {
if (bundle instanceof BundleHost) {
return ((BundleHost) bundle).getClassLoader();
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Cannot obtain classloader for bundle " + bundle);
}
return null;
}
}
Set<Bundle> getApiBundles() {
return this.apiBundles;
}
Set<Bundle> getImplBundles() {
return this.implBundles;
}
private boolean isBundleWebAppCL(ClassLoader tccl) {
Bundle bundle = FrameworkUtil.getBundle(tccl.getClass());
if(bundle!= null && bundle.getSymbolicName().equals(GEMINI_WEB_TOMCAT_SYMBOLIC_NAME)) {
return true;
}
return false;
}
private boolean matchesNegativeCache(String className) {
for(String prefix : negativeCacheClassPrefixes) {
if(className.startsWith(prefix)) {
return true;
}
}
return false;
}
private boolean matchesNegativeCachePerTCCL(String name, ClassLoader tccl, boolean isClass) {
if (tccl instanceof BundleReference) {
Bundle bundle = ((BundleReference) tccl).getBundle();
CacheableObject object;
if (isClass) {
object = (negativeCacheClassPerTCCL.get(bundle));
} else {
object = (negativeCacheResourcePerTCCL.get(bundle));
}
if (object != null) {
Queue<String> cache = object.getObject();
for (String cachedName : cache) {
if (name.equals(cachedName)) {
return true;
}
}
}
}
return false;
}
private synchronized void addToNegativeCachePerTCCL(String name, ClassLoader tccl, boolean isClass) {
if (tccl instanceof BundleReference) {
Bundle bundle = ((BundleReference) tccl).getBundle();
CacheableObject object;
if (isClass) {
object = (negativeCacheClassPerTCCL.get(bundle));
} else {
object = (negativeCacheResourcePerTCCL.get(bundle));
}
Queue<String> classes;
if (object == null) {
classes = new ConcurrentLinkedQueue<>();
if (isClass) {
negativeCacheClassPerTCCL.put(bundle, new CacheableObject(
classes, timeToLive));
} else {
negativeCacheResourcePerTCCL.put(bundle, new CacheableObject(
classes, timeToLive));
}
} else {
classes = object.getObject();
}
classes.add(name);
}
}
private boolean matchesNegativeCachePerAPIBundle(String name, Bundle apiBundle, boolean isClass) {
CacheableObject object;
if (isClass) {
object = (negativeCacheClassPerAPIBundle.get(apiBundle));
} else {
object = (negativeCacheResourcePerAPIBundle.get(apiBundle));
}
if (object != null) {
Queue<String> cache = object.getObject();
for (String cachedName : cache) {
if (name.equals(cachedName)) {
return true;
}
}
}
return false;
}
private synchronized void addToNegativeCachePerAPIBundle(String name, Bundle apiBundle, boolean isClass) {
CacheableObject object;
if (isClass) {
object = (negativeCacheClassPerAPIBundle.get(apiBundle));
} else {
object = (negativeCacheResourcePerAPIBundle.get(apiBundle));
}
Queue<String> classes;
if (object == null) {
classes = new ConcurrentLinkedQueue<>();
if (isClass) {
negativeCacheClassPerAPIBundle.put(apiBundle, new CacheableObject(
classes, timeToLive));
} else {
negativeCacheResourcePerAPIBundle.put(apiBundle, new CacheableObject(
classes, timeToLive));
}
} else {
classes = object.getObject();
}
classes.add(name);
}
static class CacheableObject {
private Queue<String> object;
private long expiration;
CacheableObject(Queue<String> object, long timeToLive) {
this.object = object;
this.expiration = System.currentTimeMillis() + timeToLive;
}
boolean isExpired() {
if (System.currentTimeMillis() > expiration) {
return true;
}
return false;
}
Queue<String> getObject() {
return object;
}
}
}