blob: 7b6df6d79739de7c0ea92f252428641881ffccaf [file] [log] [blame]
/*
* 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.brooklyn.util.core;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import java.io.ByteArrayInputStream;
import java.net.URL;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import javax.annotation.Nullable;
import org.apache.brooklyn.api.catalog.CatalogItem.CatalogBundle;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.core.BrooklynVersion;
import org.apache.brooklyn.core.catalog.internal.CatalogBundleDto;
import org.apache.brooklyn.core.catalog.internal.CatalogEntityItemDto;
import org.apache.brooklyn.core.catalog.internal.CatalogItemBuilder;
import org.apache.brooklyn.core.entity.AbstractEntity;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.mgmt.ha.OsgiManager;
import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.core.mgmt.osgi.OsgiStandaloneTest;
import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
import org.apache.brooklyn.entity.stock.BasicEntity;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.test.support.TestResourceUnavailableException;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableSet;
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.maven.MavenArtifact;
import org.apache.brooklyn.util.maven.MavenRetriever;
import org.apache.brooklyn.util.osgi.OsgiTestResources;
import org.apache.brooklyn.util.osgi.VersionedName;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.osgi.framework.Bundle;
import org.osgi.framework.launch.Framework;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
public class ClassLoaderUtilsTest {
private LocalManagementContext mgmt;
private String origWhiteListKey;
@BeforeMethod(alwaysRun=true)
public void setUp() throws Exception {
origWhiteListKey = System.getProperty(ClassLoaderUtils.WHITE_LIST_KEY);
}
@AfterMethod(alwaysRun=true)
public void tearDown() throws Exception {
if (origWhiteListKey != null) {
System.setProperty(ClassLoaderUtils.WHITE_LIST_KEY, origWhiteListKey);
} else {
System.clearProperty(ClassLoaderUtils.WHITE_LIST_KEY);
}
if (mgmt != null) {
Entities.destroyAll(mgmt);
}
}
@Test
public void testLoadClassNotInOsgi() throws Exception {
ClassLoaderUtils clu = new ClassLoaderUtils(getClass());
assertLoadSucceeds(clu, getClass().getName(), getClass());
assertLoadSucceeds(clu, Entity.class.getName(), Entity.class);
assertLoadSucceeds(clu, AbstractEntity.class.getName(), AbstractEntity.class);
assertLoadFails(clu, "org.apache.brooklyn.this.name.does.not.Exist");
}
@Test
public void testLoadClassInOsgi() throws Exception {
String bundlePath = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH;
String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL;
String classname = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY;
TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), bundlePath);
mgmt = LocalManagementContextForTests.builder(true).enableOsgiReusable().build();
Bundle bundle = installBundle(mgmt, bundleUrl, false /* this works without starting it */);
@SuppressWarnings("unchecked")
Class<? extends Entity> clazz = (Class<? extends Entity>) bundle.loadClass(classname);
Entity entity = createSimpleEntity(bundleUrl, clazz);
System.setProperty(ClassLoaderUtils.WHITE_LIST_KEY, "do.not.match.whitelist:1.0.0");
ClassLoaderUtils cluMgmt = new ClassLoaderUtils(getClass(), mgmt);
ClassLoaderUtils cluClass = new ClassLoaderUtils(clazz);
ClassLoaderUtils cluEntity = new ClassLoaderUtils(getClass(), entity);
assertLoadFails(classname, cluMgmt);
assertLoadSucceeds(bundle.getSymbolicName() + ":" + classname, clazz, cluMgmt, cluClass, cluEntity);
assertLoadSucceeds(bundle.getSymbolicName() + ":" + bundle.getVersion()+":" + classname, clazz, cluMgmt, cluClass, cluEntity);
// we can also load classpath url in the scope of the entity
ResourceUtils.create(entity).getResourceAsString("classpath://"+classname.replaceAll("\\.", "/")+".class");
// (but not in global scope)
Asserts.assertFailsWith(() -> ResourceUtils.create(null).getResourceAsString("classpath://"+classname.replaceAll("\\.", "/")+".class"),
e -> Asserts.expectedFailureContainsIgnoreCase(e, "not found on classpath", "SimpleEntity"));
// test load still works when we have the item in the search path only but not a catalog item id on the entity
((EntityInternal)entity).setCatalogItemIdAndSearchPath(null, MutableList.of(entity.getCatalogItemId()));
ResourceUtils.create(entity).getResourceAsString("classpath://"+classname.replaceAll("\\.", "/")+".class");
Entity e2 = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicEntity.class).addSearchPath(MutableList.of(bundle.getSymbolicName()+":"+bundle.getVersion())));
ResourceUtils.create(e2).getResourceAsString("classpath://"+classname.replaceAll("\\.", "/")+".class");
}
@Test
public void testLoadJustOneClassInOsgiWhiteList() throws Exception {
String bundlePath = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH;
String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL;
String classname = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY;
TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), bundlePath);
mgmt = LocalManagementContextForTests.builder(true).enableOsgiReusable().build();
Bundle bundle = installBundle(mgmt, bundleUrl, false /* this works without starting it */);
Class<?> clazz = bundle.loadClass(classname);
Entity entity = createSimpleEntity(bundleUrl, clazz);
String whiteList = bundle.getSymbolicName()+":"+bundle.getVersion();
System.setProperty(ClassLoaderUtils.WHITE_LIST_KEY, whiteList);
ClassLoaderUtils cluEntity = new ClassLoaderUtils(getClass(), entity);
BundledName resource = new BundledName(classname).toResource();
BundledName bn = new BundledName(resource.bundle, resource.version, "/" + resource.name);
Asserts.assertSize(cluEntity.getResources(bn.toString()), 2); // should no longer mask the other available option
}
@Test
public void testVariousLoadersLoadClassInOsgiWhiteList() throws Exception {
String bundlePath = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH;
String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL;
String classname = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY;
TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), bundlePath);
mgmt = LocalManagementContextForTests.builder(true).enableOsgiReusable().build();
Bundle bundle = installBundle(mgmt, bundleUrl, true);
Class<?> clazz = bundle.loadClass(classname);
Entity entity = createSimpleEntity(bundleUrl, clazz);
String whiteList = bundle.getSymbolicName()+":"+bundle.getVersion();
System.setProperty(ClassLoaderUtils.WHITE_LIST_KEY, whiteList);
ClassLoaderUtils cluMgmt = new ClassLoaderUtils(getClass(), mgmt);
ClassLoaderUtils cluClass = new ClassLoaderUtils(clazz);
ClassLoaderUtils cluEntity = new ClassLoaderUtils(getClass(), entity);
assertLoadSucceeds(classname, clazz, cluMgmt, cluClass, cluEntity);
assertLoadSucceeds(bundle.getSymbolicName() + ":" + classname, clazz, cluMgmt, cluClass, cluEntity);
}
@Test
public void testLoadClassInOsgiWhiteListWithInvalidBundlePresent() throws Exception {
String bundlePath = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH;
String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL;
String classname = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY;
TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), bundlePath);
mgmt = LocalManagementContextForTests.builder(true).enableOsgiReusable().build();
Bundle bundle = installBundle(mgmt, bundleUrl, true);
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
JarOutputStream target = new JarOutputStream(buffer, manifest);
target.close();
OsgiManager osgiManager = ((ManagementContextInternal)mgmt).getOsgiManager().get();
Framework framework = osgiManager.getFramework();
Bundle installedBundle = framework.getBundleContext().installBundle("stream://invalid", new ByteArrayInputStream(buffer.toByteArray()));
assertNotNull(installedBundle);
Class<?> clazz = bundle.loadClass(classname);
Entity entity = createSimpleEntity(bundleUrl, clazz);
String whileList = bundle.getSymbolicName()+":"+bundle.getVersion();
System.setProperty(ClassLoaderUtils.WHITE_LIST_KEY, whileList);
ClassLoaderUtils cluMgmt = new ClassLoaderUtils(getClass(), mgmt);
ClassLoaderUtils cluClass = new ClassLoaderUtils(clazz);
ClassLoaderUtils cluEntity = new ClassLoaderUtils(getClass(), entity);
assertLoadSucceeds(classname, clazz, cluMgmt, cluClass, cluEntity);
assertLoadSucceeds(bundle.getSymbolicName() + ":" + classname, clazz, cluMgmt, cluClass, cluEntity);
}
@Test
public void testLoadClassInOsgiCore() throws Exception {
Class<?> clazz = BasicEntity.class;
String classname = clazz.getName();
mgmt = LocalManagementContextForTests.builder(true).enableOsgiReusable().build();
Bundle bundle = getBundle(mgmt, "org.apache.brooklyn.core");
String url = bundle.getLocation();
// NB: the above will be a system:file: url when running tests against target/classes/ -- but
// OSGi manager will accept that if running in dev mode
Entity entity = createSimpleEntity(url, clazz);
ClassLoaderUtils cluMgmt = new ClassLoaderUtils(getClass(), mgmt);
ClassLoaderUtils cluClass = new ClassLoaderUtils(clazz);
ClassLoaderUtils cluNone = new ClassLoaderUtils(getClass());
ClassLoaderUtils cluEntity = new ClassLoaderUtils(getClass(), entity);
assertLoadSucceeds(classname, clazz, cluMgmt, cluClass, cluNone, cluEntity);
assertLoadSucceeds(classname, clazz, cluMgmt, cluClass, cluNone, cluEntity);
assertLoadSucceeds(bundle.getSymbolicName() + ":" + classname, clazz, cluMgmt, cluClass, cluNone, cluEntity);
assertLoadSucceeds(bundle.getSymbolicName() + ":" + bundle.getVersion() + ":" + classname, clazz, cluMgmt, cluClass, cluNone, cluEntity);
}
@Test
public void testLoadClassInOsgiApi() throws Exception {
Class<?> clazz = Entity.class;
String classname = clazz.getName();
mgmt = LocalManagementContextForTests.builder(true).enableOsgiReusable().build();
Bundle bundle = getBundle(mgmt, "org.apache.brooklyn.api");
ClassLoaderUtils cluMgmt = new ClassLoaderUtils(getClass(), mgmt);
ClassLoaderUtils cluClass = new ClassLoaderUtils(clazz);
ClassLoaderUtils cluNone = new ClassLoaderUtils(getClass());
assertLoadSucceeds(classname, clazz, cluMgmt, cluClass, cluNone);
assertLoadSucceeds(classname, clazz, cluMgmt, cluClass, cluNone);
assertLoadSucceeds(bundle.getSymbolicName() + ":" + classname, clazz, cluMgmt, cluClass, cluNone);
assertLoadSucceeds(bundle.getSymbolicName() + ":" + bundle.getVersion() + ":" + classname, clazz, cluMgmt, cluClass, cluNone);
}
@Test
public void testIsBundleWhiteListed() throws Exception {
mgmt = LocalManagementContextForTests.builder(true).enableOsgiReusable().build();
ClassLoaderUtils clu = new ClassLoaderUtils(getClass(), mgmt);
assertTrue(clu.isBundleWhiteListed(getBundle(mgmt, "org.apache.brooklyn.core")));
assertTrue(clu.isBundleWhiteListed(getBundle(mgmt, "org.apache.brooklyn.api")));
assertFalse(clu.isBundleWhiteListed(getBundle(mgmt, "com.google.guava")));
}
/**
* When two guava versions installed, want us to load from the *brooklyn* version rather than
* a newer version that happens to be in Karaf.
*/
@Test(groups={"Integration"})
public void testLoadsFromRightGuavaVersion() throws Exception {
mgmt = LocalManagementContextForTests.builder(true).enableOsgiReusable().build();
ClassLoaderUtils clu = new ClassLoaderUtils(getClass(), mgmt);
String bundleUrl = MavenRetriever.localUrl(MavenArtifact.fromCoordinate("com.google.guava:guava:jar:18.0"));
Bundle bundle = installBundle(mgmt, bundleUrl, false /* this works without starting */);
String bundleName = bundle.getSymbolicName();
String classname = bundleName + ":" + ImmutableList.class.getName();
assertLoadSucceeds(clu, classname, ImmutableList.class);
}
@Test
public void testLoadBrooklynClass() throws Exception {
mgmt = LocalManagementContextForTests.builder(true).enableOsgiReusable().build();
new ClassLoaderUtils(this, mgmt).loadClass(
"org.apache.brooklyn.api",
BrooklynVersion.get(),
Entity.class.getName());
new ClassLoaderUtils(this, mgmt).loadClass(
"org.apache.brooklyn.api",
BrooklynVersion.getOsgiVersion(),
Entity.class.getName());
try {
new ClassLoaderUtils(this, mgmt).loadClass(
"org.apache.brooklyn.api",
"100.100.100-alpha-version_wth.tags",
"org.apache.brooklyn.NonExistentClass"); // actual classes may be found on the classpath
Asserts.shouldHaveFailedPreviously();
} catch (Exception e) {
IllegalStateException nested = Exceptions.getFirstThrowableOfType(e, IllegalStateException.class);
assertNotNull(nested);
Asserts.expectedFailureContains(nested, "not found to load", "org.apache.brooklyn.api", "100.100.100");
}
try {
new ClassLoaderUtils(this, mgmt).loadClass(
"org.apache.brooklyn.api",
"100.100.100-SNAPSHOT",
"org.apache.brooklyn.NonExistentClass"); // actual classes may be found on the classpath
Asserts.shouldHaveFailedPreviously();
} catch (Exception e) {
IllegalStateException nested = Exceptions.getFirstThrowableOfType(e, IllegalStateException.class);
assertNotNull(nested);
Asserts.expectedFailureContains(nested, "not found to load", "org.apache.brooklyn.api", "100.100.100");
}
}
private Bundle installBundle(ManagementContext mgmt, String bundleUrl, boolean start) throws Exception {
OsgiManager osgiManager = ((ManagementContextInternal)mgmt).getOsgiManager().get();
Framework framework = osgiManager.getFramework();
Bundle result = Osgis.install(framework, bundleUrl);
if (start) result.start();
return result;
}
private Bundle getBundle(ManagementContext mgmt, final String symbolicName) throws Exception {
OsgiManager osgiManager = ((ManagementContextInternal)mgmt).getOsgiManager().get();
Framework framework = osgiManager.getFramework();
Maybe<Bundle> result = Osgis.bundleFinder(framework)
.symbolicName(symbolicName)
.find();
return result.get();
}
private void assertLoadSucceeds(String bundledClassName, Class<?> expectedClass, ClassLoaderUtils... clua) throws ClassNotFoundException {
for (ClassLoaderUtils clu : clua) {
try {
assertLoadSucceeds(clu, bundledClassName, expectedClass);
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
throw new IllegalStateException("Load failed for " + clu, e);
}
}
}
private void assertLoadSucceeds(ClassLoaderUtils clu, String bundledClassName, Class<?> expectedClass) throws ClassNotFoundException {
BundledName className = new BundledName(bundledClassName);
Class<?> cls = clu.loadClass(bundledClassName);
assertEquals(cls.getName(), className.name);
if (expectedClass != null) {
assertEquals(cls, expectedClass);
}
ClassLoader cl = cls.getClassLoader();
BundledName resource = className.toResource();
String bundledResource = resource.toString();
URL resourceUrl = cl.getResource(resource.name);
assertEquals(clu.getResource(bundledResource), resourceUrl);
Iterable<URL> allMatches = clu.getResources(bundledResource);
assertEquals(allMatches.iterator().next(), resourceUrl, "Loading with "+clu);
// may have others, eg if the bundle provides it via multiple sources
BundledName rootResource = new BundledName(resource.bundle, resource.version, "/" + resource.name);
String rootBundledResource = rootResource.toString();
assertEquals(clu.getResource(rootBundledResource), resourceUrl);
allMatches = clu.getResources(rootBundledResource);
assertEquals(allMatches.iterator().next(), resourceUrl);
}
private void assertLoadFails(String bundledClassName, ClassLoaderUtils... clua) {
for (ClassLoaderUtils clu : clua) {
assertLoadFails(clu, bundledClassName);
}
}
private void assertLoadFails(ClassLoaderUtils clu, String bundledClassName) {
BundledName className = new BundledName(bundledClassName);
try {
clu.loadClass(bundledClassName);
Asserts.shouldHaveFailedPreviously("Using loader " + clu);
} catch (ClassNotFoundException e) {
Asserts.expectedFailureContains(e, bundledClassName, "not found on the application class path, nor in the bundle white list");
}
BundledName resource = className.toResource();
String bundledResource = resource.toString();
assertNull(clu.getResource(bundledResource), resource + " is supposed to fail resource loading, but it was successful");
assertEquals(clu.getResources(bundledResource), ImmutableList.of(), resource + " is supposed to fail resource loading, but it was successful");
}
protected Entity createSimpleEntity(String bundleUrl, Class<?> clazz) {
@SuppressWarnings("unchecked")
Class<? extends Entity> entityClass = (Class<? extends Entity>) clazz;
EntitySpec<?> spec = EntitySpec.create(entityClass);
Entity entity = mgmt.getEntityManager().createEntity(spec);
CatalogEntityItemDto item = CatalogItemBuilder.newEntity(clazz.getName(), "1.0.0")
.libraries(ImmutableList.<CatalogBundle>of(new CatalogBundleDto(null, null, bundleUrl)))
.plan("{\"services\":[{\"type\": \"" + clazz.getName() + "\"}]}")
.build();
mgmt.getCatalog().addItem(item);
final EntityInternal entityInternal = (EntityInternal) entity;
entityInternal.setCatalogItemIdAndSearchPath(item.getCatalogItemId(), item.getCatalogItemIdSearchPath());
return entity;
}
private static class BundledName {
String bundle;
String version;
String name;
BundledName(String bundledName) {
String[] arr = bundledName.split(":");
if (arr.length == 1) {
bundle = null;
version = null;
name = arr[0];
} else if (arr.length == 2) {
bundle = arr[0];
version = null;
name = arr[1];
} else if (arr.length == 3) {
bundle = arr[0];
version = arr[1];
name = arr[2];
} else {
throw new IllegalStateException("Invalid bundled name " + bundledName);
}
}
BundledName(@Nullable String bundle, @Nullable String version, String name) {
this.bundle = bundle;
this.version = version;
this.name = checkNotNull(name, "name");
}
@Override
public String toString() {
return (bundle != null ? bundle + ":" : "") +
(version != null ? version + ":" : "") +
name;
}
BundledName toResource() {
return new BundledName(bundle, version, name.replace(".", "/") + ".class");
}
}
}