blob: ada908166820b98f3939faf5239a94b72dbdf413 [file] [log] [blame]
/*
* Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.jersey.internal;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.ProcessingException;
import org.glassfish.jersey.internal.util.ReflectionHelper;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleReference;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.SynchronousBundleListener;
/**
* Utility class to deal with OSGi runtime specific behavior.
* This is mainly to handle META-INF/services lookup
* and generic/application class lookup issue in OSGi.
*
* When OSGi runtime is detected by the {@link ServiceFinder} class,
* an instance of OsgiRegistry is created and associated with given
* OSGi BundleContext. META-INF/services entries are then being accessed
* via the OSGi Bundle API as direct ClassLoader#getResource() method invocation
* does not work in this case within OSGi.
*
* @author Jakub Podlesak
* @author Adam Lindenthal
*/
public final class OsgiRegistry implements SynchronousBundleListener {
private static final String WEB_INF_CLASSES = "WEB-INF/classes/";
private static final String CoreBundleSymbolicNAME = "org.glassfish.jersey.core.jersey-common";
private static final Logger LOGGER = Logger.getLogger(OsgiRegistry.class.getName());
private final BundleContext bundleContext;
private final Map<Long, Map<String, Callable<List<Class<?>>>>> factories =
new HashMap<Long, Map<String, Callable<List<Class<?>>>>>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private static OsgiRegistry instance;
private final Map<String, Bundle> classToBundleMapping = new HashMap<String, Bundle>();
/**
* Returns an {@code OsgiRegistry} instance. Call this method only if sure that the application is running in OSGi
* environment, otherwise a call to this method can lead to an {@link ClassNotFoundException}.
*
* @return an {@code OsgiRegistry} instance.
*/
public static synchronized OsgiRegistry getInstance() {
if (instance == null) {
final ClassLoader classLoader = AccessController
.doPrivileged(ReflectionHelper.getClassLoaderPA(ReflectionHelper.class));
if (classLoader instanceof BundleReference) {
final BundleContext context = FrameworkUtil.getBundle(OsgiRegistry.class).getBundleContext();
if (context != null) { // context could be still null if the current bundle has not been started
instance = new OsgiRegistry(context);
}
}
}
return instance;
}
private final class OsgiServiceFinder extends ServiceFinder.ServiceIteratorProvider {
final ServiceFinder.ServiceIteratorProvider defaultIterator = new ServiceFinder.DefaultServiceIteratorProvider();
@Override
public <T> Iterator<T> createIterator(
final Class<T> serviceClass,
final String serviceName,
final ClassLoader loader,
final boolean ignoreOnClassNotFound) {
final List<Class<?>> providerClasses = locateAllProviders(serviceName);
if (!providerClasses.isEmpty()) {
return new Iterator<T>() {
Iterator<Class<?>> it = providerClasses.iterator();
@Override
public boolean hasNext() {
return it.hasNext();
}
@SuppressWarnings("unchecked")
@Override
public T next() {
final Class<T> nextClass = (Class<T>) it.next();
try {
return nextClass.newInstance();
} catch (final Exception ex) {
final ServiceConfigurationError sce = new ServiceConfigurationError(serviceName + ": "
+ LocalizationMessages.PROVIDER_COULD_NOT_BE_CREATED(
nextClass.getName(), serviceClass, ex.getLocalizedMessage()));
sce.initCause(ex);
throw sce;
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
return defaultIterator.createIterator(serviceClass, serviceName, loader, ignoreOnClassNotFound);
}
@Override
public <T> Iterator<Class<T>> createClassIterator(
final Class<T> service, final String serviceName, final ClassLoader loader, final boolean ignoreOnClassNotFound) {
final List<Class<?>> providerClasses = locateAllProviders(serviceName);
if (!providerClasses.isEmpty()) {
return new Iterator<Class<T>>() {
Iterator<Class<?>> it = providerClasses.iterator();
@Override
public boolean hasNext() {
return it.hasNext();
}
@SuppressWarnings("unchecked")
@Override
public Class<T> next() {
return (Class<T>) it.next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
return defaultIterator.createClassIterator(service, serviceName, loader, ignoreOnClassNotFound);
}
}
private static class BundleSpiProvidersLoader implements Callable<List<Class<?>>> {
private final String spi;
private final URL spiRegistryUrl;
private final String spiRegistryUrlString;
private final Bundle bundle;
BundleSpiProvidersLoader(final String spi, final URL spiRegistryUrl, final Bundle bundle) {
this.spi = spi;
this.spiRegistryUrl = spiRegistryUrl;
this.spiRegistryUrlString = spiRegistryUrl.toExternalForm();
this.bundle = bundle;
}
@Override
public List<Class<?>> call() throws Exception {
BufferedReader reader = null;
try {
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.FINEST, "Loading providers for SPI: {0}", spi);
}
reader = new BufferedReader(new InputStreamReader(spiRegistryUrl.openStream(), "UTF-8"));
String providerClassName;
final List<Class<?>> providerClasses = new ArrayList<Class<?>>();
while ((providerClassName = reader.readLine()) != null) {
if (providerClassName.trim().length() == 0) {
continue;
}
if (providerClassName.startsWith("#")) {
continue;
}
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.FINEST, "SPI provider: {0}", providerClassName);
}
providerClasses.add(loadClass(bundle, providerClassName));
}
return providerClasses;
} catch (final Exception e) {
LOGGER.log(Level.WARNING, LocalizationMessages.EXCEPTION_CAUGHT_WHILE_LOADING_SPI_PROVIDERS(), e);
throw e;
} catch (final Error e) {
LOGGER.log(Level.WARNING, LocalizationMessages.ERROR_CAUGHT_WHILE_LOADING_SPI_PROVIDERS(), e);
throw e;
} finally {
if (reader != null) {
try {
reader.close();
} catch (final IOException ioe) {
LOGGER.log(Level.FINE, "Error closing SPI registry stream:" + spiRegistryUrl, ioe);
}
}
}
}
@Override
public String toString() {
return spiRegistryUrlString;
}
@Override
public int hashCode() {
return spiRegistryUrlString.hashCode();
}
@Override
public boolean equals(final Object obj) {
if (obj instanceof BundleSpiProvidersLoader) {
return spiRegistryUrlString.equals(((BundleSpiProvidersLoader) obj).spiRegistryUrlString);
} else {
return false;
}
}
}
@Override
public void bundleChanged(final BundleEvent event) {
if (event.getType() == BundleEvent.RESOLVED) {
register(event.getBundle());
} else if (event.getType() == BundleEvent.UNRESOLVED || event.getType() == BundleEvent.UNINSTALLED) {
final Bundle unregisteredBundle = event.getBundle();
lock.writeLock().lock();
try {
factories.remove(unregisteredBundle.getBundleId());
if (unregisteredBundle.getSymbolicName().equals(CoreBundleSymbolicNAME)) {
bundleContext.removeBundleListener(this);
factories.clear();
}
} finally {
lock.writeLock().unlock();
}
}
}
/**
* Translates bundle entry path as returned from {@link org.osgi.framework.Bundle#findEntries(String, String, boolean)} to
* fully qualified class name that resides in given package path (directly or indirectly in its subpackages).
*
* @param packagePath The package path where the class is located (even recursively)
* @param bundleEntryPath The bundle path to translate.
* @return Fully qualified class name.
*/
public static String bundleEntryPathToClassName(String packagePath, String bundleEntryPath) {
// normalize packagePath
packagePath = normalizedPackagePath(packagePath);
// remove WEB-INF/classes from bundle entry path
if (bundleEntryPath.contains(WEB_INF_CLASSES)) {
bundleEntryPath = bundleEntryPath.substring(bundleEntryPath.indexOf(WEB_INF_CLASSES) + WEB_INF_CLASSES.length());
}
final int packageIndex = bundleEntryPath.indexOf(packagePath);
String normalizedClassNamePath = packageIndex > -1
// the package path was found in the bundle path
? bundleEntryPath.substring(packageIndex)
// the package path is not included in the bundle entry path
// fall back to the original implementation of the translation which does not consider recursion
: packagePath + bundleEntryPath.substring(bundleEntryPath.lastIndexOf('/') + 1);
return (normalizedClassNamePath.startsWith("/") ? normalizedClassNamePath.substring(1) : normalizedClassNamePath)
.replace(".class", "").replace('/', '.');
}
/**
* Returns whether the given entry path is located directly in the provided package path. That is,
* if the entry is located in a sub-package, then {@code false} is returned.
*
* @param packagePath Package path which the entry is compared to
* @param entryPath Entry path
* @return Whether the given entry path is located directly in the provided package path.
*/
public static boolean isPackageLevelEntry(String packagePath, final String entryPath) {
// normalize packagePath
packagePath = normalizedPackagePath(packagePath);
// if the package path is contained in the jar entry name, subtract it
String entryWithoutPackagePath = entryPath.contains(packagePath)
? entryPath.substring(entryPath.indexOf(packagePath) + packagePath.length())
: entryPath;
return !(entryWithoutPackagePath.startsWith("/") ? entryWithoutPackagePath.substring(1)
: entryWithoutPackagePath)
.contains("/");
}
/**
* Normalized package returns path that does not start with '/' character and ends with '/' character.
* If the argument is '/' then returned value is empty string "".
*
* @param packagePath package path to normalize.
* @return Normalized package path.
*/
public static String normalizedPackagePath(String packagePath) {
packagePath = packagePath.startsWith("/") ? packagePath.substring(1) : packagePath;
packagePath = packagePath.endsWith("/") ? packagePath : packagePath + "/";
packagePath = "/".equals(packagePath) ? "" : packagePath;
return packagePath;
}
/**
* Get URLs of resources from a given package.
*
* @param packagePath package.
* @param classLoader resource class loader.
* @param recursive whether the given package path should be scanned recursively by OSGi
* @return URLs of the located resources.
*/
@SuppressWarnings("unchecked")
public Enumeration<URL> getPackageResources(final String packagePath,
final ClassLoader classLoader,
final boolean recursive) {
final List<URL> result = new LinkedList<URL>();
for (final Bundle bundle : bundleContext.getBundles()) {
// Look for resources at the given <packagePath> and at WEB-INF/classes/<packagePath> in case a WAR is being examined.
for (final String bundlePackagePath : new String[] {packagePath, WEB_INF_CLASSES + packagePath}) {
final Enumeration<URL> enumeration = findEntries(bundle, bundlePackagePath, "*.class", recursive);
if (enumeration != null) {
while (enumeration.hasMoreElements()) {
final URL url = enumeration.nextElement();
final String path = url.getPath();
classToBundleMapping.put(bundleEntryPathToClassName(packagePath, path), bundle);
result.add(url);
}
}
}
// Now interested only in .jar provided by current bundle.
final Enumeration<URL> jars = findEntries(bundle, "/", "*.jar", true);
if (jars != null) {
while (jars.hasMoreElements()) {
final URL jar = jars.nextElement();
final InputStream inputStream = classLoader.getResourceAsStream(jar.getPath());
if (inputStream == null) {
LOGGER.config(LocalizationMessages.OSGI_REGISTRY_ERROR_OPENING_RESOURCE_STREAM(jar));
continue;
}
final JarInputStream jarInputStream;
try {
jarInputStream = new JarInputStream(inputStream);
} catch (final IOException ex) {
LOGGER.log(Level.CONFIG, LocalizationMessages.OSGI_REGISTRY_ERROR_PROCESSING_RESOURCE_STREAM(jar), ex);
try {
inputStream.close();
} catch (final IOException e) {
// ignored
}
continue;
}
try {
JarEntry jarEntry;
while ((jarEntry = jarInputStream.getNextJarEntry()) != null) {
final String jarEntryName = jarEntry.getName();
final String jarEntryNameLeadingSlash = jarEntryName.startsWith("/")
? jarEntryName : "/" + jarEntryName;
if (jarEntryName.endsWith(".class")
// Added leading and trailing slashes '/' to package path (e.g. '/com/') helps us to not
// accidentally match sub-strings of the package path (e.g., if package path 'com' was used
// for scanning, package 'whatever.foo.telecom' would be matched because of word 'tele[com]').
// Note that we cannot avoid all corner cases with accidental matches since jar
// entry name might be almost anything (e.g., if package path 'telecom' was used, package
// 'whatever.foo.telecom' will be matched and there is no way to avoid it unless user
// explicitly instructs us to do so somehow (not implemented)
&& jarEntryNameLeadingSlash.contains("/" + normalizedPackagePath(packagePath))) {
if (!recursive && !isPackageLevelEntry(packagePath, jarEntryName)) {
continue;
}
classToBundleMapping.put(jarEntryName.replace(".class", "").replace('/', '.'), bundle);
result.add(bundle.getResource(jarEntryName));
}
}
} catch (final Exception ex) {
LOGGER.log(Level.CONFIG, LocalizationMessages.OSGI_REGISTRY_ERROR_PROCESSING_RESOURCE_STREAM(jar), ex);
} finally {
try {
jarInputStream.close();
} catch (final IOException e) {
// ignored
}
}
}
}
}
return Collections.enumeration(result);
}
/**
* Get the Class from the class name.
* <p>
* The context class loader will be utilized if accessible and non-null.
* Otherwise the defining class loader of this class will
* be utilized.
*
* @param className the class name.
* @return the Class, otherwise null if the class cannot be found.
* @throws ClassNotFoundException if the class cannot be found.
*/
public Class<?> classForNameWithException(final String className) throws ClassNotFoundException {
final Bundle bundle = classToBundleMapping.get(className);
if (bundle == null) {
throw new ClassNotFoundException(className);
}
return loadClass(bundle, className);
}
/**
* Tries to load resource bundle via OSGi means. No caching involved here,
* as localization properties are being cached in Localizer class already.
*
* @param bundleName name of the resource bundle to load
* @return resource bundle instance if found, null otherwise
*/
public ResourceBundle getResourceBundle(final String bundleName) {
final int lastDotIndex = bundleName.lastIndexOf('.');
final String path = bundleName.substring(0, lastDotIndex).replace('.', '/');
final String propertiesName = bundleName.substring(lastDotIndex + 1, bundleName.length()) + ".properties";
for (final Bundle bundle : bundleContext.getBundles()) {
final Enumeration<URL> entries = findEntries(bundle, path, propertiesName, false);
if (entries != null && entries.hasMoreElements()) {
final URL entryUrl = entries.nextElement();
try {
return new PropertyResourceBundle(entryUrl.openStream());
} catch (final IOException ex) {
if (LOGGER.isLoggable(Level.FINE)) {
// does not make sense to localize this
LOGGER.fine("Exception caught when tried to load resource bundle in OSGi");
}
return null;
}
}
}
return null;
}
/**
* Creates a new OsgiRegistry instance bound to a particular OSGi runtime.
* The only parameter must be an instance of a {@link BundleContext}.
*
* @param bundleContext must be a non-null instance of a BundleContext
*/
private OsgiRegistry(final BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
/**
* Will hook up this instance with the OSGi runtime.
* This is to actually update SPI provider lookup and class loading mechanisms in Jersey
* to utilize OSGi features.
*/
void hookUp() {
setOSGiServiceFinderIteratorProvider();
bundleContext.addBundleListener(this);
registerExistingBundles();
}
private void registerExistingBundles() {
for (final Bundle bundle : bundleContext.getBundles()) {
if (bundle.getState() == Bundle.RESOLVED || bundle.getState() == Bundle.STARTING
|| bundle.getState() == Bundle.ACTIVE || bundle.getState() == Bundle.STOPPING) {
register(bundle);
}
}
}
private void setOSGiServiceFinderIteratorProvider() {
ServiceFinder.setIteratorProvider(new OsgiServiceFinder());
}
private void register(final Bundle bundle) {
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.FINEST, "checking bundle {0}", bundle.getBundleId());
}
Map<String, Callable<List<Class<?>>>> map;
lock.writeLock().lock();
try {
map = factories.get(bundle.getBundleId());
if (map == null) {
map = new ConcurrentHashMap<String, Callable<List<Class<?>>>>();
factories.put(bundle.getBundleId(), map);
}
} finally {
lock.writeLock().unlock();
}
final Enumeration<URL> e = findEntries(bundle, "META-INF/services/", "*", false);
if (e != null) {
while (e.hasMoreElements()) {
final URL u = e.nextElement();
final String url = u.toString();
if (url.endsWith("/")) {
continue;
}
final String factoryId = url.substring(url.lastIndexOf("/") + 1);
map.put(factoryId, new BundleSpiProvidersLoader(factoryId, u, bundle));
}
}
}
private List<Class<?>> locateAllProviders(final String serviceName) {
lock.readLock().lock();
try {
final List<Class<?>> result = new LinkedList<Class<?>>();
for (final Map<String, Callable<List<Class<?>>>> value : factories.values()) {
if (value.containsKey(serviceName)) {
try {
result.addAll(value.get(serviceName).call());
} catch (final Exception ex) {
// ignore
}
}
}
return result;
} finally {
lock.readLock().unlock();
}
}
private static Class<?> loadClass(final Bundle bundle, final String className) throws ClassNotFoundException {
try {
return AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() {
@Override
public Class<?> run() throws ClassNotFoundException {
return bundle.loadClass(className);
}
});
} catch (final PrivilegedActionException ex) {
final Exception originalException = ex.getException();
if (originalException instanceof ClassNotFoundException) {
throw (ClassNotFoundException) originalException;
} else if (originalException instanceof RuntimeException) {
throw (RuntimeException) originalException;
} else {
throw new ProcessingException(originalException);
}
}
}
private static Enumeration<URL> findEntries(final Bundle bundle,
final String path,
final String fileNamePattern,
final boolean recursive) {
return AccessController.doPrivileged(new PrivilegedAction<Enumeration<URL>>() {
@SuppressWarnings("unchecked")
@Override
public Enumeration<URL> run() {
return bundle.findEntries(path, fileNamePattern, recursive);
}
});
}
}