| /* |
| * Copyright 2016 The Apache Software Foundation. |
| * |
| * Licensed 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.brooklyn.util.core; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static org.apache.brooklyn.core.catalog.internal.CatalogUtils.newClassLoadingContextForCatalogItems; |
| |
| import java.net.URL; |
| import java.util.List; |
| import java.util.regex.Pattern; |
| |
| import javax.annotation.Nullable; |
| |
| import org.apache.brooklyn.api.catalog.BrooklynCatalog; |
| import org.apache.brooklyn.api.catalog.CatalogItem; |
| import org.apache.brooklyn.api.entity.Entity; |
| import org.apache.brooklyn.api.mgmt.ManagementContext; |
| import org.apache.brooklyn.api.typereg.OsgiBundleWithUrl; |
| import org.apache.brooklyn.api.typereg.RegisteredType; |
| import org.apache.brooklyn.core.BrooklynVersion; |
| import org.apache.brooklyn.core.catalog.internal.BasicBrooklynCatalog; |
| import org.apache.brooklyn.core.catalog.internal.CatalogUtils; |
| import org.apache.brooklyn.core.entity.EntityInternal; |
| import org.apache.brooklyn.core.mgmt.classloading.BrooklynClassLoadingContextSequential; |
| import org.apache.brooklyn.core.mgmt.ha.OsgiManager; |
| import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; |
| import org.apache.brooklyn.core.typereg.BundleUpgradeParser.CatalogUpgrades; |
| import org.apache.brooklyn.util.collections.MutableList; |
| import org.apache.brooklyn.util.core.LoaderDispatcher.ClassLoaderDispatcher; |
| import org.apache.brooklyn.util.core.LoaderDispatcher.MultipleResourceLoaderDispatcher; |
| import org.apache.brooklyn.util.core.LoaderDispatcher.ResourceLoaderDispatcher; |
| import org.apache.brooklyn.util.core.osgi.Osgis; |
| import org.apache.brooklyn.util.exceptions.Exceptions; |
| import org.apache.brooklyn.util.guava.Maybe; |
| import org.apache.brooklyn.util.osgi.VersionedName; |
| import org.apache.brooklyn.util.text.Strings; |
| import org.osgi.framework.Bundle; |
| import org.osgi.framework.BundleContext; |
| import org.osgi.framework.FrameworkUtil; |
| import org.osgi.framework.launch.Framework; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import com.google.common.annotations.Beta; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.MoreObjects; |
| import com.google.common.base.Objects; |
| import com.google.common.base.Predicate; |
| import com.google.common.collect.ImmutableList; |
| |
| public class ClassLoaderUtils { |
| |
| private static final Logger log = LoggerFactory.getLogger(ClassLoaderUtils.class); |
| |
| /** |
| * White list format should be |
| * <bundle symbolic name regex>[:<bundle version regex in dotted format (X.X.X.SNAPSHOT, instead of X.X.X-SNAPSHOT)>] |
| */ |
| static final String WHITE_LIST_KEY = "org.apache.brooklyn.classloader.fallback.bundles"; |
| static final String CLASS_NAME_DELIMITER = ":"; |
| private static final String WHITE_LIST_DEFAULT = |
| "org\\.apache\\.brooklyn\\..*:" + BrooklynVersion.getOsgiVersion(); |
| |
| // Class.forName gets the class loader from the calling class. |
| // We don't have access to the same reflection API so need to pass it explicitly. |
| private final ClassLoader classLoader; |
| private final Entity entity; |
| private final ManagementContext mgmt; |
| |
| public ClassLoaderUtils(Object callingObj, Entity entity) { |
| this(callingObj.getClass(), entity); |
| } |
| public ClassLoaderUtils(Object callingObj, @Nullable ManagementContext mgmt) { |
| this(callingObj.getClass(), mgmt); |
| } |
| |
| public ClassLoaderUtils(Class<?> callingClass) { |
| checkNotNull(callingClass, "callingClass"); |
| this.classLoader = getValidClassLoader(callingClass.getClassLoader()); |
| this.entity = null; |
| this.mgmt = null; |
| } |
| |
| public ClassLoaderUtils(@Nullable ClassLoader cl) { |
| this.classLoader = getValidClassLoader(cl); |
| this.entity = null; |
| this.mgmt = null; |
| } |
| |
| public ClassLoaderUtils(@Nullable ClassLoader cl, @Nullable ManagementContext mgmt) { |
| this.classLoader = getValidClassLoader(cl); |
| this.entity = null; |
| this.mgmt = mgmt; |
| } |
| |
| public ClassLoaderUtils(Class<?> callingClass, Entity entity) { |
| checkNotNull(callingClass, "callingClass"); |
| this.classLoader = getValidClassLoader(callingClass.getClassLoader()); |
| this.entity = checkNotNull(entity, "entity"); |
| this.mgmt = ((EntityInternal)entity).getManagementContext(); |
| } |
| |
| public ClassLoaderUtils(Class<?> callingClass, @Nullable ManagementContext mgmt) { |
| checkNotNull(callingClass, "callingClass"); |
| this.classLoader = getValidClassLoader(callingClass.getClassLoader()); |
| this.entity = null; |
| this.mgmt = mgmt; |
| } |
| |
| // class.getClassLoader() could return null for classes on the boot class path, |
| // provide an alternative in this case. |
| protected ClassLoader getValidClassLoader(ClassLoader cl) { |
| return (cl != null) ? cl : getClass().getClassLoader(); |
| } |
| |
| /** |
| * Loads the given class, handle OSGi bundles. The class could be in one of the following formats: |
| * <ul> |
| * <li>{@code <classname>}, such as {@code com.google.common.net.HostAndPort} |
| * <li>{@code <bunde-symbolicName>:<classname>}, such as {@code com.google.guava:com.google.common.net.HostAndPort} |
| * <li>{@code <bunde-symbolicName>:<bundle-version>:<classname>}, such as |
| * {@code com.google.guava:16.0.1:com.google.common.net.HostAndPort} |
| * </ul> |
| * |
| * The classloading order is as follows: |
| * <ol> |
| * <li>If the class explicitly states the bundle name and version, then load from that. |
| * <li>Otherwise try to load from the catalog's classloader. This is so we respect any |
| * {@code libraries} supplied in the catalog metadata, and can thus handle updating |
| * catalog versions. It also means we can try our best to handle a catalog that |
| * uses a different bundle version from something that ships with Brooklyn. |
| * <li>The white-listed bundles (i.e. those that ship with Brooklyn). We prefer the |
| * version of the bundle that Brooklyn depends on, rather than taking the highest |
| * version installed (e.g. Karaf has Guava 16.0.1 and 18.0; we want the former, which |
| * Brooklyn uses). |
| * <li>The classloader passed in. Normally this is a boring unhelpful classloader (e.g. |
| * obtained from {@code callingClass.getClassLoader()}), so won't work. But it's up |
| * to the caller if they pass in something more useful. |
| * <li>The {@link ManagementContext#getCatalogClassLoader()}. Again, this is normally not helpful. |
| * We instead would prefer the specific catalog item's classloader (which we tried earlier). |
| * <li>If we were given a bundle name without a version, then finally try just using the |
| * most recent version of the bundle that is available in the OSGi container. |
| * </ol> |
| * |
| * The list of "white-listed bundles" are controlled using the system property named |
| * {@link #WHITE_LIST_KEY}, defaulting to all {@code org.apache.brooklyn.*} bundles. |
| */ |
| public Class<?> loadClass(String name) throws ClassNotFoundException { |
| Maybe<Class<?>> cls = load(name, ClassLoaderDispatcher.INSTANCE); |
| if (cls.isPresent()) { |
| return cls.get(); |
| } else { |
| throw new ClassNotFoundException("Class " + name + |
| " not found on the application class path, nor in the bundle white list.", getReturnException(cls)); |
| } |
| } |
| |
| public Class<?> loadClass(String symbolicName, @Nullable String version, String className) throws ClassNotFoundException { |
| try { |
| return tryLoadFromBundle(ClassLoaderDispatcher.INSTANCE, symbolicName, version, className).get(); |
| } catch (IllegalStateException e) { |
| throw new ClassNotFoundException("Class " + className + " could not be loaded from bundle " |
| + toBundleString(symbolicName, version), e); |
| } |
| } |
| |
| /** |
| * Finds the resource with the given name. |
| * @see {@link #loadClass(String)} for loading order |
| * |
| * @return null when no resource is found |
| */ |
| public URL getResource(String name) { |
| return load(stripLeadingSlash(name), ResourceLoaderDispatcher.INSTANCE).orNull(); |
| } |
| |
| /** |
| * Finds all the resources with the given name. |
| * Aborts going through subsequent fallbacks when it finds at least one resource. |
| * @see {@link #loadClass(String)} for loading order |
| * |
| * @return empty {@link Iterable} when no resources find |
| */ |
| public Iterable<URL> getResources(String name) { |
| Maybe<Iterable<URL>> ret = load(stripLeadingSlash(name), MultipleResourceLoaderDispatcher.INSTANCE); |
| if (ret.isPresent()) { |
| return ret.get(); |
| } else { |
| return ImmutableList.of(); |
| } |
| } |
| |
| protected <T> Maybe<T> load(String name, LoaderDispatcher<T> dispatcher) { |
| String symbolicName; |
| String version; |
| String className; |
| |
| if (looksLikeBundledClassName(name)) { |
| String[] arr = name.split(CLASS_NAME_DELIMITER); |
| if (arr.length > 3) { |
| throw new IllegalStateException("'" + name + |
| "' doesn't look like a class name and contains too many colons to be parsed as bundle:version:class triple."); |
| } else if (arr.length == 3) { |
| symbolicName = arr[0]; |
| version = arr[1]; |
| className = arr[2]; |
| } else if (arr.length == 2) { |
| symbolicName = arr[0]; |
| version = null; |
| className = arr[1]; |
| } else { |
| throw new IllegalStateException("'" + name + |
| "' contains a bundle:version:class delimiter, but only one of those specified"); |
| } |
| } else { |
| symbolicName = null; |
| version = null; |
| className = name; |
| } |
| |
| Maybe<T> cls; |
| if (symbolicName != null && version != null) { |
| // Very explicit; do as we're told! |
| return tryLoadFromBundle(dispatcher, symbolicName, version, className); |
| } |
| |
| if (symbolicName != null) { |
| cls = tryLoadFromBundle(dispatcher, symbolicName, version, className); |
| if (cls.isPresent()) { |
| return cls; |
| } |
| Maybe<T> result = loadClass(name, dispatcher, className); |
| |
| String notFoundWithSymbolicNameMessage = "No class '"+className+"' in bundle '"+symbolicName+"' (using spec '"+name+"')"; |
| if (result.isPresent()) { |
| log.warn(notFoundWithSymbolicNameMessage+"; found using deprecated no-bundle syntax, but this behaviour is deprecated and likely to be unsupported in future. Change so invalid bundle is not supplied."); |
| } else { |
| return Maybe.absent(notFoundWithSymbolicNameMessage); |
| } |
| } |
| |
| return loadClass(name, dispatcher, className); |
| } |
| |
| private <T> Maybe<T> loadClass(String name, LoaderDispatcher<T> dispatcher, String className) { |
| Maybe<T> cls; |
| if (entity != null && mgmt != null) { |
| String catalogItemId = entity.getCatalogItemId(); |
| |
| if (catalogItemId != null) { |
| { |
| BrooklynClassLoadingContextSequential loader = new BrooklynClassLoadingContextSequential(mgmt); |
| loader.add( newClassLoadingContextForCatalogItems(mgmt, catalogItemId, |
| entity.getCatalogItemIdSearchPath()) ); |
| cls = dispatcher.tryLoadFrom(loader, className); |
| if (cls.isPresent()) { |
| return cls; |
| } |
| } |
| // the above (entity) should be sufficient? |
| |
| RegisteredType type = mgmt.getTypeRegistry().get(catalogItemId); |
| if (type != null) { |
| BrooklynClassLoadingContextSequential loader = new BrooklynClassLoadingContextSequential(mgmt); |
| List<String> libs = MutableList.of(); |
| for (OsgiBundleWithUrl o: type.getLibraries()) libs.add(o.getVersionedName().toString()); |
| loader.add( newClassLoadingContextForCatalogItems(mgmt, type.getContainingBundle(), libs) ); |
| cls = dispatcher.tryLoadFrom(loader, className); |
| if (cls.isPresent()) { |
| return cls; |
| } |
| } |
| |
| CatalogItem<?, ?> item = CatalogUtils.getCatalogItemOptionalVersion(mgmt, catalogItemId); |
| if (item != null) { |
| BrooklynClassLoadingContextSequential loader = new BrooklynClassLoadingContextSequential(mgmt); |
| loader.add(newClassLoadingContextForCatalogItems(mgmt, item.getCatalogItemId(), |
| item.getCatalogItemIdSearchPath())); |
| cls = dispatcher.tryLoadFrom(loader, className); |
| if (cls.isPresent()) { |
| return cls; |
| } |
| |
| } else { |
| log.warn("Entity " + entity + " refers to non-existent catalog item " + catalogItemId + |
| ". Trying to load class " + name); |
| } |
| } |
| } |
| |
| cls = tryLoadFromBundleWhiteList(dispatcher, className); |
| if (cls.isPresent()) { |
| return cls; |
| } |
| |
| cls = dispatcher.tryLoadFrom(classLoader, className); |
| if (cls.isPresent()) { |
| return cls; |
| } |
| |
| if (mgmt != null) { |
| cls = dispatcher.tryLoadFrom(mgmt.getCatalogClassLoader(), className); |
| if (cls.isPresent()) { |
| return cls; |
| } |
| } |
| |
| return Maybe.absentNull(); |
| } |
| |
| protected <T> Maybe<T> tryLoadFromBundle(LoaderDispatcher<T> dispatcher, String symbolicName, String version, |
| String name) { |
| Framework framework = getFramework(); |
| if (framework != null) { |
| Maybe<Bundle> bundle = Osgis.bundleFinder(framework) |
| .symbolicName(symbolicName) |
| .version(version) |
| .find(); |
| if (bundle.isAbsent()) { |
| String requestedV = symbolicName+":"+(Strings.isBlank(version) ? BrooklynCatalog.DEFAULT_VERSION : version); |
| String upgradedV = CatalogUpgrades.getBundleUpgradedIfNecessary(mgmt, requestedV); |
| if (!Objects.equal(upgradedV, requestedV)) { |
| log.debug("Upgraded access to bundle "+requestedV+" for loading "+name+" to use bundle "+upgradedV); |
| bundle = Osgis.bundleFinder(framework) |
| .id(upgradedV) |
| .find(); |
| } |
| if (bundle.isAbsent()) { |
| throw new IllegalStateException("Bundle " + toBundleString(symbolicName, version) |
| + " not found to load " + name); |
| } |
| } |
| return dispatcher.tryLoadFrom(bundle.get(), name); |
| } else { |
| Maybe<T> result = dispatcher.tryLoadFrom(classLoader, name); |
| if (result.isAbsent()) { |
| if (name==null || name.startsWith("//") || symbolicName==null || "classpath".equals(symbolicName) || "http".equals(symbolicName) || "https".equals(symbolicName) || "file".equals(symbolicName)) { |
| // this is called speculatively by BasicBrooklynCatalog.PlanInterpreterGuessingType so can log a lot of warnings where URLs are passed |
| if (log.isTraceEnabled()) { |
| log.trace("Request for bundle '"+symbolicName+"' "+(Strings.isNonBlank(version) ? "("+version+") " : "")+"was ignored as no framework available; and failed to find '"+name+"' in plain old classpath"); |
| } |
| } else { |
| // TODO not sure warning is appropriate, but won't hide that in all cases yet |
| log.warn("Request for bundle '"+symbolicName+"' "+(Strings.isNonBlank(version) ? "("+version+") " : "")+"was ignored as no framework available; and failed to find '"+name+"' in plain old classpath"); |
| } |
| } |
| return result; |
| } |
| } |
| |
| @Beta |
| public boolean isBundleWhiteListed(Bundle bundle) { |
| WhiteListBundlePredicate p = createBundleMatchingPredicate(); |
| return p.apply(bundle); |
| } |
| |
| private Framework getFramework() { |
| if (mgmt != null) { |
| Maybe<OsgiManager> osgiManager = ((ManagementContextInternal)mgmt).getOsgiManager(); |
| if (osgiManager.isPresent()) { |
| OsgiManager osgi = osgiManager.get(); |
| return osgi.getFramework(); |
| } |
| } |
| |
| // Requires that caller code is executed AFTER loading bundle brooklyn-core |
| Bundle bundle = FrameworkUtil.getBundle(ClassLoaderUtils.class); |
| if (bundle != null) { |
| BundleContext bundleContext = bundle.getBundleContext(); |
| return (Framework) bundleContext.getBundle(0); |
| } else { |
| return null; |
| } |
| } |
| |
| private boolean looksLikeBundledClassName(String name) { |
| return name.indexOf(CLASS_NAME_DELIMITER) != -1; |
| } |
| |
| |
| protected static class WhiteListBundlePredicate implements Predicate<Bundle> { |
| private final Pattern symbolicName; |
| private final Pattern version; |
| |
| private WhiteListBundlePredicate(String symbolicName, String version) { |
| this.symbolicName = Pattern.compile(checkNotNull(symbolicName, "symbolicName")); |
| this.version = version != null ? Pattern.compile(version) : null; |
| } |
| |
| @Override |
| public boolean apply(Bundle input) { |
| return input.getSymbolicName() != null && input.getVersion() != null && |
| symbolicName.matcher(input.getSymbolicName()).matches() && |
| (version == null || version.matcher(input.getVersion().toString()).matches()); |
| } |
| } |
| |
| protected <T> Maybe<T> tryLoadFromBundleWhiteList(LoaderDispatcher<T> dispatcher, String className) { |
| Framework framework = getFramework(); |
| if (framework == null) { |
| return Maybe.absentNull(); |
| } |
| List<Bundle> bundles = Osgis.bundleFinder(framework) |
| .satisfying(createBundleMatchingPredicate()) |
| .findAll(); |
| for (Bundle b : bundles) { |
| Maybe<T> item = dispatcher.tryLoadFrom(b, className); |
| if (item.isPresent()) { |
| return item; |
| } |
| } |
| return Maybe.absentNull(); |
| } |
| |
| protected WhiteListBundlePredicate createBundleMatchingPredicate() { |
| String whiteList = System.getProperty(WHITE_LIST_KEY, WHITE_LIST_DEFAULT); |
| String[] arr = whiteList.split(":"); |
| String symbolicName = arr[0]; |
| String version = null; |
| if (arr.length > 2) { |
| throw new IllegalStateException("Class loading fallback bundle white list '" + whiteList + |
| "' not in the expected format <symbolic name regex>[:<version regex>]."); |
| } else if (arr.length == 2) { |
| version = arr[1]; |
| } |
| return new WhiteListBundlePredicate(symbolicName, version); |
| } |
| |
| private String toBundleString(String symbolicName, String version) { |
| return symbolicName + ":" + (version != null ? version : "any"); |
| } |
| |
| /** |
| * cls should always be isAbsent() |
| * @return null for cls.isNull(). Otherwise return the exception that's contained in the Maybe. |
| */ |
| protected Exception getReturnException(Maybe<Class<?>> cls) { |
| if (cls.isNull()) { |
| return null; |
| } |
| try { |
| cls.get(); |
| return null; |
| } catch (Exception e) { |
| Exceptions.propagateIfFatal(e); |
| return e; |
| } |
| } |
| |
| // ClassLoader.getResource returns null if there's a leading slash |
| private String stripLeadingSlash(String name) { |
| String[] arr = name.split(":"); |
| int last = arr.length - 1; |
| if (arr[last].startsWith("/")) { |
| arr[last] = arr[last].substring(1); |
| } |
| return Joiner.on(":").join(arr); |
| } |
| |
| @Override |
| public String toString() { |
| return getClass().getSimpleName() + "[" + MoreObjects.toStringHelper(this) |
| .add("claddLoader", classLoader) |
| .add("entity", entity) |
| .add("mgmt", mgmt) + "]"; |
| } |
| } |