special xml serialization for EntitySpec to support CatalogItem libraries
Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/0dc533d1 Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/0dc533d1 Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/0dc533d1 Branch: refs/heads/master Commit: 0dc533d116cc92a7d5811338cec060a7868e64a0 Parents: 6156a77 Author: Alex Heneveld <alex.henev...@cloudsoftcorp.com> Authored: Fri Feb 6 17:58:41 2015 +0000 Committer: Alex Heneveld <alex.henev...@cloudsoftcorp.com> Committed: Fri Feb 6 22:12:14 2015 +0000 ---------------------------------------------------------------------- .../basic/AbstractBrooklynObjectSpec.java | 17 ++ .../rebind/persister/XmlMementoSerializer.java | 160 ++++++++++++++++++- .../persister/XmlMementoSerializerTest.java | 32 ++++ 3 files changed, 208 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/0dc533d1/api/src/main/java/brooklyn/basic/AbstractBrooklynObjectSpec.java ---------------------------------------------------------------------- diff --git a/api/src/main/java/brooklyn/basic/AbstractBrooklynObjectSpec.java b/api/src/main/java/brooklyn/basic/AbstractBrooklynObjectSpec.java index c0a4ccc..83db969 100644 --- a/api/src/main/java/brooklyn/basic/AbstractBrooklynObjectSpec.java +++ b/api/src/main/java/brooklyn/basic/AbstractBrooklynObjectSpec.java @@ -121,4 +121,21 @@ public abstract class AbstractBrooklynObjectSpec<T,K extends AbstractBrooklynObj if (Modifier.isAbstract(val.getModifiers())) throw new IllegalStateException("Implementation "+val+" is abstract, but must be a non-abstract class"); } + @Override + public boolean equals(Object obj) { + if (obj==null) return false; + if (!obj.getClass().equals(getClass())) return false; + AbstractBrooklynObjectSpec<?,?> other = (AbstractBrooklynObjectSpec<?,?>)obj; + if (!Objects.equal(getDisplayName(), other.getDisplayName())) return false; + if (!Objects.equal(getCatalogItemId(), other.getCatalogItemId())) return false; + if (!Objects.equal(getType(), other.getType())) return false; + if (!Objects.equal(getTags(), other.getTags())) return false; + return true; + } + + @Override + public int hashCode() { + return Objects.hashCode(getCatalogItemId(), getDisplayName(), getType(), getTags()); + } + } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/0dc533d1/core/src/main/java/brooklyn/entity/rebind/persister/XmlMementoSerializer.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/brooklyn/entity/rebind/persister/XmlMementoSerializer.java b/core/src/main/java/brooklyn/entity/rebind/persister/XmlMementoSerializer.java index 5f3fb5c..78644b3 100644 --- a/core/src/main/java/brooklyn/entity/rebind/persister/XmlMementoSerializer.java +++ b/core/src/main/java/brooklyn/entity/rebind/persister/XmlMementoSerializer.java @@ -22,13 +22,18 @@ import static com.google.common.base.Preconditions.checkNotNull; import java.io.IOException; import java.io.Writer; +import java.util.NoSuchElementException; +import java.util.Stack; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import brooklyn.basic.AbstractBrooklynObjectSpec; import brooklyn.catalog.CatalogItem; import brooklyn.catalog.internal.CatalogBundleDto; +import brooklyn.catalog.internal.CatalogUtils; import brooklyn.entity.Effector; import brooklyn.entity.Entity; import brooklyn.entity.Feed; @@ -49,20 +54,27 @@ import brooklyn.event.basic.BasicConfigKey; import brooklyn.location.Location; import brooklyn.management.ManagementContext; import brooklyn.management.Task; +import brooklyn.management.classloading.BrooklynClassLoadingContext; +import brooklyn.management.classloading.BrooklynClassLoadingContextSequential; +import brooklyn.management.classloading.ClassLoaderFromBrooklynClassLoadingContext; +import brooklyn.management.classloading.JavaBrooklynClassLoadingContext; import brooklyn.mementos.BrooklynMementoPersister.LookupContext; import brooklyn.policy.Enricher; import brooklyn.policy.Policy; import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.text.Strings; import brooklyn.util.xstream.XmlSerializer; 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.ReferencingMarshallingContext; import com.thoughtworks.xstream.core.util.HierarchicalStreams; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; +import com.thoughtworks.xstream.io.path.PathTrackingReader; import com.thoughtworks.xstream.mapper.Mapper; import com.thoughtworks.xstream.mapper.MapperWrapper; @@ -110,9 +122,9 @@ public class XmlMementoSerializer<T> extends XmlSerializer<T> implements Memento xstream.registerConverter(new EntityConverter()); xstream.registerConverter(new FeedConverter()); xstream.registerConverter(new CatalogItemConverter()); + xstream.registerConverter(new SpecConverter()); xstream.registerConverter(new ManagementContextConverter()); - xstream.registerConverter(new TaskConverter(xstream.getMapper())); //For compatibility with existing persistence stores content. @@ -341,5 +353,151 @@ public class XmlMementoSerializer<T> extends XmlSerializer<T> implements Memento return lookupContext.lookupManagementContext(); } } + + /** 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() { + super(xstream.getMapper(), xstream.getReflectionProvider()); + } + @Override + public boolean canConvert(@SuppressWarnings("rawtypes") Class type) { + return AbstractBrooklynObjectSpec.class.isAssignableFrom(type); + } + @Override + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + if (source == null) return; + AbstractBrooklynObjectSpec<?, ?> spec = (AbstractBrooklynObjectSpec<?, ?>) source; + String catalogItemId = spec.getCatalogItemId(); + if (Strings.isNonBlank(catalogItemId)) { + // write this field first, so we can peek at it when we read + writer.startNode("catalogItemId"); + writer.setValue(catalogItemId); + writer.endNode(); + + // we're going to write the catalogItemId field twice :( but that's okay. + // better solution would be to have mark/reset on reader so we can peek for such a field; + // see comment below + super.marshal(source, writer, context); + } else { + super.marshal(source, writer, context); + } + } + @Override + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + String catalogItemId = null; + instantiateNewInstanceSettingCache(reader, context); + + if (reader instanceof PathTrackingReader) { + // have to assume this is first; there is no mark/reset support on these readers + // (if there were then it would be easier, we could just look for that child anywhere, + // and not need a custom writer!) + if ("catalogItemId".equals( ((PathTrackingReader)reader).peekNextChild() )) { + // cache the instance + + reader.moveDown(); + catalogItemId = reader.getValue(); + reader.moveUp(); + } + } + boolean customLoaderSet = false; + try { + if (Strings.isNonBlank(catalogItemId)) { + if (lookupContext==null) throw new NullPointerException("lookupContext required to load catalog item "+catalogItemId); + CatalogItem<?, ?> cat = CatalogUtils.getCatalogItemOptionalVersion(lookupContext.lookupManagementContext(), catalogItemId); + if (cat==null) throw new NoSuchElementException("catalog item: "+catalogItemId); + BrooklynClassLoadingContext clcNew = CatalogUtils.newClassLoadingContext(lookupContext.lookupManagementContext(), cat); + pushXstreamCustomClassLoader(clcNew); + customLoaderSet = true; + } + + AbstractBrooklynObjectSpec<?, ?> result = (AbstractBrooklynObjectSpec<?, ?>) super.unmarshal(reader, context); + // we wrote it twice so this shouldn't be necessary; but if we fix it so we only write once, we'd need this + result.catalogItemId(catalogItemId); + return result; + } finally { + instance = null; + if (customLoaderSet) { + popXstreamCustomClassLoader(); + } + } + } + + Object instance; + + @Override + protected Object instantiateNewInstance(HierarchicalStreamReader reader, UnmarshallingContext context) { + // the super calls getAttribute which requires that we have not yet done moveDown, + // so we do this earlier and cache it for when we call super.unmarshal + if (instance==null) + throw new IllegalStateException("Instance should be created and cached"); + return instance; + } + protected void instantiateNewInstanceSettingCache(HierarchicalStreamReader reader, UnmarshallingContext context) { + instance = super.instantiateNewInstance(reader, context); + } + } + + Stack<BrooklynClassLoadingContext> contexts = new Stack<BrooklynClassLoadingContext>(); + Stack<ClassLoader> cls = new Stack<ClassLoader>(); + AtomicReference<Thread> xstreamLockOwner = new AtomicReference<Thread>(); + 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); + } + + protected void popXstreamCustomClassLoader() { + synchronized (xstreamLockOwner) { + releaseXstreamLock(); + xstream.setClassLoader(cls.pop()); + contexts.pop(); + } + } + + 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++; + } + } + + 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()); + } + } + } + } + } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/0dc533d1/core/src/test/java/brooklyn/entity/rebind/persister/XmlMementoSerializerTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/brooklyn/entity/rebind/persister/XmlMementoSerializerTest.java b/core/src/test/java/brooklyn/entity/rebind/persister/XmlMementoSerializerTest.java index e0b4f4e..6083134 100644 --- a/core/src/test/java/brooklyn/entity/rebind/persister/XmlMementoSerializerTest.java +++ b/core/src/test/java/brooklyn/entity/rebind/persister/XmlMementoSerializerTest.java @@ -37,17 +37,22 @@ import brooklyn.basic.BrooklynObject; import brooklyn.catalog.CatalogItem; import brooklyn.catalog.internal.CatalogItemBuilder; import brooklyn.catalog.internal.CatalogItemDtoAbstract; +import brooklyn.catalog.internal.CatalogUtils; import brooklyn.entity.Entity; import brooklyn.entity.Feed; import brooklyn.entity.basic.Entities; +import brooklyn.entity.group.DynamicCluster; import brooklyn.entity.proxying.EntitySpec; import brooklyn.entity.rebind.BrooklynObjectType; import brooklyn.location.Location; import brooklyn.location.LocationSpec; import brooklyn.management.ManagementContext; +import brooklyn.management.internal.LocalManagementContext; +import brooklyn.management.osgi.OsgiVersionMoreEntityTest; import brooklyn.mementos.BrooklynMementoPersister.LookupContext; import brooklyn.policy.Enricher; import brooklyn.policy.Policy; +import brooklyn.test.entity.LocalManagementContextForTests; import brooklyn.test.entity.TestApplication; import brooklyn.test.entity.TestEntity; import brooklyn.util.collections.MutableList; @@ -129,6 +134,12 @@ public class XmlMementoSerializerTest { } @Test + public void testClass() throws Exception { + Class<?> t = XmlMementoSerializer.class; + assertSerializeAndDeserialize(t); + } + + @Test public void testEntity() throws Exception { final TestApplication app = TestApplication.Factory.newManagedInstanceForTests(); ManagementContext managementContext = app.getManagementContext(); @@ -180,6 +191,27 @@ public class XmlMementoSerializerTest { } @Test + public void testEntitySpec() throws Exception { + EntitySpec<?> obj = EntitySpec.create(TestEntity.class); + assertSerializeAndDeserialize(obj); + } + + @Test + public void testEntitySpecFromOsgi() throws Exception { + ManagementContext mgmt = LocalManagementContextForTests.builder(true).disableOsgi(false).build(); + CatalogItem<?, ?> ci = OsgiVersionMoreEntityTest.addMoreEntityV1(mgmt, "1.0"); + + EntitySpec<DynamicCluster> spec = EntitySpec.create(DynamicCluster.class) + .configure(DynamicCluster.INITIAL_SIZE, 1) + .configure(DynamicCluster.MEMBER_SPEC, CatalogUtils.createEntitySpec(mgmt, ci)); + + serializer.setLookupContext(new LookupContextImpl(mgmt, + ImmutableList.<Entity>of(), ImmutableList.<Location>of(), ImmutableList.<Policy>of(), + ImmutableList.<Enricher>of(), ImmutableList.<Feed>of(), ImmutableList.<CatalogItem<?,?>>of(), true)); + assertSerializeAndDeserialize(spec); + } + + @Test public void testImmutableCollectionsWithDanglingEntityRef() throws Exception { // If there's a dangling entity in an ImmutableList etc, then discard it entirely. // If we try to insert null then it fails, breaking the deserialization of that entire file.