From a4930f02734c80846ae6c7a7469ead9a145ccd5b Mon Sep 17 00:00:00 2001 From: Svetoslav Neykov Date: Wed, 6 Jul 2016 17:37:19 +0300 Subject: [PATCH 1/7] Persistence hacks --- .../mgmt/persist/XmlMementoSerializer.java | 16 +++++++++++- .../core/mgmt/rebind/RebindIteration.java | 26 ++++++++++++++++--- .../brooklyn/util/core/ClassLoaderUtils.java | 20 +++++++++----- .../main/resources/etc/default.catalog.bom | 2 +- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializer.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializer.java index e9085dc52d..58df79f207 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializer.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializer.java @@ -60,6 +60,7 @@ import org.apache.brooklyn.core.mgmt.rebind.dto.BasicPolicyMemento; import org.apache.brooklyn.core.mgmt.rebind.dto.MutableBrooklynMemento; import org.apache.brooklyn.core.sensor.BasicAttributeSensor; +import org.apache.brooklyn.util.core.ClassLoaderUtils; import org.apache.brooklyn.util.core.xstream.XmlSerializer; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.text.Strings; @@ -94,10 +95,23 @@ public XmlMementoSerializer(ClassLoader classLoader) { this(classLoader, DeserializingClassRenamesProvider.loadDeserializingClassRenames()); } + private static class CustomClassLoader extends ClassLoader { + private ClassLoaderUtils loader; + private CustomClassLoader(ClassLoader cl) { + loader = new ClassLoaderUtils(cl); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + return loader.loadClass(name); + } + + } + public XmlMementoSerializer(ClassLoader classLoader, Map deserializingClassRenames) { super(deserializingClassRenames); this.classLoader = checkNotNull(classLoader, "classLoader"); - xstream.setClassLoader(this.classLoader); + xstream.setClassLoader(new CustomClassLoader(this.classLoader)); // old (deprecated in 070? or earlier) single-file persistence uses this keyword; TODO remove soon in 080 ? xstream.alias("brooklyn", MutableBrooklynMemento.class); diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java index 6c0bd9dbf8..1f1ac2e7da 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java @@ -91,10 +91,12 @@ import org.apache.brooklyn.core.policy.AbstractPolicy; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.core.ClassLoaderUtils; import org.apache.brooklyn.util.core.flags.FlagUtils; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.javalang.Reflections; +import org.apache.brooklyn.util.javalang.Reflections.ReflectionNotFoundException; import org.apache.brooklyn.util.text.Strings; import org.apache.brooklyn.util.time.Duration; import org.apache.brooklyn.util.time.Time; @@ -956,7 +958,7 @@ protected LoadedClass load(Class bTyp } try { - return new LoadedClass((Class)reflections.loadClass(jType), catalogItemId); + return new LoadedClass((Class)loadClass(jType), catalogItemId); } catch (Exception e) { Exceptions.propagateIfFatal(e); LOG.warn("Unable to load "+jType+" using reflections; will try standard context"); @@ -980,11 +982,29 @@ protected LoadedClass load(Class bTyp } } + protected Class loadClass(String jType) throws ClassNotFoundException { + try { + return reflections.loadClass(jType); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + } + return new ClassLoaderUtils(reflections.getClassLoader()).loadClass(jType); + } + + @SuppressWarnings("unchecked") + public Class loadClass(String classname, Class superType) { + try { + return (Class) loadClass(classname); + } catch (ClassNotFoundException e) { + throw Exceptions.propagate(e); + } + } + /** * Constructs a new location, passing to its constructor the location id and all of memento.getFlags(). */ protected Location newLocation(String locationId, String locationType) { - Class locationClazz = reflections.loadClass(locationType, Location.class); + Class locationClazz = loadClass(locationType, Location.class); if (InternalFactory.isNewStyle(locationClazz)) { // Not using loationManager.createLocation(LocationSpec) because don't want init() to be called @@ -1110,7 +1130,7 @@ protected Feed newFeed(FeedMemento memento) { String id = memento.getId(); // catalog item subtypes are internal to brooklyn, not in osgi String itemType = checkNotNull(memento.getType(), "catalog item type of %s must not be null in memento", id); - Class clazz = reflections.loadClass(itemType, CatalogItem.class); + Class clazz = loadClass(itemType, CatalogItem.class); return invokeConstructor(reflections, clazz, new Object[]{}); } diff --git a/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java b/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java index 644f07a932..00fd05fca2 100644 --- a/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java +++ b/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java @@ -57,7 +57,7 @@ public class ClassLoaderUtils { // 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 Class callingClass; + private final ClassLoader classLoader; private final Entity entity; private final ManagementContext mgmt; @@ -69,19 +69,25 @@ public ClassLoaderUtils(Object callingObj, @Nullable ManagementContext mgmt) { } public ClassLoaderUtils(Class callingClass) { - this.callingClass = callingClass; + this.classLoader = callingClass.getClassLoader(); + this.entity = null; + this.mgmt = null; + } + + public ClassLoaderUtils(ClassLoader cl) { + this.classLoader = cl; this.entity = null; this.mgmt = null; } public ClassLoaderUtils(Class callingClass, Entity entity) { - this.callingClass = callingClass; + this.classLoader = callingClass.getClassLoader(); this.entity = entity; this.mgmt = ((EntityInternal)entity).getManagementContext(); } public ClassLoaderUtils(Class callingClass, @Nullable ManagementContext mgmt) { - this.callingClass = callingClass; + this.classLoader = callingClass.getClassLoader(); this.entity = null; this.mgmt = mgmt; } @@ -132,8 +138,8 @@ public Class loadClass(String name) throws ClassNotFoundException { } try { - // Used instead of callingClass.getClassLoader() as it could be null (only for bootstrap classes) - return Class.forName(name, true, callingClass.getClassLoader()); + // Used instead of callingClass.getClassLoader().loadClass(...) as it could be null (only for bootstrap classes) + return Class.forName(name, true, classLoader); } catch (ClassNotFoundException e) { } @@ -171,7 +177,7 @@ public Class loadClass(String symbolicName, @Nullable String version, String } return SystemFrameworkLoader.get().loadClassFromBundle(className, bundle.get()); } else { - return Class.forName(className); + return Class.forName(className, true, classLoader); } } diff --git a/karaf/apache-brooklyn/src/main/resources/etc/default.catalog.bom b/karaf/apache-brooklyn/src/main/resources/etc/default.catalog.bom index 8184c6e101..e1cfe996c7 100644 --- a/karaf/apache-brooklyn/src/main/resources/etc/default.catalog.bom +++ b/karaf/apache-brooklyn/src/main/resources/etc/default.catalog.bom @@ -7,7 +7,7 @@ brooklyn.catalog: items: - brooklyn.libraries: - name: org.apache.brooklyn.karaf-init - version: 0.10.0.SNAPSHOT # BROOKLYN_VERSION + version: "0.10.0.SNAPSHOT" # BROOKLYN_VERSION include: classpath://catalog-classes.bom - id: server From 25605caee4a2b2313eb3dc31759967f65be67b87 Mon Sep 17 00:00:00 2001 From: Aled Sage Date: Thu, 7 Jul 2016 00:07:25 +0100 Subject: [PATCH 2/7] $brooklyn:object: handle osgi classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If can’t load immediately, then defer until we know the entity thus the catalog (and libraries). --- .../spi/dsl/methods/BrooklynDslCommon.java | 63 +++++++++++++------ 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java index d496fd7aec..23cc3a377a 100644 --- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java @@ -18,6 +18,8 @@ */ package org.apache.brooklyn.camp.brooklyn.spi.dsl.methods; +import static com.google.common.base.Preconditions.checkNotNull; + import java.util.Arrays; import java.util.Iterator; import java.util.List; @@ -39,7 +41,9 @@ import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslUtils; import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent.Scope; import org.apache.brooklyn.core.config.external.ExternalConfigSupplier; +import org.apache.brooklyn.core.entity.AbstractEntity; import org.apache.brooklyn.core.entity.EntityDynamicType; +import org.apache.brooklyn.core.entity.EntityInternal; import org.apache.brooklyn.core.mgmt.internal.ExternalConfigSupplierRegistry; import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.core.mgmt.persist.DeserializingClassRenamesProvider; @@ -57,6 +61,8 @@ import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes; import org.apache.brooklyn.util.text.Strings; import org.apache.commons.beanutils.BeanUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.base.Function; import com.google.common.base.Objects; @@ -66,6 +72,8 @@ /** static import functions which can be used in `$brooklyn:xxx` contexts */ public class BrooklynDslCommon { + private static final Logger LOG = LoggerFactory.getLogger(BrooklynDslCommon.class); + // Access specific entities public static DslComponent self() { @@ -162,8 +170,9 @@ public static EntitySpecConfiguration entitySpec(Map arguments) /** * Return an instance of the specified class with its fields set according - * to the {@link Map} or a {@link BrooklynDslDeferredSupplier} if the arguments are not - * yet fully resolved. + * to the {@link Map}. Or a {@link BrooklynDslDeferredSupplier} if either the arguments are + * not yet fully resolved, or the class cannot be loaded yet (e.g. needs the catalog's OSGi + * bundles). */ @SuppressWarnings("unchecked") public static Object object(Map arguments) { @@ -171,22 +180,24 @@ public static Object object(Map arguments) { String typeName = BrooklynYamlTypeInstantiator.InstantiatorFromKey.extractTypeName("object", config).orNull(); Map objectFields = (Map) config.getStringKeyMaybe("object.fields").or(MutableMap.of()); Map brooklynConfig = (Map) config.getStringKeyMaybe(BrooklynCampReservedKeys.BROOKLYN_CONFIG).or(MutableMap.of()); + + String mappedTypeName = DeserializingClassRenamesProvider.findMappedName(typeName); + Class type; try { - // TODO Should use catalog's classloader, rather than ClassLoaderUtils; how to get that? Should we return a future?! - String mappedTypeName = DeserializingClassRenamesProvider.findMappedName(typeName); - Class type = new ClassLoaderUtils(BrooklynDslCommon.class).loadClass(mappedTypeName); - - if (!Reflections.hasNoArgConstructor(type)) { - throw new IllegalStateException(String.format("Cannot construct %s bean: No public no-arg constructor available", type)); - } - if ((objectFields.isEmpty() || DslUtils.resolved(objectFields.values())) && - (brooklynConfig.isEmpty() || DslUtils.resolved(brooklynConfig.values()))) { - return DslObject.create(type, objectFields, brooklynConfig); - } else { - return new DslObject(type, objectFields, brooklynConfig); - } + type = new ClassLoaderUtils(BrooklynDslCommon.class).loadClass(mappedTypeName); } catch (ClassNotFoundException e) { - throw Exceptions.propagate(e); + LOG.debug("Cannot load class " + typeName + " for DLS object; assuming it is in OSGi bundle; will defer its loading"); + return new DslObject(mappedTypeName, objectFields, brooklynConfig); + } + + if (!Reflections.hasNoArgConstructor(type)) { + throw new IllegalStateException(String.format("Cannot construct %s bean: No public no-arg constructor available", type)); + } + if ((objectFields.isEmpty() || DslUtils.resolved(objectFields.values())) && + (brooklynConfig.isEmpty() || DslUtils.resolved(brooklynConfig.values()))) { + return DslObject.create(type, objectFields, brooklynConfig); + } else { + return new DslObject(type, objectFields, brooklynConfig); } } @@ -316,11 +327,18 @@ protected static class DslObject extends BrooklynDslDeferredSupplier { private static final long serialVersionUID = 8878388748085419L; + private String typeName; private Class type; private Map fields, config; + public DslObject(String typeName, Map fields, Map config) { + this.typeName = checkNotNull(typeName, "typeName"); + this.fields = MutableMap.copyOf(fields); + this.config = MutableMap.copyOf(config); + } + public DslObject(Class type, Map fields, Map config) { - this.type = type; + this.type = checkNotNull(type, "type"); this.fields = MutableMap.copyOf(fields); this.config = MutableMap.copyOf(config); } @@ -328,6 +346,14 @@ public DslObject(Class type, Map fields, Map c @SuppressWarnings("unchecked") @Override public Task newTask() { + if (type == null) { + EntityInternal entity = entity(); + try { + type = new ClassLoaderUtils(BrooklynDslCommon.class, entity).loadClass(typeName); + } catch (ClassNotFoundException e) { + throw Exceptions.propagate(e); + } + } List> tasks = Lists.newLinkedList(); for (Object value : Iterables.concat(fields.values(), config.values())) { if (value instanceof TaskAdaptable) { @@ -336,6 +362,7 @@ public Task newTask() { tasks.add(((TaskFactory>) value).newTask()); } } + Map flags = MutableMap.of("displayName", "building '"+type+"' with "+tasks.size()+" task"+(tasks.size()!=1?"s":"")); return DependentConfiguration.transformMultiple(flags, new Function, Object>() { @Override @@ -399,7 +426,7 @@ public boolean equals(Object obj) { @Override public String toString() { - return "$brooklyn:object(\""+type.getName()+"\")"; + return "$brooklyn:object(\""+(type != null ? type.getName() : typeName)+"\")"; } } From a555beac618c931d0a1a682f34f1158cb23b3df2 Mon Sep 17 00:00:00 2001 From: Aled Sage Date: Thu, 7 Jul 2016 00:12:30 +0100 Subject: [PATCH 3/7] OSGi classloading for persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * classnames are written out with the OSGi bundle’s symbolic name as a prefix in persisted state (unless from a white-listed brooklyn bundle) * stream custom classloading to handle the bundle-name prefix * adds SimpleObject to osgi jar (and adds config keys to entity) * adds tests for rebind with OSGi --- .../camp/brooklyn/RebindOsgiTest.java | 398 ++++++++++++++++++ .../brooklyn/core/mgmt/ha/OsgiManager.java | 1 - .../mgmt/persist/XmlMementoSerializer.java | 216 ++++++---- .../core/mgmt/rebind/RebindIteration.java | 4 +- .../brooklyn/util/core/ClassLoaderUtils.java | 39 +- .../apache/brooklyn/util/core/osgi/Osgis.java | 6 + ...toSerializerDelegatingClassLoaderTest.java | 140 ++++++ .../persist/XmlMementoSerializerTest.java | 115 ++++- .../util/core/ClassLoaderUtilsTest.java | 12 + .../test/osgi/entities/SimpleApplication.java | 1 - .../osgi/entities/SimpleApplicationImpl.java | 1 - .../test/osgi/entities/SimpleEntity.java | 9 +- .../test/osgi/entities/SimpleEntityImpl.java | 1 - .../test/osgi/entities/SimpleObject.java | 56 +++ .../test/osgi/entities/SimplePolicy.java | 6 +- .../brooklyn/util/osgi/VersionedName.java | 11 +- .../brooklyn/util/osgi/OsgiTestResources.java | 3 + 17 files changed, 909 insertions(+), 110 deletions(-) create mode 100644 camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/RebindOsgiTest.java create mode 100644 core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerDelegatingClassLoaderTest.java create mode 100644 utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleObject.java diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/RebindOsgiTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/RebindOsgiTest.java new file mode 100644 index 0000000000..752b066f0c --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/RebindOsgiTest.java @@ -0,0 +1,398 @@ +/* + * 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.camp.brooklyn; + +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 java.io.File; +import java.util.List; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.api.policy.Policy; +import org.apache.brooklyn.core.catalog.internal.CatalogUtils; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.entity.StartableApplication; +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.sensor.Sensors; +import org.apache.brooklyn.core.test.entity.TestEntity; +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.javalang.Reflections; +import org.apache.brooklyn.util.osgi.OsgiTestResources; +import org.jclouds.compute.domain.OsFamily; +import org.osgi.framework.Bundle; +import org.osgi.framework.launch.Framework; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import com.google.common.base.Joiner; +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +public class RebindOsgiTest extends AbstractYamlRebindTest { + + private static final Logger LOG = LoggerFactory.getLogger(RebindOsgiTest.class); + + private static final String OSGI_BUNDLE_URL = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL; + private static final String OSGI_BUNDLE_SYMBOLIC_NAME = "org.apache.brooklyn.test.resources.osgi.brooklyn-test-osgi-entities"; + private static final String OSGI_ENTITY_TYPE = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY; + private static final String OSGI_POLICY_TYPE = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_POLICY; + private static final String OSGI_OBJECT_TYPE = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_OBJECT; + private static final String OSGI_ENTITY_CONFIG_NAME = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY_CONFIG_NAME; + private static final String OSGI_ENTITY_SENSOR_NAME = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY_SENSOR_NAME; + + private List bundleUrlsToInstallOnRebind; + + @BeforeMethod(alwaysRun = true) + @Override + public void setUp() throws Exception { + bundleUrlsToInstallOnRebind = Lists.newArrayList(); + super.setUp(); + } + + @Override + protected boolean useOsgi() { + return true; + } + + @Override + protected LocalManagementContext createNewManagementContext(File mementoDir) { + LocalManagementContext result = super.createNewManagementContext(mementoDir); + for (String bundleUrl : bundleUrlsToInstallOnRebind) { + try { + installBundle(result, bundleUrl); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + } + return result; + } + + @DataProvider(name = "valInEntityDataProvider") + public Object[][] valInEntityDataProvider() { + return new Object[][] { + {Predicates.alwaysTrue(), false}, + {Predicates.alwaysTrue(), true}, + {OsFamily.CENTOS, false}, + {OsFamily.CENTOS, true}, + }; + } + + @Test(dataProvider = "valInEntityDataProvider") + public void testValInEntity(Object val, boolean useOsgi) throws Exception { + String appSymbolicName = "my.catalog.app.id.load"; + String appVersion = "0.1.0"; + String appCatalogFormat; + if (useOsgi) { + appCatalogFormat = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + appSymbolicName, + " version: " + appVersion, + " itemType: entity", + " libraries:", + " - " + OSGI_BUNDLE_URL, + " item:", + " type: " + OSGI_ENTITY_TYPE); + } else { + appCatalogFormat = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + appSymbolicName, + " version: " + appVersion, + " itemType: entity", + " item:", + " type: " + TestEntity.class.getName()); + } + + // Create the catalog items + Iterables.getOnlyElement(addCatalogItems(String.format(appCatalogFormat, appVersion))); + + // Create an app, using that catalog item + String appBlueprintYaml = Joiner.on("\n").join( + "location: localhost\n", + "services:", + "- type: " + CatalogUtils.getVersionedId(appSymbolicName, appVersion)); + origApp = (StartableApplication) createAndStartApplication(appBlueprintYaml); + Entity origEntity = Iterables.getOnlyElement(origApp.getChildren()); + origEntity.config().set(TestEntity.CONF_OBJECT, val); + + // Rebind + rebind(); + + Entity newEntity = Iterables.getOnlyElement(newApp.getChildren()); + assertEquals(newEntity.config().get(TestEntity.CONF_OBJECT), val); + } + + @Test + public void testValInEntityFromOtherBundle() throws Exception { + installBundle(mgmt(), OSGI_BUNDLE_URL); + bundleUrlsToInstallOnRebind.add(OSGI_BUNDLE_URL); + + // Create an app, using that catalog item + String appBlueprintYaml = Joiner.on("\n").join( + "services:", + "- type: " + TestEntity.class.getName()); + origApp = (StartableApplication) createAndStartApplication(appBlueprintYaml); + Entity origEntity = Iterables.getOnlyElement(origApp.getChildren()); + + Object configVal = newOsgiSimpleObject("myEntityConfigVal"); + origEntity.config().set(ConfigKeys.newConfigKey(Object.class, OSGI_ENTITY_CONFIG_NAME), configVal); + + // Rebind + rebind(); + + // Ensure app is still there, and that it is usable - e.g. "stop" effector functions as expected + Entity newEntity = Iterables.getOnlyElement(newApp.getChildren()); + + Object newConfigVal = newEntity.config().get(ConfigKeys.newConfigKey(Object.class, OSGI_ENTITY_CONFIG_NAME)); + assertOsgiSimpleObjectsEqual(newConfigVal, configVal); + } + + @Test + public void testEntityAndPolicyFromCatalogOsgi() throws Exception { + String appSymbolicName = "my.catalog.app.id.load"; + String appVersion = "0.1.0"; + String appCatalogFormat = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + appSymbolicName, + " version: " + appVersion, + " itemType: entity", + " libraries:", + " - " + OSGI_BUNDLE_URL, + " item:", + " type: " + OSGI_ENTITY_TYPE, + " brooklyn.policies:", + " - type: " + OSGI_POLICY_TYPE); + + // Create the catalog items + Iterables.getOnlyElement(addCatalogItems(String.format(appCatalogFormat, appVersion))); + + // Create an app, using that catalog item + String appBlueprintYaml = Joiner.on("\n").join( + "location: localhost\n", + "services:", + "- type: " + CatalogUtils.getVersionedId(appSymbolicName, appVersion)); + origApp = (StartableApplication) createAndStartApplication(appBlueprintYaml); + Entity origEntity = Iterables.getOnlyElement(origApp.getChildren()); + Policy origPolicy = Iterables.getOnlyElement(origEntity.policies()); + + // Rebind + rebind(); + + // Ensure app is still there, and that it is usable - e.g. "stop" effector functions as expected + Entity newEntity = Iterables.getOnlyElement(newApp.getChildren()); + Policy newPolicy = Iterables.getOnlyElement(newEntity.policies()); + assertEquals(newEntity.getCatalogItemId(), appSymbolicName+":"+appVersion); + assertEquals(newPolicy.getId(), origPolicy.getId()); + + // Ensure stop works as expected + newApp.stop(); + assertFalse(Entities.isManaged(newApp)); + assertFalse(Entities.isManaged(newEntity)); + + // Ensure can still use catalog item to deploy a new entity + StartableApplication app2 = (StartableApplication) createAndStartApplication(appBlueprintYaml); + Entity entity2 = Iterables.getOnlyElement(app2.getChildren()); + assertEquals(entity2.getCatalogItemId(), appSymbolicName+":"+appVersion); + } + + @Test + public void testJavaPojoFromCatalogOsgi() throws Exception { + String appSymbolicName = "my.catalog.app.id.load"; + String appVersion = "0.1.0"; + String appCatalogFormat = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + appSymbolicName, + " version: " + appVersion, + " itemType: entity", + " libraries:", + " - " + OSGI_BUNDLE_URL, + " item:", + " type: " + OSGI_ENTITY_TYPE); + + // Create the catalog items + Iterables.getOnlyElement(addCatalogItems(String.format(appCatalogFormat, appVersion))); + + // Create an app, using that catalog item + String appBlueprintYaml = Joiner.on("\n").join( + "location: localhost\n", + "services:", + "- type: " + CatalogUtils.getVersionedId(appSymbolicName, appVersion)); + origApp = (StartableApplication) createAndStartApplication(appBlueprintYaml); + Entity origEntity = Iterables.getOnlyElement(origApp.getChildren()); + + Object configVal = newOsgiSimpleObject("myEntityConfigVal"); + Object sensorVal = newOsgiSimpleObject("myEntitySensorVal"); + origEntity.config().set(ConfigKeys.newConfigKey(Object.class, OSGI_ENTITY_CONFIG_NAME), configVal); + origEntity.sensors().set(Sensors.newSensor(Object.class, OSGI_ENTITY_SENSOR_NAME), sensorVal); + + // Rebind + rebind(); + + // Ensure app is still there, and that it is usable - e.g. "stop" effector functions as expected + Entity newEntity = Iterables.getOnlyElement(newApp.getChildren()); + + Object newConfigVal = newEntity.config().get(ConfigKeys.newConfigKey(Object.class, OSGI_ENTITY_CONFIG_NAME)); + Object newSensorVal = newEntity.sensors().get(Sensors.newSensor(Object.class, OSGI_ENTITY_SENSOR_NAME)); + assertOsgiSimpleObjectsEqual(newConfigVal, configVal); + assertOsgiSimpleObjectsEqual(newSensorVal, sensorVal); + } + + @Test + public void testBrooklynObjectDslFromCatalogOsgi() throws Exception { + String appSymbolicName = "my.catalog.app.id.load"; + String appVersion = "0.1.0"; + String appCatalogFormat = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + appSymbolicName, + " version: " + appVersion, + " itemType: entity", + " libraries:", + " - " + OSGI_BUNDLE_URL, + " item:", + " type: " + OSGI_ENTITY_TYPE, + " brooklyn.config:", + " " + OSGI_ENTITY_CONFIG_NAME + ":", + " $brooklyn:object:", + " type: " + OSGI_OBJECT_TYPE, + " object.fields:", + " val: myEntityVal"); + + // Create the catalog items + Iterables.getOnlyElement(addCatalogItems(String.format(appCatalogFormat, appVersion))); + + // Create an app, using that catalog item + String appBlueprintYaml = Joiner.on("\n").join( + "location: localhost\n", + "services:", + "- type: " + CatalogUtils.getVersionedId(appSymbolicName, appVersion)); + origApp = (StartableApplication) createAndStartApplication(appBlueprintYaml); + Entity origEntity = Iterables.getOnlyElement(origApp.getChildren()); + + Object configVal = origEntity.config().get(ConfigKeys.newConfigKey(Object.class, OSGI_ENTITY_CONFIG_NAME)); + assertEquals(getOsgiSimpleObjectsVal(configVal), "myEntityVal"); + + // Rebind + rebind(); + + // Ensure app is still there, and that it is usable - e.g. "stop" effector functions as expected + Entity newEntity = Iterables.getOnlyElement(newApp.getChildren()); + + Object newConfigVal = newEntity.config().get(ConfigKeys.newConfigKey(Object.class, OSGI_ENTITY_CONFIG_NAME)); + assertOsgiSimpleObjectsEqual(newConfigVal, configVal); + + // Ensure stop works as expected + newApp.stop(); + assertFalse(Entities.isManaged(newApp)); + assertFalse(Entities.isManaged(newEntity)); + + // Ensure can still use catalog item to deploy a new entity + StartableApplication app2 = (StartableApplication) createAndStartApplication(appBlueprintYaml); + Entity entity2 = Iterables.getOnlyElement(app2.getChildren()); + assertEquals(entity2.getCatalogItemId(), appSymbolicName+":"+appVersion); + } + + // TODO Does not do rebind; the config isn't there after rebind. + // Need to reproduce that in a simpler use-case. + @Test + public void testBrooklynObjectDslFromCatalogOsgiInPolicy() throws Exception { + String appSymbolicName = "my.catalog.app.id.load"; + String appVersion = "0.1.0"; + String appCatalogFormat = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + appSymbolicName, + " version: " + appVersion, + " itemType: entity", + " libraries:", + " - " + OSGI_BUNDLE_URL, + " item:", + " type: " + OSGI_ENTITY_TYPE, + " brooklyn.policies:", + " - type: " + OSGI_POLICY_TYPE, + " brooklyn.config:", + " " + OSGI_ENTITY_CONFIG_NAME + ":", + " $brooklyn:object:", + " type: " + OSGI_OBJECT_TYPE, + " object.fields:", + " val: myPolicyVal"); + + // Create the catalog items + Iterables.getOnlyElement(addCatalogItems(String.format(appCatalogFormat, appVersion))); + + // Create an app, using that catalog item + String appBlueprintYaml = Joiner.on("\n").join( + "location: localhost\n", + "services:", + "- type: " + CatalogUtils.getVersionedId(appSymbolicName, appVersion)); + origApp = (StartableApplication) createAndStartApplication(appBlueprintYaml); + Entity origEntity = Iterables.getOnlyElement(origApp.getChildren()); + Policy origPolicy = Iterables.getOnlyElement(origEntity.policies()); + + Object policyConfigVal = origPolicy.config().get(ConfigKeys.newConfigKey(Object.class, OSGI_ENTITY_CONFIG_NAME)); + assertEquals(getOsgiSimpleObjectsVal(policyConfigVal), "myPolicyVal"); + } + + private Bundle getBundle(ManagementContext mgmt, final String symbolicName) throws Exception { + OsgiManager osgiManager = ((ManagementContextInternal)mgmt).getOsgiManager().get(); + Framework framework = osgiManager.getFramework(); + Maybe result = Osgis.bundleFinder(framework) + .symbolicName(symbolicName) + .find(); + return result.get(); + } + + private Object newOsgiSimpleObject(String val) throws Exception { + Class osgiObjectClazz = getBundle(mgmt(), OSGI_BUNDLE_SYMBOLIC_NAME).loadClass(OSGI_OBJECT_TYPE); + return Reflections.invokeConstructorWithArgs(osgiObjectClazz, val).get(); + } + + private void assertOsgiSimpleObjectsEqual(Object val1, Object val2) throws Exception { + if (val2 == null) { + assertNull(val1); + } else { + assertNotNull(val1); + } + assertEquals(val1.getClass().getName(), val2.getClass().getName()); + assertEquals(getOsgiSimpleObjectsVal(val1), getOsgiSimpleObjectsVal(val2)); + } + + private String getOsgiSimpleObjectsVal(Object val) throws Exception { + assertNotNull(val); + return (String) Reflections.invokeMethodWithArgs(val, "getVal", ImmutableList.of()).get(); + } + + private Bundle installBundle(ManagementContext mgmt, String bundleUrl) throws Exception { + OsgiManager osgiManager = ((ManagementContextInternal)mgmt).getOsgiManager().get(); + Framework framework = osgiManager.getFramework(); + return Osgis.install(framework, bundleUrl); + } +} diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java index 28b20b3f8d..2c68c8af05 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java @@ -45,7 +45,6 @@ import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.os.Os; import org.apache.brooklyn.util.os.Os.DeletionResult; -import org.apache.brooklyn.util.osgi.SystemFramework; import org.apache.brooklyn.util.repeat.Repeater; import org.apache.brooklyn.util.text.Strings; import org.apache.brooklyn.util.time.Duration; diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializer.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializer.java index 58df79f207..15b3346e46 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializer.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializer.java @@ -61,22 +61,29 @@ import org.apache.brooklyn.core.mgmt.rebind.dto.MutableBrooklynMemento; import org.apache.brooklyn.core.sensor.BasicAttributeSensor; import org.apache.brooklyn.util.core.ClassLoaderUtils; +import org.apache.brooklyn.util.core.osgi.Osgis; import org.apache.brooklyn.util.core.xstream.XmlSerializer; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.text.Strings; +import org.osgi.framework.Bundle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.thoughtworks.xstream.MarshallingStrategy; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.SingleValueConverter; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.converters.reflection.ReflectionConverter; +import com.thoughtworks.xstream.core.ClassLoaderReference; import com.thoughtworks.xstream.core.ReferencingMarshallingContext; +import com.thoughtworks.xstream.core.util.Primitives; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.io.path.PathTrackingReader; +import com.thoughtworks.xstream.mapper.CannotResolveClassException; +import com.thoughtworks.xstream.mapper.DefaultMapper; import com.thoughtworks.xstream.mapper.Mapper; import com.thoughtworks.xstream.mapper.MapperWrapper; @@ -88,30 +95,17 @@ public class XmlMementoSerializer extends XmlSerializer implements Memento private static final Logger LOG = LoggerFactory.getLogger(XmlMementoSerializer.class); - private final ClassLoader classLoader; + private final OsgiClassLoader delegatingClassLoader; private LookupContext lookupContext; - + public XmlMementoSerializer(ClassLoader classLoader) { this(classLoader, DeserializingClassRenamesProvider.loadDeserializingClassRenames()); } - private static class CustomClassLoader extends ClassLoader { - private ClassLoaderUtils loader; - private CustomClassLoader(ClassLoader cl) { - loader = new ClassLoaderUtils(cl); - } - - @Override - protected Class findClass(String name) throws ClassNotFoundException { - return loader.loadClass(name); - } - - } - public XmlMementoSerializer(ClassLoader classLoader, Map deserializingClassRenames) { super(deserializingClassRenames); - this.classLoader = checkNotNull(classLoader, "classLoader"); - xstream.setClassLoader(new CustomClassLoader(this.classLoader)); + this.delegatingClassLoader = new OsgiClassLoader(classLoader); + xstream.setClassLoader(this.delegatingClassLoader); // old (deprecated in 070? or earlier) single-file persistence uses this keyword; TODO remove soon in 080 ? xstream.alias("brooklyn", MutableBrooklynMemento.class); @@ -156,6 +150,7 @@ public XmlMementoSerializer(ClassLoader classLoader, Map deseria @Override protected MapperWrapper wrapMapperForNormalUsage(Mapper next) { MapperWrapper mapper = super.wrapMapperForNormalUsage(next); + mapper = new OsgiClassnameMapper(mapper); mapper = new CustomMapper(mapper, Entity.class, "entityProxy"); mapper = new CustomMapper(mapper, Location.class, "locationProxy"); mapper = new UnwantedStateLoggingMapper(mapper); @@ -175,6 +170,7 @@ public void serialize(Object object, Writer writer) { @Override public void setLookupContext(LookupContext lookupContext) { this.lookupContext = checkNotNull(lookupContext, "lookupContext"); + delegatingClassLoader.setManagementContext(lookupContext.lookupManagementContext()); } @Override @@ -410,6 +406,47 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co } } + public class OsgiClassnameMapper extends MapperWrapper { + private final ClassLoaderUtils whiteListRetriever; + + OsgiClassnameMapper(MapperWrapper mapper) { + super(mapper); + whiteListRetriever = new ClassLoaderUtils(getClass()); + } + + @Override + public String serializedClass(Class type) { + // TODO What if previous stages have already renamed it? + // For example the "outer class renaming stuff"?! + String superResult = super.serializedClass(type); + if (type != null && type.getName().equals(superResult)) { + Optional bundle = Osgis.getBundleOf(type); + if (bundle.isPresent() && !whiteListRetriever.isBundleWhiteListed(bundle.get())) { + return bundle.get().getSymbolicName() + ":" + superResult; + } + } + return superResult; + } + + @Override + public Class realClass(String elementName) { + CannotResolveClassException tothrow; + try { + return super.realClass(elementName); + } catch (CannotResolveClassException e) { + tothrow = e; + } + + // Class.forName(elementName, false, classLader) does not seem to like us returned a + // class whose name does not match that passed in. Therefore fallback to using loadClass. + try { + return xstream.getClassLoaderReference().getReference().loadClass(elementName); + } catch (ClassNotFoundException e) { + throw new CannotResolveClassException(elementName + " via loadClass", tothrow); + } + } + } + /** When reading/writing specs, it checks whether there is a catalog item id set and uses it to load */ public class SpecConverter extends ReflectionConverter { SpecConverter() { @@ -462,7 +499,7 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co RegisteredType cat = lookupContext.lookupManagementContext().getTypeRegistry().get(catalogItemId); if (cat==null) throw new NoSuchElementException("catalog item: "+catalogItemId); BrooklynClassLoadingContext clcNew = CatalogUtils.newClassLoadingContext(lookupContext.lookupManagementContext(), cat); - pushXstreamCustomClassLoader(clcNew); + delegatingClassLoader.pushXstreamCustomClassLoader(clcNew); customLoaderSet = true; } @@ -473,7 +510,7 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co } finally { context.put("SpecConverter.instance", null); if (customLoaderSet) { - popXstreamCustomClassLoader(); + delegatingClassLoader.popXstreamCustomClassLoader(); } } } @@ -497,68 +534,99 @@ protected void instantiateNewInstanceSettingCache(HierarchicalStreamReader reade } } - Stack contexts = new Stack(); - Stack cls = new Stack(); - AtomicReference xstreamLockOwner = new AtomicReference(); - int lockCount; - - /** Must be accompanied by a corresponding {@link #popXstreamCustomClassLoader()} when finished. */ - @SuppressWarnings("deprecation") - protected void pushXstreamCustomClassLoader(BrooklynClassLoadingContext clcNew) { - acquireXstreamLock(); - BrooklynClassLoadingContext oldClc; - if (!contexts.isEmpty()) { - oldClc = contexts.peek(); - } else { - // TODO XmlMementoSerializer should take a BCLC instead of a CL - oldClc = JavaBrooklynClassLoadingContext.create(lookupContext.lookupManagementContext(), xstream.getClassLoader()); - } - BrooklynClassLoadingContextSequential clcMerged = new BrooklynClassLoadingContextSequential(lookupContext.lookupManagementContext(), - oldClc, clcNew); - contexts.push(clcMerged); - cls.push(xstream.getClassLoader()); - ClassLoader newCL = ClassLoaderFromBrooklynClassLoadingContext.of(clcMerged); - xstream.setClassLoader(newCL); - } + @VisibleForTesting + static class OsgiClassLoader extends ClassLoader { + private final Stack contexts = new Stack(); + private final Stack cls = new Stack(); + private final AtomicReference xstreamLockOwner = new AtomicReference(); + private ManagementContext mgmt; + private ClassLoader currentClassLoader; + private AtomicReference currentLoader = new AtomicReference<>(); + private int lockCount; + + protected OsgiClassLoader(ClassLoader classLoader) { + setCurrentClassLoader(classLoader); + } + + protected void setManagementContext(ManagementContext mgmt) { + this.mgmt = checkNotNull(mgmt, "mgmt"); + currentLoader.set(new ClassLoaderUtils(currentClassLoader, mgmt)); + } - protected void popXstreamCustomClassLoader() { - synchronized (xstreamLockOwner) { - releaseXstreamLock(); - xstream.setClassLoader(cls.pop()); - contexts.pop(); + @Override + protected Class findClass(String name) throws ClassNotFoundException { + return currentLoader.get().loadClass(name); } - } - - protected void acquireXstreamLock() { - synchronized (xstreamLockOwner) { - while (true) { - if (xstreamLockOwner.compareAndSet(null, Thread.currentThread()) || - Thread.currentThread().equals( xstreamLockOwner.get() )) { - break; - } - try { - xstreamLockOwner.wait(1000); - } catch (InterruptedException e) { - throw Exceptions.propagate(e); + + /** Must be accompanied by a corresponding {@link #popXstreamCustomClassLoader()} when finished. */ + @SuppressWarnings("deprecation") + protected void pushXstreamCustomClassLoader(BrooklynClassLoadingContext clcNew) { + acquireXstreamLock(); + BrooklynClassLoadingContext oldClc; + if (!contexts.isEmpty()) { + oldClc = contexts.peek(); + } else { + // TODO XmlMementoSerializer should take a BCLC instead of a CL + oldClc = JavaBrooklynClassLoadingContext.create(mgmt, getCurrentClassLoader()); + } + BrooklynClassLoadingContextSequential clcMerged = new BrooklynClassLoadingContextSequential(mgmt, oldClc, clcNew); + ClassLoader newCL = ClassLoaderFromBrooklynClassLoadingContext.of(clcMerged); + contexts.push(clcMerged); + cls.push(getCurrentClassLoader()); + setCurrentClassLoader(newCL); + } + + protected void popXstreamCustomClassLoader() { + synchronized (xstreamLockOwner) { + releaseXstreamLock(); + setCurrentClassLoader(cls.pop()); + contexts.pop(); + } + } + + private ClassLoader getCurrentClassLoader() { + return currentClassLoader; + } + + private void setCurrentClassLoader(ClassLoader classLoader) { + currentClassLoader = checkNotNull(classLoader); + if (mgmt != null) { + currentLoader.set(new ClassLoaderUtils(currentClassLoader, mgmt)); + } else { + currentLoader.set(new ClassLoaderUtils(currentClassLoader)); + } + } + + protected void acquireXstreamLock() { + synchronized (xstreamLockOwner) { + while (true) { + if (xstreamLockOwner.compareAndSet(null, Thread.currentThread()) || + Thread.currentThread().equals( xstreamLockOwner.get() )) { + break; + } + try { + xstreamLockOwner.wait(1000); + } catch (InterruptedException e) { + throw Exceptions.propagate(e); + } } + lockCount++; } - lockCount++; } - } - protected void releaseXstreamLock() { - synchronized (xstreamLockOwner) { - if (lockCount<=0) { - throw new IllegalStateException("xstream not locked"); - } - if (--lockCount == 0) { - if (!xstreamLockOwner.compareAndSet(Thread.currentThread(), null)) { - Thread oldOwner = xstreamLockOwner.getAndSet(null); - throw new IllegalStateException("xstream was locked by "+oldOwner+" but unlock attempt by "+Thread.currentThread()); + protected void releaseXstreamLock() { + synchronized (xstreamLockOwner) { + if (lockCount<=0) { + throw new IllegalStateException("xstream not locked"); + } + if (--lockCount == 0) { + if (!xstreamLockOwner.compareAndSet(Thread.currentThread(), null)) { + Thread oldOwner = xstreamLockOwner.getAndSet(null); + throw new IllegalStateException("xstream was locked by "+oldOwner+" but unlock attempt by "+Thread.currentThread()); + } + xstreamLockOwner.notifyAll(); } - xstreamLockOwner.notifyAll(); } } } - } diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java index 1f1ac2e7da..4aee9a80a0 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java @@ -976,9 +976,9 @@ protected LoadedClass load(Class bTyp return new LoadedClass((Class) catalogClass.get(), catalogItemId); } } - throw new IllegalStateException("No catalogItemId specified for "+contextSuchAsId+" and can't load class from either classpath or catalog items"); + throw new IllegalStateException("No catalogItemId specified for "+contextSuchAsId+" and can't load class (" + jType + ") from either classpath or catalog items"); } else { - throw new IllegalStateException("No catalogItemId specified for "+contextSuchAsId+" and can't load class from classpath"); + throw new IllegalStateException("No catalogItemId specified for "+contextSuchAsId+" and can't load class (" + jType + ") from classpath"); } } diff --git a/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java b/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java index 00fd05fca2..bc46b6012c 100644 --- a/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java +++ b/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java @@ -15,6 +15,8 @@ */ package org.apache.brooklyn.util.core; +import static com.google.common.base.Preconditions.checkNotNull; + import java.util.List; import java.util.regex.Pattern; @@ -41,6 +43,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.annotations.Beta; import com.google.common.base.Predicate; public class ClassLoaderUtils { @@ -69,27 +72,33 @@ public ClassLoaderUtils(Object callingObj, @Nullable ManagementContext mgmt) { } public ClassLoaderUtils(Class callingClass) { - this.classLoader = callingClass.getClassLoader(); + this.classLoader = checkNotNull(callingClass, "callingClass").getClassLoader(); this.entity = null; this.mgmt = null; } public ClassLoaderUtils(ClassLoader cl) { - this.classLoader = cl; + this.classLoader = checkNotNull(cl, "classLoader"); this.entity = null; this.mgmt = null; } + public ClassLoaderUtils(ClassLoader cl, @Nullable ManagementContext mgmt) { + this.classLoader = checkNotNull(cl, "classLoader"); + this.entity = null; + this.mgmt = checkNotNull(mgmt, "mgmt"); + } + public ClassLoaderUtils(Class callingClass, Entity entity) { - this.classLoader = callingClass.getClassLoader(); - this.entity = entity; + this.classLoader = checkNotNull(callingClass, "callingClass").getClassLoader(); + this.entity = checkNotNull(entity, "entity"); this.mgmt = ((EntityInternal)entity).getManagementContext(); } public ClassLoaderUtils(Class callingClass, @Nullable ManagementContext mgmt) { - this.classLoader = callingClass.getClassLoader(); + this.classLoader = checkNotNull(callingClass, "callingClass").getClassLoader(); this.entity = null; - this.mgmt = mgmt; + this.mgmt = checkNotNull(mgmt, "mgmt"); } public Class loadClass(String name) throws ClassNotFoundException { @@ -139,7 +148,9 @@ public Class loadClass(String name) throws ClassNotFoundException { try { // Used instead of callingClass.getClassLoader().loadClass(...) as it could be null (only for bootstrap classes) - return Class.forName(name, true, classLoader); + // Note that Class.forName(name, false, classLoader) doesn't seem to like us returning a + // class with a different name from that intended (e.g. stripping off an OSGi prefix). + return classLoader.loadClass(name); } catch (ClassNotFoundException e) { } @@ -181,6 +192,12 @@ public Class loadClass(String symbolicName, @Nullable String version, String } } + @Beta + public boolean isBundleWhiteListed(Bundle bundle) { + WhiteListBundlePredicate p = createBundleMatchingPredicate(); + return p.apply(bundle); + } + protected Framework getFramework() { if (mgmt != null) { Maybe osgiManager = ((ManagementContextInternal)mgmt).getOsgiManager(); @@ -206,11 +223,11 @@ private boolean looksLikeBundledClassName(String name) { private static class WhiteListBundlePredicate implements Predicate { - private Pattern symbolicName; - private Pattern version; + private final Pattern symbolicName; + private final Pattern version; private WhiteListBundlePredicate(String symbolicName, String version) { - this.symbolicName = Pattern.compile(symbolicName); + this.symbolicName = Pattern.compile(checkNotNull(symbolicName, "symbolicName")); this.version = version != null ? Pattern.compile(version) : null; } @@ -219,7 +236,6 @@ public boolean apply(Bundle input) { return symbolicName.matcher(input.getSymbolicName()).matches() && (version == null || version.matcher(input.getVersion().toString()).matches()); } - } private Class tryLoadFromBundleWhiteList(String name) { @@ -253,5 +269,4 @@ protected WhiteListBundlePredicate createBundleMatchingPredicate() { } return new WhiteListBundlePredicate(symbolicName, version); } - } diff --git a/core/src/main/java/org/apache/brooklyn/util/core/osgi/Osgis.java b/core/src/main/java/org/apache/brooklyn/util/core/osgi/Osgis.java index 82f8723c26..a33b4467e8 100644 --- a/core/src/main/java/org/apache/brooklyn/util/core/osgi/Osgis.java +++ b/core/src/main/java/org/apache/brooklyn/util/core/osgi/Osgis.java @@ -50,6 +50,7 @@ import com.google.common.annotations.Beta; import com.google.common.base.Function; import com.google.common.base.Joiner; +import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.base.Predicates; @@ -438,4 +439,9 @@ public VersionedName apply(org.apache.brooklyn.util.osgi.VersionedName input) { }); } + @Beta + public static Optional getBundleOf(Class clazz) { + Bundle bundle = org.osgi.framework.FrameworkUtil.getBundle(clazz); + return Optional.fromNullable(bundle); + } } diff --git a/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerDelegatingClassLoaderTest.java b/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerDelegatingClassLoaderTest.java new file mode 100644 index 0000000000..deb240af23 --- /dev/null +++ b/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerDelegatingClassLoaderTest.java @@ -0,0 +1,140 @@ +/* + * 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.core.mgmt.persist; + +import static org.testng.Assert.assertEquals; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.core.entity.AbstractEntity; +import org.apache.brooklyn.core.entity.Entities; +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.mgmt.persist.XmlMementoSerializer.OsgiClassLoader; +import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests; +import org.apache.brooklyn.util.core.osgi.Osgis; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.osgi.OsgiTestResources; +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.base.Optional; + +public class XmlMementoSerializerDelegatingClassLoaderTest { + + private LocalManagementContext mgmt; + + @BeforeMethod(alwaysRun=true) + public void setUp() throws Exception { + mgmt = LocalManagementContextForTests.builder(true).disableOsgi(false).build(); + } + + @AfterMethod(alwaysRun=true) + public void tearDown() throws Exception { + if (mgmt != null) { + Entities.destroyAll(mgmt); + } + } + + @Test + public void testLoadClassFromBundle() throws Exception { + ClassLoader classLoader = getClass().getClassLoader(); + Bundle apiBundle = getBundle(mgmt, "org.apache.brooklyn.api"); + Bundle coreBundle = getBundle(mgmt, "org.apache.brooklyn.core"); + + String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL; + Bundle otherBundle = installBundle(mgmt, bundleUrl); + + assertLoads(classLoader, Entity.class, Optional.of(apiBundle)); + assertLoads(classLoader, AbstractEntity.class, Optional.of(coreBundle)); + assertLoads(classLoader, OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY, Optional.of(otherBundle)); + } + + @Test + public void testLoadClassVanilla() throws Exception { + ClassLoader classLoader = getClass().getClassLoader(); + + assertLoads(classLoader, Entity.class, Optional.absent()); + assertLoads(classLoader, AbstractEntity.class, Optional.absent()); + } + + // Tests we can do funny stuff, like return a differently named class from that expected! + @Test + public void testLoadClassReturningDifferentlyNamedClass() throws Exception { + final String specialClassName = "my.madeup.Clazz"; + + ClassLoader classLoader = new ClassLoader() { + @Override + protected Class findClass(String name) throws ClassNotFoundException { + if (name != null && name.equals(specialClassName)) { + return Entity.class; + } + return getClass().getClassLoader().loadClass(name); + } + }; + + OsgiClassLoader ocl = new XmlMementoSerializer.OsgiClassLoader(classLoader); + ocl.setManagementContext(mgmt); + assertEquals(ocl.loadClass(specialClassName), Entity.class); + + // TODO The line below fails: java.lang.ClassNotFoundException: my/madeup/Clazz + //assertEquals(Class.forName(specialClassName, false, ocl).getName(), Entity.class.getName()); + } + + private void assertLoads(ClassLoader delegateClassLoader, Class clazz, Optional bundle) throws Exception { + OsgiClassLoader ocl = new XmlMementoSerializer.OsgiClassLoader(delegateClassLoader); + ocl.setManagementContext(mgmt); + String classname = (bundle.isPresent() ? bundle.get().getSymbolicName() + ":" : "") + clazz.getName(); + assertEquals(ocl.loadClass(classname), clazz); + + // TODO The line below fails, e.g.: java.lang.ClassNotFoundException: org/apache/brooklyn/api:org/apache/brooklyn/api/entity/Entity + //assertEquals(Class.forName(classname, false, ocl), clazz); + + } + + private void assertLoads(ClassLoader delegateClassLoader, String clazz, Optional bundle) throws Exception { + OsgiClassLoader ocl = new XmlMementoSerializer.OsgiClassLoader(delegateClassLoader); + ocl.setManagementContext(mgmt); + String classname = (bundle.isPresent() ? bundle.get().getSymbolicName() + ":" : "") + clazz; + assertEquals(ocl.loadClass(classname).getName(), clazz); + + // TODO The line below fails, e.g.: java.lang.ClassNotFoundException: org/apache/brooklyn/test/resources/osgi/brooklyn-test-osgi-entities:org/apache/brooklyn/test/osgi/entities/SimpleEntity + //assertEquals(Class.forName(classname, false, ocl).getName(), clazz); + } + + private Bundle getBundle(ManagementContext mgmt, final String symbolicName) throws Exception { + OsgiManager osgiManager = ((ManagementContextInternal)mgmt).getOsgiManager().get(); + Framework framework = osgiManager.getFramework(); + Maybe result = Osgis.bundleFinder(framework) + .symbolicName(symbolicName) + .find(); + return result.get(); + } + + private Bundle installBundle(ManagementContext mgmt, String bundleUrl) throws Exception { + OsgiManager osgiManager = ((ManagementContextInternal)mgmt).getOsgiManager().get(); + Framework framework = osgiManager.getFramework(); + return Osgis.install(framework, bundleUrl); + } +} diff --git a/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java b/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java index aefd6230cf..9251354e33 100644 --- a/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java +++ b/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java @@ -50,6 +50,9 @@ import org.apache.brooklyn.core.catalog.internal.CatalogItemDtoAbstract; import org.apache.brooklyn.core.entity.Entities; import org.apache.brooklyn.core.location.SimulatedLocation; +import org.apache.brooklyn.core.mgmt.ha.OsgiManager; +import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; +import org.apache.brooklyn.core.mgmt.osgi.OsgiStandaloneTest; import org.apache.brooklyn.core.mgmt.osgi.OsgiVersionMoreEntityTest; import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests; import org.apache.brooklyn.core.test.entity.TestApplication; @@ -59,16 +62,22 @@ import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.core.osgi.Osgis; +import org.apache.brooklyn.util.javalang.Reflections; import org.apache.brooklyn.util.net.Networking; import org.apache.brooklyn.util.net.UserAndHostAndPort; import org.apache.brooklyn.util.osgi.OsgiTestResources; +import org.osgi.framework.Bundle; +import org.osgi.framework.launch.Framework; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import com.google.api.client.repackaged.com.google.common.base.Joiner; import com.google.common.base.Objects; +import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -84,12 +93,21 @@ public class XmlMementoSerializerTest { private static final Logger LOG = LoggerFactory.getLogger(XmlMementoSerializerTest.class); private XmlMementoSerializer serializer; - + private ManagementContext mgmt; + @BeforeMethod(alwaysRun=true) public void setUp() throws Exception { serializer = new XmlMementoSerializer(XmlMementoSerializerTest.class.getClassLoader()); } + @AfterMethod(alwaysRun=true) + private void tearDown() { + if (mgmt != null) { + Entities.destroyAllCatching(mgmt); + mgmt = null; + } + } + @Test public void testRenamedClass() throws Exception { serializer = new XmlMementoSerializer(XmlMementoSerializerTest.class.getClassLoader(), @@ -337,23 +355,90 @@ public void testEntitySpecManyConcurrently() throws Exception { @Test public void testEntitySpecFromOsgi() throws Exception { TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_V1_PATH); - ManagementContext mgmt = LocalManagementContextForTests.builder(true).disableOsgi(false).build(); - try { - RegisteredType ci = OsgiVersionMoreEntityTest.addMoreEntityV1(mgmt, "1.0"); + mgmt = LocalManagementContextForTests.builder(true).disableOsgi(false).build(); + + RegisteredType ci = OsgiVersionMoreEntityTest.addMoreEntityV1(mgmt, "1.0"); - EntitySpec spec = EntitySpec.create(DynamicCluster.class) - .configure(DynamicCluster.INITIAL_SIZE, 1) - .configure(DynamicCluster.MEMBER_SPEC, mgmt.getTypeRegistry().createSpec(ci, null, EntitySpec.class)); + EntitySpec spec = EntitySpec.create(DynamicCluster.class) + .configure(DynamicCluster.INITIAL_SIZE, 1) + .configure(DynamicCluster.MEMBER_SPEC, mgmt.getTypeRegistry().createSpec(ci, null, EntitySpec.class)); + + serializer.setLookupContext(new LookupContextImpl(mgmt, + ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), + ImmutableList.of(), ImmutableList.of(), ImmutableList.>of(), true)); + assertSerializeAndDeserialize(spec); + } - serializer.setLookupContext(new LookupContextImpl(mgmt, + @Test + public void testOsgiBundleNameNotIncludedForWhiteListed() throws Exception { + mgmt = LocalManagementContextForTests.builder(true).disableOsgi(false).build(); + + serializer.setLookupContext(new LookupContextImpl(mgmt, + ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), + ImmutableList.of(), ImmutableList.of(), ImmutableList.>of(), true)); + + Object obj = PersistMode.AUTO; + + assertSerializeAndDeserialize(obj); + + // i.e. not pre-pended with "org.apache.brooklyn.core:" + String expectedForm = "<"+PersistMode.class.getName()+">AUTO"; + String serializedForm = serializer.toString(obj); + assertEquals(serializedForm.trim(), expectedForm.trim()); + } + + @Test + public void testOsgiBundleNamePrefixIncluded() throws Exception { + String bundlePath = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH; + String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL; + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), bundlePath); + + mgmt = LocalManagementContextForTests.builder(true).disableOsgi(false).build(); + + serializer.setLookupContext(new LookupContextImpl(mgmt, ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), ImmutableList.>of(), true)); - assertSerializeAndDeserialize(spec); - } finally { - Entities.destroyAllCatching(mgmt); - } + + Bundle bundle = installBundle(mgmt, bundleUrl); + + String classname = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_OBJECT; + Class osgiObjectClazz = bundle.loadClass(classname); + Object obj = Reflections.invokeConstructorWithArgs(osgiObjectClazz, "myval").get(); + + assertSerializeAndDeserialize(obj); + + // i.e. prepended with bundle name + String expectedForm = Joiner.on("\n").join( + "<"+bundle.getSymbolicName()+":"+classname+">", + " myval", + ""); + String serializedForm = serializer.toString(obj); + assertEquals(serializedForm.trim(), expectedForm.trim()); } + + // TODO This doesn't get the bundleName - should we expect it to? Is this because of + // how we're using Felix? Would it also be true in Karaf? + @Test(groups="Broken") + public void testOsgiBundleNamePrefixIncludedForDownstreamDependency() throws Exception { + mgmt = LocalManagementContextForTests.builder(true).disableOsgi(false).build(); + serializer.setLookupContext(new LookupContextImpl(mgmt, + ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), + ImmutableList.of(), ImmutableList.of(), ImmutableList.>of(), true)); + + // Using a guava type (which is a downstream dependency of Brooklyn) + String bundleName = "com.goole.guava"; + String classname = "com.google.common.base.Predicates_-ObjectPredicate"; + Object obj = Predicates.alwaysTrue(); + + assertSerializeAndDeserialize(obj); + + // i.e. prepended with bundle name + String expectedForm = "<"+bundleName+":"+classname+">ALWAYS_TRUE"; + String serializedForm = serializer.toString(obj); + assertEquals(serializedForm.trim(), expectedForm.trim()); + } + @Test public void testImmutableCollectionsWithDanglingEntityRef() throws Exception { // If there's a dangling entity in an ImmutableList etc, then discard it entirely. @@ -647,4 +732,10 @@ public MyStaticInner(String val) { return Objects.hashCode(myStaticInnerField); } } + + private Bundle installBundle(ManagementContext mgmt, String bundleUrl) throws Exception { + OsgiManager osgiManager = ((ManagementContextInternal)mgmt).getOsgiManager().get(); + Framework framework = osgiManager.getFramework(); + return Osgis.install(framework, bundleUrl); + } } \ No newline at end of file diff --git a/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java b/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java index bfa5adca6c..3f2a8e119e 100644 --- a/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java +++ b/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java @@ -19,6 +19,8 @@ package org.apache.brooklyn.util.core; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.mgmt.ManagementContext; @@ -132,6 +134,16 @@ public void testLoadClassInOsgiApi() throws Exception { assertEquals(clu.loadClass(bundle.getSymbolicName() + ":" + bundle.getVersion() + ":" + classname), clazz); } + @Test + public void testIsBundleWhiteListed() throws Exception { + mgmt = LocalManagementContextForTests.builder(true).disableOsgi(false).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"))); + } + private Bundle installBundle(ManagementContext mgmt, String bundleUrl) throws Exception { OsgiManager osgiManager = ((ManagementContextInternal)mgmt).getOsgiManager().get(); Framework framework = osgiManager.getFramework(); diff --git a/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleApplication.java b/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleApplication.java index dcfb495aea..f561b97625 100644 --- a/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleApplication.java +++ b/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleApplication.java @@ -18,7 +18,6 @@ */ package org.apache.brooklyn.test.osgi.entities; - import org.apache.brooklyn.api.entity.ImplementedBy; import org.apache.brooklyn.core.entity.StartableApplication; diff --git a/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleApplicationImpl.java b/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleApplicationImpl.java index fe6f1a2c48..ae4c69ddda 100644 --- a/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleApplicationImpl.java +++ b/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleApplicationImpl.java @@ -18,7 +18,6 @@ */ package org.apache.brooklyn.test.osgi.entities; - import org.apache.brooklyn.core.entity.AbstractApplication; import org.apache.brooklyn.core.entity.StartableApplication; diff --git a/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleEntity.java b/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleEntity.java index ffed15f306..f256af76cb 100644 --- a/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleEntity.java +++ b/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleEntity.java @@ -19,13 +19,20 @@ package org.apache.brooklyn.test.osgi.entities; +import org.apache.brooklyn.api.catalog.Catalog; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.entity.ImplementedBy; -import org.apache.brooklyn.api.catalog.Catalog; +import org.apache.brooklyn.api.sensor.AttributeSensor; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.sensor.Sensors; @Catalog(name="A Simple Entity", description="Simple entity for test purposes") @ImplementedBy(SimpleEntityImpl.class) public interface SimpleEntity extends Entity { + + ConfigKey SIMPLE_CONFIG = ConfigKeys.newConfigKey(Object.class, "simple.config"); + AttributeSensor SIMPLE_SENSOR = Sensors.newSensor(Object.class, "simple.sensor"); } diff --git a/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleEntityImpl.java b/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleEntityImpl.java index 2595c396cb..16ecd68000 100644 --- a/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleEntityImpl.java +++ b/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleEntityImpl.java @@ -20,7 +20,6 @@ import org.apache.brooklyn.core.entity.AbstractEntity; - public class SimpleEntityImpl extends AbstractEntity implements SimpleEntity { } diff --git a/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleObject.java b/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleObject.java new file mode 100644 index 0000000000..dad1ab2f82 --- /dev/null +++ b/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimpleObject.java @@ -0,0 +1,56 @@ +/* + * 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.test.osgi.entities; + +public class SimpleObject { + + private String val; + + public SimpleObject() { + } + + public SimpleObject(String val) { + this.val = val; + } + + public String getVal() { + return val; + } + + public void setVal(String val) { + this.val = val; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SimpleObject)) return false; + String oVal = ((SimpleObject)obj).val; + return (val == null) ? oVal == null : val.equals(oVal); + } + + @Override + public int hashCode() { + return (val == null) ? 0 : val.hashCode(); + } + + @Override + public String toString() { + return "SimpleObject["+val+"]"; + } +} diff --git a/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimplePolicy.java b/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimplePolicy.java index fdea82123b..f6589c5462 100644 --- a/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimplePolicy.java +++ b/utils/common/dependencies/osgi/entities/src/main/java/org/apache/brooklyn/test/osgi/entities/SimplePolicy.java @@ -18,7 +18,6 @@ */ package org.apache.brooklyn.test.osgi.entities; - import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.policy.AbstractPolicy; @@ -33,4 +32,9 @@ public class SimplePolicy extends AbstractPolicy { @SetFromFlag("config3") public static final ConfigKey CONFIG3 = ConfigKeys.newStringConfigKey("config3"); + + @Override + protected void doReconfigureConfig(ConfigKey key, T val) { + // no-op; allow any config to be set + } } diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java b/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java index df36a8006a..2d89be278d 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java @@ -15,6 +15,10 @@ */ package org.apache.brooklyn.util.osgi; +import static com.google.common.base.Preconditions.checkNotNull; + +import javax.annotation.Nullable; + import com.google.common.base.Objects; import org.apache.brooklyn.util.text.Strings; import org.osgi.framework.Bundle; @@ -29,12 +33,11 @@ public class VersionedName { private final Version version; public VersionedName(Bundle b) { - this.symbolicName = b.getSymbolicName(); - this.version = b.getVersion(); + this(b.getSymbolicName(), b.getVersion()); } - public VersionedName(String symbolicName, Version version) { - this.symbolicName = symbolicName; + public VersionedName(String symbolicName, @Nullable Version version) { + this.symbolicName = checkNotNull(symbolicName, "symbolicName"); this.version = version; } diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/osgi/OsgiTestResources.java b/utils/common/src/test/java/org/apache/brooklyn/util/osgi/OsgiTestResources.java index 21827a04da..181849dafa 100644 --- a/utils/common/src/test/java/org/apache/brooklyn/util/osgi/OsgiTestResources.java +++ b/utils/common/src/test/java/org/apache/brooklyn/util/osgi/OsgiTestResources.java @@ -44,6 +44,9 @@ public class OsgiTestResources { public static final String BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_APPLICATION = "org.apache.brooklyn.test.osgi.entities.SimpleApplication"; public static final String BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY = "org.apache.brooklyn.test.osgi.entities.SimpleEntity"; public static final String BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_POLICY = "org.apache.brooklyn.test.osgi.entities.SimplePolicy"; + public static final String BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_OBJECT = "org.apache.brooklyn.test.osgi.entities.SimpleObject"; + public static final String BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY_CONFIG_NAME = "simple.config"; + public static final String BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY_SENSOR_NAME = "simple.sensor"; /** * brooklyn-test-osgi-more-entities_0.1.0 - From 2654305d4f4bc5f9d1a45bffb363996c4eb6e6fc Mon Sep 17 00:00:00 2001 From: Aled Sage Date: Thu, 7 Jul 2016 14:34:04 +0100 Subject: [PATCH 4/7] Change classloading order in ClassLoaderUtils --- .../camp/brooklyn/RebindOsgiTest.java | 63 +++++++++++++++++++ .../brooklyn/util/core/ClassLoaderUtils.java | 63 ++++++++++++++++--- .../persist/XmlMementoSerializerTest.java | 8 +++ .../util/core/ClassLoaderUtilsTest.java | 20 ++++++ 4 files changed, 145 insertions(+), 9 deletions(-) diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/RebindOsgiTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/RebindOsgiTest.java index 752b066f0c..46387da635 100644 --- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/RebindOsgiTest.java +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/RebindOsgiTest.java @@ -37,6 +37,7 @@ 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.mgmt.osgi.OsgiVersionMoreEntityTest; import org.apache.brooklyn.core.sensor.Sensors; import org.apache.brooklyn.core.test.entity.TestEntity; import org.apache.brooklyn.util.core.osgi.Osgis; @@ -321,6 +322,62 @@ public void testBrooklynObjectDslFromCatalogOsgi() throws Exception { assertEquals(entity2.getCatalogItemId(), appSymbolicName+":"+appVersion); } + /** + * Installs a newer version of the bundle than that used in the catalog. Then creates an + * app with that catalog item, and rebinds. Confirms that we use our explicit version, rather + * that the newer version available in the OSGi container. + */ + @Test + public void testUsesCatalogBundleVersion() throws Exception { + String bundleV1Url = OsgiVersionMoreEntityTest.BROOKLYN_TEST_MORE_ENTITIES_V1_URL; + String bundleV2Url = OsgiVersionMoreEntityTest.BROOKLYN_TEST_MORE_ENTITIES_V2_URL; + String bundleEntityType = "org.apache.brooklyn.test.osgi.entities.more.MoreEntity"; + String bundleObjectType = "org.apache.brooklyn.test.osgi.entities.more.MoreObject"; + String v1Version = "0.1.0"; + + installBundle(mgmt(), bundleV2Url); + bundleUrlsToInstallOnRebind.add(bundleV2Url); + + String appSymbolicName = "my.catalog.app.id.load"; + String appVersion = "0.1.0"; + String appCatalogFormat = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + appSymbolicName, + " version: " + appVersion, + " itemType: entity", + " libraries:", + " - " + bundleV1Url, + " item:", + " type: " + bundleEntityType, + " brooklyn.config:", + " my.conf:", + " $brooklyn:object:", + " type: " + bundleObjectType, + " object.fields:", + " val: myEntityVal"); + + Iterables.getOnlyElement(addCatalogItems(String.format(appCatalogFormat, appVersion))); + + String appBlueprintYaml = Joiner.on("\n").join( + "location: localhost\n", + "services:", + "- type: " + CatalogUtils.getVersionedId(appSymbolicName, appVersion)); + origApp = (StartableApplication) createAndStartApplication(appBlueprintYaml); + Entity origEntity = Iterables.getOnlyElement(origApp.getChildren()); + Object configVal = origEntity.config().get(ConfigKeys.newConfigKey(Object.class, "my.conf")); + assertBundleVersionOf(Entities.deproxy(origEntity), v1Version); + assertBundleVersionOf(configVal, v1Version); + + // Rebind + rebind(); + + // Ensure entity/config is loaded from the explicit catalog version + Entity newEntity = Iterables.getOnlyElement(newApp.getChildren()); + Object newConfigVal = newEntity.config().get(ConfigKeys.newConfigKey(Object.class, "my.conf")); + assertBundleVersionOf(Entities.deproxy(newEntity), v1Version); + assertBundleVersionOf(newConfigVal, v1Version); + } + // TODO Does not do rebind; the config isn't there after rebind. // Need to reproduce that in a simpler use-case. @Test @@ -395,4 +452,10 @@ private Bundle installBundle(ManagementContext mgmt, String bundleUrl) throws Ex Framework framework = osgiManager.getFramework(); return Osgis.install(framework, bundleUrl); } + + protected void assertBundleVersionOf(Object obj, String expectedVersion) { + assertNotNull(obj); + Class clazz = (obj instanceof Class) ? (Class)obj : obj.getClass(); + assertEquals(Osgis.getBundleOf(clazz).get().getVersion().toString(), expectedVersion); + } } diff --git a/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java b/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java index bc46b6012c..b46f68dafd 100644 --- a/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java +++ b/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java @@ -101,12 +101,44 @@ public ClassLoaderUtils(Class callingClass, @Nullable ManagementContext mgmt) this.mgmt = checkNotNull(mgmt, "mgmt"); } + /** + * Loads the given class, handle OSGi bundles. The class could be in one of the following formats: + *
    + *
  • {@code }, such as {@code com.google.common.net.HostAndPort} + *
  • {@code :}, such as {@code com.google.guava:com.google.common.net.HostAndPort} + *
  • {@code ::}, such as {@code com.google.guava:16.0.1:com.google.common.net.HostAndPort} + *
+ * + * The classloading order is as follows: + *
    + *
  1. If the class explicitly states the bundle name and version, then load from that. + *
  2. 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. + *
  3. 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). + *
  4. 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. + *
  5. The {@link ManagementContext#getCatalogClassLoader()}. Again, this is normally not helpful. + * We instead would prefer the specific catalog item's classloader (which we tried earlier). + *
  6. 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. + *
+ * + * 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 { + String symbolicName; + String version; + String className; + if (looksLikeBundledClassName(name)) { String[] arr = name.split(CLASS_NAME_DELIMITER); - String symbolicName; - String version; - String className; 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) { @@ -120,9 +152,17 @@ public Class loadClass(String name) throws ClassNotFoundException { } else { throw new IllegalStateException("'" + name + "' contains a bundle:version:class delimiter, but only one of those specified"); } - return loadClass(symbolicName, version, className); + } else { + symbolicName = null; + version = null; + className = name; } + if (symbolicName != null && version != null) { + // Very explicit; do as we're told! + return loadClass(symbolicName, version, className); + } + if (entity != null && mgmt != null) { String catalogItemId = entity.getCatalogItemId(); if (catalogItemId != null) { @@ -130,7 +170,7 @@ public Class loadClass(String name) throws ClassNotFoundException { if (item != null) { BrooklynClassLoadingContext loader = CatalogUtils.newClassLoadingContext(mgmt, item); try { - return loader.loadClass(name); + return loader.loadClass(className); } catch (IllegalStateException e) { ClassNotFoundException cnfe = Exceptions.getFirstThrowableOfType(e, ClassNotFoundException.class); NoClassDefFoundError ncdfe = Exceptions.getFirstThrowableOfType(e, NoClassDefFoundError.class); @@ -146,11 +186,16 @@ public Class loadClass(String name) throws ClassNotFoundException { } } + Class cls = tryLoadFromBundleWhiteList(name); + if (cls != null) { + return cls; + } + try { // Used instead of callingClass.getClassLoader().loadClass(...) as it could be null (only for bootstrap classes) // Note that Class.forName(name, false, classLoader) doesn't seem to like us returning a // class with a different name from that intended (e.g. stripping off an OSGi prefix). - return classLoader.loadClass(name); + return classLoader.loadClass(className); } catch (ClassNotFoundException e) { } @@ -161,9 +206,9 @@ public Class loadClass(String name) throws ClassNotFoundException { } } - Class cls = tryLoadFromBundleWhiteList(name); - if (cls != null) { - return cls; + if (symbolicName != null) { + // Finally fall back to loading from any version of the bundle + return loadClass(symbolicName, version, className); } else { throw new ClassNotFoundException("Class " + name + " not found on the application class path, nor in the bundle white list."); } diff --git a/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java b/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java index 9251354e33..e26983b00d 100644 --- a/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java +++ b/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java @@ -43,11 +43,13 @@ import org.apache.brooklyn.api.objs.BrooklynObject; import org.apache.brooklyn.api.objs.BrooklynObjectType; import org.apache.brooklyn.api.policy.Policy; +import org.apache.brooklyn.api.sensor.AttributeSensor; import org.apache.brooklyn.api.sensor.Enricher; import org.apache.brooklyn.api.sensor.Feed; import org.apache.brooklyn.api.typereg.RegisteredType; import org.apache.brooklyn.core.catalog.internal.CatalogItemBuilder; import org.apache.brooklyn.core.catalog.internal.CatalogItemDtoAbstract; +import org.apache.brooklyn.core.entity.Attributes; import org.apache.brooklyn.core.entity.Entities; import org.apache.brooklyn.core.location.SimulatedLocation; import org.apache.brooklyn.core.mgmt.ha.OsgiManager; @@ -262,6 +264,12 @@ public void testImmutableMap() throws Exception { assertSerializeAndDeserialize(obj); } + @Test + public void testSensorWithTypeToken() throws Exception { + AttributeSensor> obj = Attributes.SERVICE_NOT_UP_DIAGNOSTICS; + assertSerializeAndDeserialize(obj); + } + @Test public void testClass() throws Exception { Class t = XmlMementoSerializer.class; diff --git a/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java b/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java index 3f2a8e119e..98a93ef934 100644 --- a/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java +++ b/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java @@ -41,6 +41,8 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import com.google.common.collect.ImmutableList; + public class ClassLoaderUtilsTest { private LocalManagementContext mgmt; @@ -144,6 +146,24 @@ public void testIsBundleWhiteListed() throws Exception { 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).disableOsgi(false).build(); + ClassLoaderUtils clu = new ClassLoaderUtils(getClass(), mgmt); + + String bundleUrl = "http://search.maven.org/remotecontent?filepath=com/google/guava/guava/18.0/guava-18.0.jar"; + Bundle bundle = installBundle(mgmt, bundleUrl); + String bundleName = bundle.getSymbolicName(); + + String classname = bundleName + ":" + ImmutableList.class.getName(); + Class clazz = clu.loadClass(classname); + assertEquals(clazz, ImmutableList.class); + } + private Bundle installBundle(ManagementContext mgmt, String bundleUrl) throws Exception { OsgiManager osgiManager = ((ManagementContextInternal)mgmt).getOsgiManager().get(); Framework framework = osgiManager.getFramework(); From 77357873a1feec996cd6b3f8e98f01e6999c70ea Mon Sep 17 00:00:00 2001 From: Aled Sage Date: Thu, 7 Jul 2016 23:05:38 +0100 Subject: [PATCH 5/7] Handle wrapped ClassNotFoundException And try not to wrap it! --- ...LoaderFromBrooklynClassLoadingContext.java | 11 ++-- .../brooklyn/util/core/ClassLoaderUtils.java | 23 +++++-- ...erFromBrooklynClassLoadingContextTest.java | 65 +++++++++++++++++++ 3 files changed, 87 insertions(+), 12 deletions(-) create mode 100644 core/src/test/java/org/apache/brooklyn/core/mgmt/classloading/ClassLoaderFromBrooklynClassLoadingContextTest.java diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/classloading/ClassLoaderFromBrooklynClassLoadingContext.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/classloading/ClassLoaderFromBrooklynClassLoadingContext.java index f36e2ac3aa..35d3b05c44 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/classloading/ClassLoaderFromBrooklynClassLoadingContext.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/classloading/ClassLoaderFromBrooklynClassLoadingContext.java @@ -21,6 +21,7 @@ import java.net.URL; import org.apache.brooklyn.api.mgmt.classloading.BrooklynClassLoadingContext; +import org.apache.brooklyn.util.guava.Maybe; public class ClassLoaderFromBrooklynClassLoadingContext extends ClassLoader { @@ -36,15 +37,15 @@ protected ClassLoaderFromBrooklynClassLoadingContext(BrooklynClassLoadingContext } @Override - public Class findClass(String className) throws ClassNotFoundException { - Class result = clc.loadClass(className); - if (result!=null) return result; + protected Class findClass(String className) throws ClassNotFoundException { + Maybe> result = clc.tryLoadClass(className); + if (result.isPresent()) return result.get(); // last resort. see comment in XStream CompositeClassLoader ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); if (contextClassLoader != null) { - result = contextClassLoader.loadClass(className); - if (result!=null) return result; + Class result2 = contextClassLoader.loadClass(className); + if (result2 != null) return result2; } return null; } diff --git a/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java b/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java index b46f68dafd..b6d39e911c 100644 --- a/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java +++ b/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java @@ -172,13 +172,7 @@ public Class loadClass(String name) throws ClassNotFoundException { try { return loader.loadClass(className); } catch (IllegalStateException e) { - ClassNotFoundException cnfe = Exceptions.getFirstThrowableOfType(e, ClassNotFoundException.class); - NoClassDefFoundError ncdfe = Exceptions.getFirstThrowableOfType(e, NoClassDefFoundError.class); - if (cnfe == null && ncdfe == null) { - throw e; - } else { - // ignore, fall back to Class.forName(...) - } + propagateIfCauseNotClassNotFound(e); } } else { log.warn("Entity " + entity + " refers to non-existent catalog item " + catalogItemId + ". Trying to load class " + name); @@ -196,12 +190,16 @@ public Class loadClass(String name) throws ClassNotFoundException { // Note that Class.forName(name, false, classLoader) doesn't seem to like us returning a // class with a different name from that intended (e.g. stripping off an OSGi prefix). return classLoader.loadClass(className); + } catch (IllegalStateException e) { + propagateIfCauseNotClassNotFound(e); } catch (ClassNotFoundException e) { } if (mgmt != null) { try { return mgmt.getCatalogClassLoader().loadClass(name); + } catch (IllegalStateException e) { + propagateIfCauseNotClassNotFound(e); } catch (ClassNotFoundException e) { } } @@ -214,6 +212,17 @@ public Class loadClass(String name) throws ClassNotFoundException { } } + protected void propagateIfCauseNotClassNotFound(IllegalStateException e) { + // TODO loadClass() should not throw IllegalStateException; should throw ClassNotFoundException without wrapping. + ClassNotFoundException cnfe = Exceptions.getFirstThrowableOfType(e, ClassNotFoundException.class); + NoClassDefFoundError ncdfe = Exceptions.getFirstThrowableOfType(e, NoClassDefFoundError.class); + if (cnfe == null && ncdfe == null) { + throw e; + } else { + // ignore, try next way of loading + } + } + public Class loadClass(String symbolicName, @Nullable String version, String className) throws ClassNotFoundException { Framework framework = getFramework(); if (framework != null) { diff --git a/core/src/test/java/org/apache/brooklyn/core/mgmt/classloading/ClassLoaderFromBrooklynClassLoadingContextTest.java b/core/src/test/java/org/apache/brooklyn/core/mgmt/classloading/ClassLoaderFromBrooklynClassLoadingContextTest.java new file mode 100644 index 0000000000..997ff5c7a6 --- /dev/null +++ b/core/src/test/java/org/apache/brooklyn/core/mgmt/classloading/ClassLoaderFromBrooklynClassLoadingContextTest.java @@ -0,0 +1,65 @@ +/* + * 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.core.mgmt.classloading; + +import org.apache.brooklyn.api.catalog.CatalogItem; +import org.apache.brooklyn.api.mgmt.classloading.BrooklynClassLoadingContext; +import org.apache.brooklyn.api.typereg.OsgiBundleWithUrl; +import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext; +import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests; +import org.apache.brooklyn.entity.stock.BasicApplication; +import org.apache.brooklyn.test.Asserts; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableList; + +public class ClassLoaderFromBrooklynClassLoadingContextTest { + + private LocalManagementContext mgmt; + private CatalogItem item; + private ClassLoaderFromBrooklynClassLoadingContext loader; + + @BeforeMethod(alwaysRun=true) + @SuppressWarnings("deprecation") + public void setUp() throws Exception { + mgmt = LocalManagementContextForTests.builder(true).disableOsgi(false).build(); + item = mgmt.getCatalog().addItem(BasicApplication.class); + + BrooklynClassLoadingContext clc = new OsgiBrooklynClassLoadingContext(mgmt, item.getCatalogItemId(), ImmutableList.of()); + loader = new ClassLoaderFromBrooklynClassLoadingContext(clc); + } + + @AfterMethod(alwaysRun=true) + public void tearDown() throws Exception { + if (mgmt != null) { + Entities.destroyAll(mgmt); + mgmt = null; + } + } + + @Test + public void testLoadNonExistantClassThrowsClassNotFound() throws Exception { + try { + loader.loadClass("my.clazz.does.not.Exist"); + Asserts.shouldHaveFailedPreviously(); + } catch (ClassNotFoundException e) { + // success; + } + } +} From cde747f39a2af547fd0d4c7da02ffdf168de3e1f Mon Sep 17 00:00:00 2001 From: Aled Sage Date: Thu, 7 Jul 2016 23:40:28 +0100 Subject: [PATCH 6/7] OSGi classloading: incorporate review comments --- .../camp/brooklyn/RebindOsgiTest.java | 20 +++++++++++++++++-- .../brooklyn/util/core/ClassLoaderUtils.java | 11 ++++++---- .../persist/XmlMementoSerializerTest.java | 2 +- .../util/core/ClassLoaderUtilsTest.java | 7 +++++++ 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/RebindOsgiTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/RebindOsgiTest.java index 46387da635..2f2f46287f 100644 --- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/RebindOsgiTest.java +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/RebindOsgiTest.java @@ -40,6 +40,7 @@ import org.apache.brooklyn.core.mgmt.osgi.OsgiVersionMoreEntityTest; import org.apache.brooklyn.core.sensor.Sensors; import org.apache.brooklyn.core.test.entity.TestEntity; +import org.apache.brooklyn.test.support.TestResourceUnavailableException; import org.apache.brooklyn.util.core.osgi.Osgis; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.guava.Maybe; @@ -62,8 +63,10 @@ public class RebindOsgiTest extends AbstractYamlRebindTest { + @SuppressWarnings("unused") private static final Logger LOG = LoggerFactory.getLogger(RebindOsgiTest.class); + private static final String OSGI_BUNDLE_PATH = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH; private static final String OSGI_BUNDLE_URL = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL; private static final String OSGI_BUNDLE_SYMBOLIC_NAME = "org.apache.brooklyn.test.resources.osgi.brooklyn-test-osgi-entities"; private static final String OSGI_ENTITY_TYPE = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY; @@ -155,6 +158,8 @@ public void testValInEntity(Object val, boolean useOsgi) throws Exception { @Test public void testValInEntityFromOtherBundle() throws Exception { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OSGI_BUNDLE_PATH); + installBundle(mgmt(), OSGI_BUNDLE_URL); bundleUrlsToInstallOnRebind.add(OSGI_BUNDLE_URL); @@ -180,6 +185,8 @@ public void testValInEntityFromOtherBundle() throws Exception { @Test public void testEntityAndPolicyFromCatalogOsgi() throws Exception { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OSGI_BUNDLE_PATH); + String appSymbolicName = "my.catalog.app.id.load"; String appVersion = "0.1.0"; String appCatalogFormat = Joiner.on("\n").join( @@ -228,6 +235,8 @@ public void testEntityAndPolicyFromCatalogOsgi() throws Exception { @Test public void testJavaPojoFromCatalogOsgi() throws Exception { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OSGI_BUNDLE_PATH); + String appSymbolicName = "my.catalog.app.id.load"; String appVersion = "0.1.0"; String appCatalogFormat = Joiner.on("\n").join( @@ -270,6 +279,8 @@ public void testJavaPojoFromCatalogOsgi() throws Exception { @Test public void testBrooklynObjectDslFromCatalogOsgi() throws Exception { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OSGI_BUNDLE_PATH); + String appSymbolicName = "my.catalog.app.id.load"; String appVersion = "0.1.0"; String appCatalogFormat = Joiner.on("\n").join( @@ -329,6 +340,9 @@ public void testBrooklynObjectDslFromCatalogOsgi() throws Exception { */ @Test public void testUsesCatalogBundleVersion() throws Exception { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiVersionMoreEntityTest.BROOKLYN_TEST_MORE_ENTITIES_V1_PATH); + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiVersionMoreEntityTest.BROOKLYN_TEST_MORE_ENTITIES_V2_PATH); + String bundleV1Url = OsgiVersionMoreEntityTest.BROOKLYN_TEST_MORE_ENTITIES_V1_URL; String bundleV2Url = OsgiVersionMoreEntityTest.BROOKLYN_TEST_MORE_ENTITIES_V2_URL; String bundleEntityType = "org.apache.brooklyn.test.osgi.entities.more.MoreEntity"; @@ -382,6 +396,8 @@ public void testUsesCatalogBundleVersion() throws Exception { // Need to reproduce that in a simpler use-case. @Test public void testBrooklynObjectDslFromCatalogOsgiInPolicy() throws Exception { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OSGI_BUNDLE_PATH); + String appSymbolicName = "my.catalog.app.id.load"; String appVersion = "0.1.0"; String appCatalogFormat = Joiner.on("\n").join( @@ -429,7 +445,7 @@ private Bundle getBundle(ManagementContext mgmt, final String symbolicName) thro private Object newOsgiSimpleObject(String val) throws Exception { Class osgiObjectClazz = getBundle(mgmt(), OSGI_BUNDLE_SYMBOLIC_NAME).loadClass(OSGI_OBJECT_TYPE); - return Reflections.invokeConstructorWithArgs(osgiObjectClazz, val).get(); + return Reflections.invokeConstructorFromArgs(osgiObjectClazz, val).get(); } private void assertOsgiSimpleObjectsEqual(Object val1, Object val2) throws Exception { @@ -444,7 +460,7 @@ private void assertOsgiSimpleObjectsEqual(Object val1, Object val2) throws Excep private String getOsgiSimpleObjectsVal(Object val) throws Exception { assertNotNull(val); - return (String) Reflections.invokeMethodWithArgs(val, "getVal", ImmutableList.of()).get(); + return (String) Reflections.invokeMethodFromArgs(val, "getVal", ImmutableList.of()).get(); } private Bundle installBundle(ManagementContext mgmt, String bundleUrl) throws Exception { diff --git a/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java b/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java index b6d39e911c..179cc4fd72 100644 --- a/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java +++ b/core/src/main/java/org/apache/brooklyn/util/core/ClassLoaderUtils.java @@ -72,7 +72,8 @@ public ClassLoaderUtils(Object callingObj, @Nullable ManagementContext mgmt) { } public ClassLoaderUtils(Class callingClass) { - this.classLoader = checkNotNull(callingClass, "callingClass").getClassLoader(); + checkNotNull(callingClass, "callingClass"); + this.classLoader = (callingClass.getClassLoader() != null) ? callingClass.getClassLoader() : getClass().getClassLoader(); this.entity = null; this.mgmt = null; } @@ -90,13 +91,15 @@ public ClassLoaderUtils(ClassLoader cl, @Nullable ManagementContext mgmt) { } public ClassLoaderUtils(Class callingClass, Entity entity) { - this.classLoader = checkNotNull(callingClass, "callingClass").getClassLoader(); + checkNotNull(callingClass, "callingClass"); + this.classLoader = (callingClass.getClassLoader() != null) ? callingClass.getClassLoader() : getClass().getClassLoader(); this.entity = checkNotNull(entity, "entity"); this.mgmt = ((EntityInternal)entity).getManagementContext(); } public ClassLoaderUtils(Class callingClass, @Nullable ManagementContext mgmt) { - this.classLoader = checkNotNull(callingClass, "callingClass").getClassLoader(); + checkNotNull(callingClass, "callingClass"); + this.classLoader = (callingClass.getClassLoader() != null) ? callingClass.getClassLoader() : getClass().getClassLoader(); this.entity = null; this.mgmt = checkNotNull(mgmt, "mgmt"); } @@ -180,7 +183,7 @@ public Class loadClass(String name) throws ClassNotFoundException { } } - Class cls = tryLoadFromBundleWhiteList(name); + Class cls = tryLoadFromBundleWhiteList(className); if (cls != null) { return cls; } diff --git a/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java b/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java index e26983b00d..069416e941 100644 --- a/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java +++ b/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java @@ -411,7 +411,7 @@ public void testOsgiBundleNamePrefixIncluded() throws Exception { String classname = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_OBJECT; Class osgiObjectClazz = bundle.loadClass(classname); - Object obj = Reflections.invokeConstructorWithArgs(osgiObjectClazz, "myval").get(); + Object obj = Reflections.invokeConstructorFromArgs(osgiObjectClazz, "myval").get(); assertSerializeAndDeserialize(obj); diff --git a/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java b/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java index 98a93ef934..b5bfbc2b85 100644 --- a/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java +++ b/core/src/test/java/org/apache/brooklyn/util/core/ClassLoaderUtilsTest.java @@ -31,6 +31,7 @@ import org.apache.brooklyn.core.mgmt.osgi.OsgiStandaloneTest; import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests; import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.test.support.TestResourceUnavailableException; import org.apache.brooklyn.util.core.osgi.Osgis; import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.osgi.OsgiTestResources; @@ -77,9 +78,12 @@ public void testLoadClassNotInOsgi() throws Exception { @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).disableOsgi(false).build(); Bundle bundle = installBundle(mgmt, bundleUrl); @@ -92,9 +96,12 @@ public void testLoadClassInOsgi() throws Exception { @Test public void testLoadClassInOsgiWhiteList() 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).disableOsgi(false).build(); Bundle bundle = installBundle(mgmt, bundleUrl); From 3bb478830deaec57ba6714695dd4e3094ba21349 Mon Sep 17 00:00:00 2001 From: Aled Sage Date: Fri, 8 Jul 2016 10:47:03 +0100 Subject: [PATCH 7/7] Update osgi test jars --- .../test/osgi/entities/more/MoreObject.java | 56 ++++++++++++++++++ .../test/osgi/entities/more/MoreObject.java | 56 ++++++++++++++++++ .../osgi/brooklyn-test-osgi-entities.jar | Bin 17775 -> 19168 bytes ...brooklyn-test-osgi-more-entities_0.1.0.jar | Bin 14964 -> 16004 bytes ...brooklyn-test-osgi-more-entities_0.2.0.jar | Bin 15646 -> 16906 bytes 5 files changed, 112 insertions(+) create mode 100644 utils/common/dependencies/osgi/more-entities-v1/src/main/java/org/apache/brooklyn/test/osgi/entities/more/MoreObject.java create mode 100644 utils/common/dependencies/osgi/more-entities-v2/src/main/java/org/apache/brooklyn/test/osgi/entities/more/MoreObject.java diff --git a/utils/common/dependencies/osgi/more-entities-v1/src/main/java/org/apache/brooklyn/test/osgi/entities/more/MoreObject.java b/utils/common/dependencies/osgi/more-entities-v1/src/main/java/org/apache/brooklyn/test/osgi/entities/more/MoreObject.java new file mode 100644 index 0000000000..1725f0862a --- /dev/null +++ b/utils/common/dependencies/osgi/more-entities-v1/src/main/java/org/apache/brooklyn/test/osgi/entities/more/MoreObject.java @@ -0,0 +1,56 @@ +/* + * 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.test.osgi.entities.more; + +public class MoreObject { + + private String val; + + public MoreObject() { + } + + public MoreObject(String val) { + this.val = val; + } + + public String getVal() { + return val; + } + + public void setVal(String val) { + this.val = val; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MoreObject)) return false; + String oVal = ((MoreObject)obj).val; + return (val == null) ? oVal == null : val.equals(oVal); + } + + @Override + public int hashCode() { + return (val == null) ? 0 : val.hashCode(); + } + + @Override + public String toString() { + return "MoreObject-v1["+val+"]"; + } +} diff --git a/utils/common/dependencies/osgi/more-entities-v2/src/main/java/org/apache/brooklyn/test/osgi/entities/more/MoreObject.java b/utils/common/dependencies/osgi/more-entities-v2/src/main/java/org/apache/brooklyn/test/osgi/entities/more/MoreObject.java new file mode 100644 index 0000000000..08bf69c913 --- /dev/null +++ b/utils/common/dependencies/osgi/more-entities-v2/src/main/java/org/apache/brooklyn/test/osgi/entities/more/MoreObject.java @@ -0,0 +1,56 @@ +/* + * 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.test.osgi.entities.more; + +public class MoreObject { + + private String val; + + public MoreObject() { + } + + public MoreObject(String val) { + this.val = val; + } + + public String getVal() { + return val; + } + + public void setVal(String val) { + this.val = val; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MoreObject)) return false; + String oVal = ((MoreObject)obj).val; + return (val == null) ? oVal == null : val.equals(oVal); + } + + @Override + public int hashCode() { + return (val == null) ? 0 : val.hashCode(); + } + + @Override + public String toString() { + return "MoreObject-v2["+val+"]"; + } +} diff --git a/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-entities.jar b/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-entities.jar index 63edc5dc33b13130b7ad16b6fb35c4cc21787c49..42ba7ca2e542d80c797a298e8fe7e24fdbe9c1d9 100644 GIT binary patch delta 9025 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 delta 7834 zcmZWu1yoeq*B=zhVJf?6p)tA5eaFKp}QL?=@bMd1w@eU?vNDu2HyKW zpYNNs?(B8f`Th3U=iYtJ-LaT}a1f4wt*QV-!T{X;`0W$lVuNw`>=R|jSy7|M5CMP_ z)VEK`p-L{(oOo?My|<9=9N6GT>2Ogs~I^gpQ}nhBzP}rt!`+ z`zV;7q3|hdR|kJjtbQ;doZd1bBd%;{mfvX3^wG9e)^KP~J~7NtJ+X;4?GQ-t$zY2< z2g*a8LK=?eYoa=8Yv}z$za?-dia~a#qExtnv&WG8Q)B_7oOqF_H^~k0lb@s%@?R-j zTZvApCLQWO5ew+EjnNvn80Y;6HHlF_013zzP3&$4*dC-;vgXAV^%D~~&FO4`!-II| zk1bvk@LDRy90zT?f?Vo!PtdN8c@IZ5OQ7SG+8ZZcOW9a`C9lKj_Xor1Rwr9;A134`Z4rH4fgiMY;%b+-h>Ey`X zHm2R2Ys#*PfUe{jZZ)iU3BR=nd9FXx3b%`GM5ku*SlX0jWq5dv<~FO!R^QcGI|$9D z4ZHB?B#hgWsa)w?={55@e

o26w9ExYz+IK%O0h`Tx3ihk>e1DiiZC$HQm(bl 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 Br&0g_ diff --git a/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.1.0.jar b/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.1.0.jar index 110111279a6381b4205f13e01122d24ba0f6f541..b30775eca0de622a6d0f3856ae2c868aed4f4479 100644 GIT binary patch delta 6847 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 A^Z)<= diff --git a/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.2.0.jar b/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.2.0.jar index 4de9b94a7e3e12024221b0b367622e5e9d04b732..0dc12b199acac5a84c09ab8eccd94ad486b5c908 100644 GIT binary patch delta 7802 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 delta 6436 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 bGzhzrv4m~P65~?