blob: b6433912853069cd1145e692ce31981ff11e016c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.brooklyn.util.core.xstream;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.XStreamException;
import com.thoughtworks.xstream.converters.extended.JavaClassConverter;
import com.thoughtworks.xstream.core.DefaultConverterLookup;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.naming.NameCoder;
import com.thoughtworks.xstream.io.path.PathTracker;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.io.xml.XppDriver;
import com.thoughtworks.xstream.mapper.DefaultMapper;
import com.thoughtworks.xstream.mapper.Mapper;
import com.thoughtworks.xstream.mapper.MapperWrapper;
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.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.javalang.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
public class XmlSerializer<T> {
private static final Logger LOG = LoggerFactory.getLogger(XmlSerializer.class);
private final Map<String, String> deserializingClassRenames;
protected final XStream xstream;
protected final XppDriver hierarchicalStreamDriver;
protected final DefaultConverterLookup converterLookup;
public XmlSerializer() {
this(null);
}
public XmlSerializer(Map<String, String> deserializingClassRenames) {
this(null, deserializingClassRenames);
}
public XmlSerializer(ClassLoader loader, Map<String, String> deserializingClassRenames) {
this(loader, deserializingClassRenames, null);
}
public XmlSerializer(ClassLoader loader, Map<String, String> deserializingClassRenames, Function<MapperWrapper,MapperWrapper> mapperCustomizer) {
this.deserializingClassRenames = deserializingClassRenames == null ? ImmutableMap.of() : deserializingClassRenames;
hierarchicalStreamDriver = new XppDriver() {
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriterExposingStack(out, getNameCoder());
}
};
converterLookup = new DefaultConverterLookup();
XStream xs1 = new XStream(); // use this to get the class loader because its package isn't exposed
xstream = new XStream(null, hierarchicalStreamDriver, xs1.getClassLoaderReference(), (Mapper)null,
type -> converterLookup.lookupConverterForType(type),
(converter,priority) -> converterLookup.registerConverter(converter, priority)
) {
@Override
protected MapperWrapper wrapMapper(MapperWrapper next) {
MapperWrapper result = XmlSerializer.this.wrapMapperForNormalUsage(super.wrapMapper(next));
if (mapperCustomizer != null) {
result = mapperCustomizer.apply(result);
}
return result;
}
};
allowAllTypes(xstream);
if (loader != null) {
xstream.setClassLoader(loader);
}
// // we could accept losing fields in exceptions; usually they are context that we don't care about; but it can generate XML which cannot be read back
// xstream.registerConverter(new SafeThrowableConverter(t -> Throwable.class.isAssignableFrom(t), converterLookup));
xstream.registerConverter(newCustomJavaClassConverter(), XStream.PRIORITY_NORMAL);
addStandardHelpers(xstream);
}
static class PrettyPrintWriterExposingStack extends PrettyPrintWriter {
private final Writer origWriter;
public PrettyPrintWriterExposingStack(Writer writer, NameCoder nameCoder) { super(writer, nameCoder); this.origWriter = writer; }
public PathTracker path = new PathTracker();
public void startNode(String name) {
path.pushElement(name);
super.startNode(name);
}
@Override
public void endNode() {
super.endNode();
path.popElement();
}
public Writer getOrigWriter() {
return origWriter;
}
}
@VisibleForTesting
public static void addStandardHelpers(XStream xstream) {
// list as array list is default
xstream.alias("map", Map.class, LinkedHashMap.class);
xstream.alias("set", Set.class, LinkedHashSet.class);
xstream.registerConverter(new StringKeyMapConverter(xstream.getMapper()), /* priority */ 10);
xstream.alias("MutableMap", MutableMap.class);
xstream.alias("MutableSet", MutableSet.class);
xstream.alias("MutableList", MutableList.class);
// Needs an explicit MutableSet converter!
// Without it, the alias for "set" seems to interfere with the MutableSet.map field, so it gets
// a null field on deserialization.
xstream.registerConverter(new MutableSetConverter(xstream.getMapper()));
xstream.registerConverter(new MutableListConverter(xstream.getMapper(), xstream.getReflectionProvider(), xstream.getClassLoaderReference()));
xstream.aliasType("ImmutableList", ImmutableList.class);
xstream.registerConverter(new ImmutableListConverter(xstream.getMapper()));
xstream.registerConverter(new ImmutableSetConverter(xstream.getMapper()));
xstream.registerConverter(new ImmutableMapConverter(xstream.getMapper()));
xstream.registerConverter(new MinidevJsonObjectConverter(xstream.getMapper()));
xstream.registerConverter(new HashMultimapConverter(xstream.getMapper()));
xstream.registerConverter(new EnumCaseForgivingConverter());
xstream.registerConverter(new Inet4AddressConverter());
addStandardInnerClassHelpers(xstream);
// See ObjectWithDefaultStringImplConverter (and its usage) for why we want to auto-detect
// annotations (usages of this is in the camp project, so we can't just list it statically
// here unfortunately).
xstream.autodetectAnnotations(true);
}
@VisibleForTesting
public static void addStandardInnerClassHelpers(XStream xstream) {
Maybe<Object> valueTransformer = Reflections.getFieldValueMaybe(Maps.transformValues(MutableMap.of(), x -> x), "transformer");
// add old aliases first, these will be deserialized but not serialized!
// not ideal that we map both 7 and 9 to the value tansformer, but okay as 7 is not used for other serialized things
// (fortunately, as otherwise hard to deserialize!)
addAliasForInnerClass(xstream, "com.google.common.collect.Maps$7", valueTransformer);
addAliasForInnerClass(xstream, "com.google.guava:com.google.common.collect.Maps$7", valueTransformer);
// preferred alias
addAliasForInnerClass(xstream, "com.google.common.collect.Maps._inners.valueTransformer", valueTransformer);
Maybe<Iterable<Object>> iterableTransformer = Maybe.of(Iterables.transform(MutableSet.of(), x -> x));
// we don't seem to serialize iterables, not surprisingly; but if we do this is useful, and if legacy are found they can be added here
// preferred alias
addAliasForInnerClass(xstream, "com.google.common.collect.Iterables._inners.transform", iterableTransformer);
}
private static Set<String> LOGGED_ALIASES = MutableSet.of();
private static <T> void addAliasForInnerClass(XStream xstream, String alias, Maybe<T> object) {
if (object.isAbsent()) {
if (LOGGED_ALIASES.add(alias)) {
LOG.warn("No object found to register serialization alias for " + alias + "; ignoring");
}
} else {
xstream.alias(alias, object.get().getClass());
if (LOGGED_ALIASES.add(alias)) {
LOG.debug("XStream alias for "+object.get().getClass()+": "+alias+" ("+object+")");
}
}
}
public static void allowAllTypes(final XStream xstream) {
xstream.allowTypesByWildcard(new String[] {
"**"
});
}
/**
* JCC is used when Class instances are serialized/deserialized as a value
* (not as tags) and there are no aliases configured for that type.
* It is configured in XStream default *without* access to the XStream mapper,
* which is meant to apply when serializing the type name for instances of that type.
* <p>
* However we need a few selected mappers (see {@link #wrapMapperForHandlingClasses(Mapper)} )
* to apply to all class renames, but many of the mappers must NOT be used,
* e.g. because some might intercept all Class<? extends Entity> references
* (and that interception is only wanted when serializing <i>instances</i>,
* as in {@link #wrapMapperForNormalUsage(Mapper)}).
* <p>
* This can typically be done simply by registering our own instance of this (due to order guarantee of PrioritizedList),
* after the instance added by XStream.setupConverters()
*/
private JavaClassConverter newCustomJavaClassConverter() {
return new JavaClassConverter(wrapMapperForHandlingClasses(new DefaultMapper(xstream.getClassLoaderReference()))) {};
}
/** Extension point where sub-classes can add mappers needed for handling class names.
* This is used by {@link #wrapMapperForNormalUsage(Mapper)} and also to set up the {@link JavaClassConverter}
* (see {@link #newCustomJavaClassConverter()} for what that does).
* <p>
* This should apply when nice names are used for inner classes, or classes are renamed;
* however mappers which affect field aliases or intercept references to entities are not
* wanted in the {@link JavaClassConverter} and so should be added by {@link #wrapMapperForNormalUsage(Mapper)}
* instead of this.
* <p>
* Developers note this is called from the constructor; be careful when overriding and
* see comment on {@link #wrapMapperForNormalUsage(Mapper)} about field availability. */
protected MapperWrapper wrapMapperForHandlingClasses(Mapper next) {
MapperWrapper result = new CompilerIndependentOuterClassFieldMapper(next);
Supplier<ClassLoader> classLoaderSupplier = new Supplier<ClassLoader>() {
@Override public ClassLoader get() {
return xstream.getClassLoaderReference().getReference();
}
};
result = new ClassRenamingMapper(result, deserializingClassRenames, classLoaderSupplier);
result = new OsgiClassnameMapper(new Supplier<XStream>() {
@Override public XStream get() { return xstream; } }, result);
// TODO as noted in ClassRenamingMapper that class can be simplified if
// we swap the order of the above calls, because it _will_ be able to rely on
// OsgiClassnameMapper to attempt to load with the xstream reference stack
// (not doing it just now because close to a release)
return result;
}
/** Extension point where sub-classes can add mappers to set up the main {@link Mapper} given to XStream.
* This includes all of {@link #wrapMapperForHandlingClasses(Mapper)} plus anything wanted for normal usage.
* <p>
* Typically any non-class-name mappers wanted should be added in a subclass by overriding this field,
* calling this superclass method, then wrapping the result.
* <p>
* Developers note this is called from the constructor; be careful when overriding
* because most fields won't be available. In particular in a subclass,
* this method in the subclass will be invoked very early in its constructor.
* Fields like {@link #xstream} (and <i>anything</i> set in the subclass) won't
* yet be available. For this reason some mappers will need to be given a {@link Supplier} for late resolution. */
protected MapperWrapper wrapMapperForNormalUsage(Mapper next) {
return wrapMapperForHandlingClasses(next);
}
public void serialize(Object obj, Writer out) {
// xstream.toXML(obj, writer);
// we replace the above (parent impl) with the following, expanded to give better output for errors
// (mainly used for lambdas which are not serializable)
HierarchicalStreamWriter writer = hierarchicalStreamDriver.createWriter(out);
try {
xstream.marshal(obj, writer);
} catch (Throwable e) {
Exceptions.propagateIfInterrupt(e);
if (writer instanceof PrettyPrintWriterExposingStack) {
String path = ("" + ((PrettyPrintWriterExposingStack)writer).path.getPath()).trim();
if (!e.toString().contains(path)) {
throw new XStreamException(Exceptions.collapseText(e) + "; while converting element at " + path, e);
}
}
throw Exceptions.propagate(e);
} finally {
writer.flush();
}
}
@SuppressWarnings("unchecked")
public T deserialize(Reader xml) {
return (T) xstream.fromXML(xml);
}
public String toString(T memento) {
Writer writer = new StringWriter();
serialize(memento, writer);
return writer.toString();
}
public T fromString(String xml) {
return deserialize(new StringReader(xml));
}
}