/*
* 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 com.google.common.base.Preconditions.checkNotNull;
import java.io.IOException;
import java.io.Writer;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Stack;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.brooklyn.api.catalog.CatalogItem;
import org.apache.brooklyn.api.effector.Effector;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.internal.AbstractBrooklynObjectSpec;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.mgmt.classloading.BrooklynClassLoadingContext;
import org.apache.brooklyn.api.mgmt.rebind.mementos.BrooklynMementoPersister.LookupContext;
import org.apache.brooklyn.api.objs.Identifiable;
import org.apache.brooklyn.api.policy.Policy;
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.CatalogBundleDto;
import org.apache.brooklyn.core.catalog.internal.CatalogUtils;
import org.apache.brooklyn.core.config.BasicConfigKey;
import org.apache.brooklyn.core.effector.BasicParameterType;
import org.apache.brooklyn.core.effector.EffectorAndBody;
import org.apache.brooklyn.core.effector.EffectorTasks.EffectorBodyTaskFactory;
import org.apache.brooklyn.core.effector.EffectorTasks.EffectorTaskFactory;
import org.apache.brooklyn.core.mgmt.classloading.BrooklynClassLoadingContextSequential;
import org.apache.brooklyn.core.mgmt.classloading.ClassLoaderFromBrooklynClassLoadingContext;
import org.apache.brooklyn.core.mgmt.classloading.JavaBrooklynClassLoadingContext;
import org.apache.brooklyn.core.mgmt.rebind.dto.BasicCatalogItemMemento;
import org.apache.brooklyn.core.mgmt.rebind.dto.BasicEnricherMemento;
import org.apache.brooklyn.core.mgmt.rebind.dto.BasicEntityMemento;
import org.apache.brooklyn.core.mgmt.rebind.dto.BasicFeedMemento;
import org.apache.brooklyn.core.mgmt.rebind.dto.BasicLocationMemento;
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.xstream.XmlSerializer;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.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;
/* uses xml, cleaned up a bit
*
* there is an early attempt at doing this with JSON in pull request #344 but
* it is not nicely deserializable, see comments at http://xstream.codehaus.org/json-tutorial.html */
public class XmlMementoSerializer<T> extends XmlSerializer<T> implements MementoSerializer<T> {
private static final Logger LOG = LoggerFactory.getLogger(XmlMementoSerializer.class);
private final ClassLoader classLoader;
private LookupContext lookupContext;
public XmlMementoSerializer(ClassLoader classLoader) {
this(classLoader, DeserializingClassRenamesProvider.loadDeserializingClassRenames());
}
public XmlMementoSerializer(ClassLoader classLoader, Map<String, String> deserializingClassRenames) {
super(deserializingClassRenames);
this.classLoader = checkNotNull(classLoader, "classLoader");
xstream.setClassLoader(this.classLoader);
// old (deprecated in 070? or earlier) single-file persistence uses this keyword; TODO remove soon in 080 ?
xstream.alias("brooklyn", MutableBrooklynMemento.class);
xstream.alias("entity", BasicEntityMemento.class);
xstream.alias("location", BasicLocationMemento.class);
xstream.alias("policy", BasicPolicyMemento.class);
xstream.alias("feed", BasicFeedMemento.class);
xstream.alias("enricher", BasicEnricherMemento.class);
xstream.alias("configKey", BasicConfigKey.class);
xstream.alias("catalogItem", BasicCatalogItemMemento.class);
xstream.alias("bundle", CatalogBundleDto.class);
xstream.alias("attributeSensor", BasicAttributeSensor.class);
xstream.alias("effector", Effector.class);
xstream.addDefaultImplementation(EffectorAndBody.class, Effector.class);
xstream.alias("parameter", BasicParameterType.class);
xstream.addDefaultImplementation(EffectorBodyTaskFactory.class, EffectorTaskFactory.class);
xstream.alias("entityRef", Entity.class);
xstream.alias("locationRef", Location.class);
xstream.alias("policyRef", Policy.class);
xstream.alias("enricherRef", Enricher.class);
xstream.registerConverter(new LocationConverter());
xstream.registerConverter(new PolicyConverter());
xstream.registerConverter(new EnricherConverter());
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.
xstream.aliasField("registeredTypeName", BasicCatalogItemMemento.class, "symbolicName");
xstream.registerLocalConverter(BasicCatalogItemMemento.class, "libraries", new CatalogItemLibrariesConverter());
}
// Warning: this is called in the super-class constuctor, so before this constructor!
@Override
protected MapperWrapper wrapMapperForNormalUsage(Mapper next) {
MapperWrapper mapper = super.wrapMapperForNormalUsage(next);
mapper = new CustomMapper(mapper, Entity.class, "entityProxy");
mapper = new CustomMapper(mapper, Location.class, "locationProxy");
return mapper;
}
@Override
public void serialize(Object object, Writer writer) {
super.serialize(object, writer);
try {
writer.append("\n");
} catch (IOException e) {
throw Exceptions.propagate(e);
}
}
@Override
public void setLookupContext(LookupContext lookupContext) {
this.lookupContext = checkNotNull(lookupContext, "lookupContext");
}
@Override
public void unsetLookupContext() {
this.lookupContext = null;
}
/**
* For changing the tag used for anything that implements/extends the given type.
* Necessary for using EntityRef rather than the default "dynamic-proxy" tag.
*
* @author aled
*/
public class CustomMapper extends MapperWrapper {
private final Class<?> clazz;
private final String alias;
public CustomMapper(Mapper wrapped, Class<?> clazz, String alias) {
super(wrapped);
this.clazz = checkNotNull(clazz, "clazz");
this.alias = checkNotNull(alias, "alias");
}
public String getAlias() {
return alias;
}
@Override
public String serializedClass(@SuppressWarnings("rawtypes") Class type) {
if (type != null && clazz.isAssignableFrom(type)) {
return alias;
} else {
return super.serializedClass(type);
}
}
@Override
public Class<?> realClass(String elementName) {
if (elementName.equals(alias)) {
return clazz;
} else {
return super.realClass(elementName);
}
}
}
public abstract class IdentifiableConverter<IT extends Identifiable> implements SingleValueConverter {
private final Class<IT> clazz;
IdentifiableConverter(Class<IT> clazz) {
this.clazz = clazz;
}
@Override
public boolean canConvert(@SuppressWarnings("rawtypes") Class type) {
boolean result = clazz.isAssignableFrom(type);
return result;
}
@Override
public String toString(Object obj) {
return obj == null ? null : ((Identifiable)obj).getId();
}
@Override
public Object fromString(String str) {
if (lookupContext == null) {
LOG.warn("Cannot unmarshal from persisted xml {} {}; no lookup context supplied!", clazz.getSimpleName(), str);
return null;
} else {
return lookup(str);
}
}
protected abstract IT lookup(String id);
}
public class LocationConverter extends IdentifiableConverter<Location> {
LocationConverter() {
super(Location.class);
}
@Override
protected Location lookup(String id) {
return lookupContext.lookupLocation(id);
}
}
public class PolicyConverter extends IdentifiableConverter<Policy> {
PolicyConverter() {
super(Policy.class);
}
@Override
protected Policy lookup(String id) {
return lookupContext.lookupPolicy(id);
}
}
public class EnricherConverter extends IdentifiableConverter<Enricher> {
EnricherConverter() {
super(Enricher.class);
}
@Override
protected Enricher lookup(String id) {
return lookupContext.lookupEnricher(id);
}
}
public class FeedConverter extends IdentifiableConverter<Feed> {
FeedConverter() {
super(Feed.class);
}
@Override
protected Feed lookup(String id) {
return lookupContext.lookupFeed(id);
}
}
public class EntityConverter extends IdentifiableConverter<Entity> {
EntityConverter() {
super(Entity.class);
}
@Override
protected Entity lookup(String id) {
return lookupContext.lookupEntity(id);
}
}
@SuppressWarnings("rawtypes")
public class CatalogItemConverter extends IdentifiableConverter<CatalogItem> {
CatalogItemConverter() {
super(CatalogItem.class);
}
@Override
protected CatalogItem<?,?> lookup(String id) {
return lookupContext.lookupCatalogItem(id);
}
}
static boolean loggedTaskWarning = false;
public class TaskConverter implements Converter {
private final Mapper mapper;
TaskConverter(Mapper mapper) {
this.mapper = mapper;
}
@Override
public boolean canConvert(@SuppressWarnings("rawtypes") Class type) {
return Task.class.isAssignableFrom(type);
}
@SuppressWarnings("deprecation")
@Override
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
if (source == null) return;
if (((Task<?>)source).isDone() && !((Task<?>)source).isError()) {
try {
context.convertAnother(((Task<?>)source).get());
} catch (InterruptedException e) {
throw Exceptions.propagate(e);
} catch (ExecutionException e) {
LOG.warn("Unexpected exception getting done (and non-error) task result for "+source+"; continuing: "+e, e);
}
} else {
// TODO How to log sensibly, without it logging this every second?!
// jun 2014, have added a "log once" which is not ideal but better than the log never behaviour
if (!loggedTaskWarning) {
LOG.warn("Intercepting and skipping request to serialize a Task"
+ (context instanceof ReferencingMarshallingContext ? " at "+((ReferencingMarshallingContext)context).currentPath() : "")+
" (only logging this once): "+source);
loggedTaskWarning = true;
}
return;
}
}
@Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
if (reader.hasMoreChildren()) {
Class<?> type = readClassType(reader, mapper);
// Class<?> type2 = context.getRequiredType();
reader.moveDown();
Object result = context.convertAnother(null, type);
reader.moveUp();
return result;
} else {
return null;
}
}
}
// TODO: readClassType() and readClassAttribute()
// Temporarily copied until osgification is finished from bundle-private class
// com.thoughtworks.xstream.core.util.HierarchicalStreams
// Perhaps context.getRequiredType(); can be used instead?
// Other users of xstream (e.g. jenkinsci) manually check for resoved-to and class attributes
// for compatibility with older versions of xstream
private static Class readClassType(HierarchicalStreamReader reader, Mapper mapper) {
String classAttribute = readClassAttribute(reader, mapper);
Class type;
if (classAttribute == null) {
type = mapper.realClass(reader.getNodeName());
} else {
type = mapper.realClass(classAttribute);
}
return type;
}
private static String readClassAttribute(HierarchicalStreamReader reader, Mapper mapper) {
String attributeName = mapper.aliasForSystemAttribute("resolves-to");
String classAttribute = attributeName == null ? null : reader.getAttribute(attributeName);
if (classAttribute == null) {
attributeName = mapper.aliasForSystemAttribute("class");
if (attributeName != null) {
classAttribute = reader.getAttribute(attributeName);
}
}
return classAttribute;
}
public class ManagementContextConverter implements Converter {
@Override
public boolean canConvert(@SuppressWarnings("rawtypes") Class type) {
return ManagementContext.class.isAssignableFrom(type);
}
@Override
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
// write nothing, and always insert the current mgmt context
}
@Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
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);
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);
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());
}
xstreamLockOwner.notifyAll();
}
}
}
}