blob: 899e116cd712eaa9af69225720c5e35f3cfd78ec [file] [log] [blame]
package org.eclipse.scout.commons.osgi;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Pattern;
import org.eclipse.scout.commons.StringUtility;
import org.eclipse.scout.commons.internal.Activator;
import org.eclipse.scout.commons.serialization.SerializationUtility;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
/**
* Class loader implementation that uses a list of bundles to load classes.
*
* @since 3.8.2
*/
public class BundleListClassLoader extends ClassLoader {
/** table mapping primitive type names to corresponding class objects */
private static final HashMap<String, Class> PRIMITIVE_TYPES;
static {
PRIMITIVE_TYPES = new HashMap<String, Class>(8, 1f);
PRIMITIVE_TYPES.put("boolean", boolean.class);
PRIMITIVE_TYPES.put("byte", byte.class);
PRIMITIVE_TYPES.put("char", char.class);
PRIMITIVE_TYPES.put("short", short.class);
PRIMITIVE_TYPES.put("int", int.class);
PRIMITIVE_TYPES.put("long", long.class);
PRIMITIVE_TYPES.put("float", float.class);
PRIMITIVE_TYPES.put("double", double.class);
PRIMITIVE_TYPES.put("void", void.class);
//
PRIMITIVE_TYPES.put("Z", boolean.class);
PRIMITIVE_TYPES.put("B", byte.class);
PRIMITIVE_TYPES.put("C", char.class);
PRIMITIVE_TYPES.put("S", short.class);
PRIMITIVE_TYPES.put("I", int.class);
PRIMITIVE_TYPES.put("J", long.class);
PRIMITIVE_TYPES.put("F", float.class);
PRIMITIVE_TYPES.put("D", double.class);
PRIMITIVE_TYPES.put("V", void.class);
}
private static final ThreadLocal<Set<String>> LOOP_DETECTOR = new ThreadLocal<Set<String>>();
private static final Enumeration<URL> EMPTY_URL_ENUMERATION = new Enumeration<URL>() {
@Override
public boolean hasMoreElements() {
return false;
}
@Override
public URL nextElement() {
throw new NoSuchElementException();
}
};
private static final String BUNDLE_INCLUDE_FILTER_PROPERTY = "org.eclipse.scout.commons.osgi.BundleListClassLoader#includeBundles";
private static final String BUNDLE_EXCLUDE_FILTER_PROPERTY = "org.eclipse.scout.commons.osgi.BundleListClassLoader#excludeBundles";
private static final String REGEX_MARKER = "regex:";
private static ClassLoader s_myClassLoader;
static {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
s_myClassLoader = BundleListClassLoader.class.getClassLoader();
return null;
}
});
}
private final Bundle[] m_bundles;
private final Bundle[] m_bundlesSortedByBundleSymbolicNameLenght;
private String[] m_bundleOrderPrefixes = null;
private final ClassLoader m_parentContextClassLoader;
private final ReadWriteLock m_cacheLock = new ReentrantReadWriteLock();
private final Map<String, WeakReference<Class<?>>> m_classCache;
private final boolean m_useResourceFiltering;
private final boolean m_useResourceCaching;
private final Map<String, Vector<URL>> m_resourceCache;
public BundleListClassLoader(ClassLoader parent, Bundle... bundles) {
super(parent);
m_parentContextClassLoader = parent != null ? parent : new ClassLoader(Object.class.getClassLoader()) {
// boot classloader
};
if (bundles == null || bundles.length == 0) {
throw new IllegalArgumentException("bundle list must not be null or empty");
}
// filter given list of bundles.
String bundleIncludeFilter = Activator.getDefault().getBundle().getBundleContext().getProperty(BUNDLE_INCLUDE_FILTER_PROPERTY);
String bundleExcludeFilter = Activator.getDefault().getBundle().getBundleContext().getProperty(BUNDLE_EXCLUDE_FILTER_PROPERTY);
Pattern[] bundleIncludePatterns = parseFilterPatterns(bundleIncludeFilter);
Pattern[] bundleExcludePatterns = parseFilterPatterns(bundleExcludeFilter);
List<Bundle> filteredBundleList = new ArrayList<Bundle>();
for (Bundle b : BundleInspector.filterPluginBundles(bundles)) {
if (accept(b.getSymbolicName(), bundleIncludePatterns, bundleExcludePatterns)) {
filteredBundleList.add(b);
}
}
m_bundles = filteredBundleList.toArray(new Bundle[filteredBundleList.size()]);
if (getBundles().length == 0) {
throw new IllegalArgumentException("filtered bundle list must not be empty. [bundles=" + Arrays.toString(bundles) + "]");
}
//
m_bundlesSortedByBundleSymbolicNameLenght = new Bundle[getBundles().length];
System.arraycopy(getBundles(), 0, getBundlesSortedByBundleSymbolicNameLenght(), 0, getBundles().length);
Arrays.sort(getBundlesSortedByBundleSymbolicNameLenght(), new Comparator<Bundle>() {
@Override
public int compare(Bundle b1, Bundle b2) {
if (b1 == null && b2 == null) {
return 0;
}
if (b1 == null) {
return -1;
}
if (b2 == null) {
return 1;
}
return StringUtility.length(b2.getSymbolicName()) - StringUtility.length(b1.getSymbolicName());
}
});
//
m_classCache = new HashMap<String, WeakReference<Class<?>>>();
m_useResourceFiltering = SerializationUtility.isUseBundleOrderPrefixListAsResourceFilterEnabled();
m_useResourceCaching = SerializationUtility.isResourceUrlCachingInBundleListClassLoaderEnabled();
m_resourceCache = new HashMap<String, Vector<URL>>();
}
public Bundle[] getBundles() {
return m_bundles;
}
public Bundle[] getBundlesSortedByBundleSymbolicNameLenght() {
return m_bundlesSortedByBundleSymbolicNameLenght;
}
public ClassLoader getParentContextClassLoader() {
return m_parentContextClassLoader;
}
public boolean isUseResourceFilteringEnabled() {
return m_useResourceFiltering;
}
public boolean isUseResourceCachingEnabled() {
return m_useResourceCaching;
}
protected Class<?> putInCache(String name, Class<?> c) {
m_cacheLock.writeLock().lock();
try {
m_classCache.put(name, new WeakReference<Class<?>>(c));
}
finally {
m_cacheLock.writeLock().unlock();
}
return c;
}
protected URL putInCache(String name, URL resources) {
Vector<URL> urlList = new Vector<URL>();
urlList.add(resources);
urlList = putInCache(name, urlList);
return urlList.firstElement();
}
protected Vector<URL> putInCache(String name, Vector<URL> resources) {
if (isUseResourceCachingEnabled()) {
m_cacheLock.writeLock().lock();
try {
m_resourceCache.put(name, resources);
}
finally {
m_cacheLock.writeLock().unlock();
}
}
return resources;
}
protected Vector<URL> getFromCache(String name) {
if (!isUseResourceCachingEnabled()) {
return null;
}
m_cacheLock.readLock().lock();
try {
Vector<URL> ref = m_resourceCache.get(name);
return ref;
}
finally {
m_cacheLock.readLock().unlock();
}
}
protected void clearCaches() {
m_cacheLock.writeLock().lock();
try {
m_classCache.clear();
m_resourceCache.clear();
}
finally {
m_cacheLock.writeLock().unlock();
}
}
@Override
public Class<?> loadClass(String className) throws ClassNotFoundException {
if (!registerLoadingItem(className)) {
throw new ClassNotFoundException(className);
}
try {
return doLoadClass(className);
}
finally {
unregisterLoadingItem(className);
}
}
protected Class<?> doLoadClass(String className) throws ClassNotFoundException {
// 1. check primitive classes
Class<?> c = PRIMITIVE_TYPES.get(className);
if (c != null) {
return c;
}
// 2. check if class name denotes an array
int arrayDim = 0;
while (className.startsWith("[")) {
className = className.substring(1);
arrayDim++;
}
if (className.matches("L.*;")) {
className = className.substring(1, className.length() - 1);
}
if (arrayDim > 0) {
c = loadClass(className);
int[] dimensions = new int[arrayDim];
c = Array.newInstance(c, dimensions).getClass();
return c;
}
// 3. delegate classes starting with 'java.' to parent class loader
if (className.startsWith("java.")) {
return getParentContextClassLoader().loadClass(className);
}
// 4. check if class is already in the cache
m_cacheLock.readLock().lock();
try {
WeakReference<Class<?>> ref = m_classCache.get(className);
if (ref != null) {
c = ref.get();
if (c != null) {
return c;
}
}
if (m_classCache.containsKey(className)) {
throw new ClassNotFoundException(className);
}
}
finally {
m_cacheLock.readLock().unlock();
}
Set<Bundle> usedBundles = new HashSet<Bundle>();
// 5. search in best matching bundles based on class and bundle symbolic name
for (Bundle bundle : getBundlesSortedByBundleSymbolicNameLenght()) {
if (usedBundles.contains(bundle)) {
continue;
}
if (className.startsWith(bundle.getSymbolicName() + ".")) {
usedBundles.add(bundle);
try {
c = bundle.loadClass(className);
return putInCache(className, c);
}
catch (Exception e) {
//nop
}
}
}
// 6. search in active bundles
for (Bundle bundle : getBundlesSortedByBundleSymbolicNameLenght()) {
if (usedBundles.contains(bundle)) {
continue;
}
if (bundle.getState() == Bundle.ACTIVE) {
usedBundles.add(bundle);
try {
c = bundle.loadClass(className);
return putInCache(className, c);
}
catch (Exception e) {
//nop
}
}
}
// 7. search in remaining bundles
for (Bundle b : getBundles()) {
if (usedBundles.contains(b)) {
continue;
}
try {
c = b.loadClass(className);
return putInCache(className, c);
}
catch (Exception e) {
//nop
}
}
// 8. try context class loader
try {
// do not call super.loadClass because it checks the native cache (see eclipse equinox bug 127963)
c = getParentContextClassLoader().loadClass(className);
return putInCache(className, c);
}
catch (Exception e) {
//nop
}
// 9. class not found
putInCache(className, (Class<?>) null);
throw new ClassNotFoundException(className);
}
@Override
public URL getResource(String name) {
Enumeration<URL> resources = getResources(name);
if (resources != null && resources.hasMoreElements()) {
return resources.nextElement();
}
return null;
}
@Override
public InputStream getResourceAsStream(String name) {
try {
URL u = getResource(name);
if (u != null) {
return u.openStream();
}
}
catch (Exception e) {
//nop
}
return null;
}
@Override
public Enumeration<URL> getResources(String name) {
if (!registerLoadingItem(name)) {
return null;
}
try {
// 1. check if resource is already in the cache
Vector<URL> ref = getFromCache(name);
if (ref != null) {
return ref.elements();
}
// 2. search in bundles
Vector<URL> urlList = searchResourcesInBundles(name);
// 3. filter resources
urlList = filterResources(urlList);
return putInCache(name, urlList).elements();
}
finally {
unregisterLoadingItem(name);
}
}
protected Vector<URL> searchResourcesInBundles(String name) {
Vector<URL> urlList = new Vector<URL>();
for (Bundle b : getBundles()) {
try {
Enumeration resources = b.getResources(name);
while (resources != null && resources.hasMoreElements()) {
URL url = (URL) resources.nextElement();
urlList.add(url);
}
}
catch (Exception e) {
//nop
}
}
return urlList;
}
protected Vector<URL> filterResources(Vector<URL> urlList) {
if (isUseResourceFilteringEnabled()) {
Vector<URL> newUrlList = new Vector<URL>();
Vector<URL> customUrlList = new Vector<URL>();
Enumeration<URL> resources = urlList.elements();
while (resources != null && resources.hasMoreElements()) {
URL resource = resources.nextElement();
newUrlList.add(resource);
if (isUrlFromBundlePrefixes(resource)) {
customUrlList.add(resource);
}
}
if (!customUrlList.isEmpty()) {
urlList = customUrlList;
}
else {
urlList = newUrlList;
}
}
return urlList;
}
protected boolean registerLoadingItem(String name) {
Set<String> loadingItems = LOOP_DETECTOR.get();
if (loadingItems != null && loadingItems.contains(name)) {
return false;
}
if (loadingItems == null) {
loadingItems = new HashSet<String>(3);
LOOP_DETECTOR.set(loadingItems);
}
loadingItems.add(name);
return true;
}
protected void unregisterLoadingItem(String name) {
// invariant: register has already been invoked
LOOP_DETECTOR.get().remove(name);
}
/**
* Checks if the given string is included in the list of include patterns and that it is not excluded by the list of
* exclude patterns. If the include or exclude pattern list is null or empty, the string is assumed to be included and
* not excluded, respectively.
*/
private static boolean accept(String s, Pattern[] includePatterns, Pattern[] excludePatterns) {
if (s == null) {
return false;
}
boolean included = true;
boolean excluded = false;
if (includePatterns != null && includePatterns.length > 0) {
included = false;
for (Pattern p : includePatterns) {
if (p.matcher(s).matches()) {
included = true;
break;
}
}
}
if (included && excludePatterns != null && excludePatterns.length > 0) {
for (Pattern p : excludePatterns) {
if (p.matcher(s).matches()) {
excluded = true;
break;
}
}
}
return included && !excluded;
}
/**
* Parses a comma-separated list of filter patterns. A filter pattern is either a wildcard pattern or a regular
* expression. Latter must be prefixed by <em>regex:</em>
*/
private static Pattern[] parseFilterPatterns(String filter) {
if (filter == null) {
return null;
}
List<Pattern> patterns = new ArrayList<Pattern>();
for (String f : filter.split(",")) {
f = f.trim();
if (f.length() > 0) {
try {
f = toRegexPattern(f);
Pattern pattern = Pattern.compile(f);
patterns.add(pattern);
}
catch (Exception e) {
System.err.println("invalid filter pattern: " + e);
}
}
}
if (patterns.isEmpty()) {
return null;
}
return patterns.toArray(new Pattern[patterns.size()]);
}
/**
* Transforms the given string into a regular expression pattern. The string is assumed to be a wildcard pattern or
* already a regular expression pattern. The latter must be prefixed by <em>regex:</em>.
*/
private static String toRegexPattern(String s) {
if (s == null) {
return null;
}
String pattern = s.trim();
if (pattern.startsWith(REGEX_MARKER)) {
return pattern.substring(REGEX_MARKER.length());
}
pattern = pattern.replaceAll("[.]", "\\\\.");
pattern = pattern.replaceAll("[*]", ".*");
pattern = pattern.replaceAll("[?]", ".");
return pattern;
}
/**
* return true if resource {@link URL} is located in a bundle from the list of bundleOrderPrefixes
*/
protected boolean isUrlFromBundlePrefixes(URL resource) {
if (m_bundleOrderPrefixes == null) {
m_bundleOrderPrefixes = SerializationUtility.getBundleOrderPrefixes();
}
long bundleID = getBundleID(resource.getHost());
if (bundleID >= 0) {
Bundle bundle = getBundle(bundleID);
if (bundle != null) {
for (String bundlePrefix : m_bundleOrderPrefixes) {
if (StringUtility.contains(bundle.getSymbolicName(), bundlePrefix)) {
return true;
}
}
}
}
return false;
}
/**
* find the bundleId in the host string.
* Example: 80.2hwhefh29:3
*
* @param host
* from resource {@link URL}
* @return bundle id
*/
protected long getBundleID(String host) {
int dotIndex = host.indexOf('.');
return (dotIndex >= 0 && dotIndex < host.length() - 1) ? Long.parseLong(host.substring(0, dotIndex)) : -1;
}
/**
* find the {@link Bundle} from a bundle id
*
* @param id
* bundle id
* @return the corresponding {@link Bundle}
*/
protected Bundle getBundle(long id) {
BundleContext bundleContext = FrameworkUtil.getBundle(this.getClass()).getBundleContext();
Bundle result = null;
for (Bundle candidate : bundleContext.getBundles()) {
if (candidate.getBundleId() == id) {
if (result == null || result.getVersion().compareTo(candidate.getVersion()) < 0) {
result = candidate;
}
}
}
return result;
}
}