/*
* Copyright (c) 2010-2016. Axon Framework
*
* 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.axonframework.serialization;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.collections.MapConverter;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.mapper.CannotResolveClassException;
import com.thoughtworks.xstream.mapper.Mapper;
import org.axonframework.commandhandling.GenericCommandMessage;
import org.axonframework.common.Assert;
import org.axonframework.eventhandling.GenericEventMessage;
import org.axonframework.eventhandling.saga.AnnotatedSaga;
import org.axonframework.eventhandling.saga.AssociationValue;
import org.axonframework.eventhandling.saga.AssociationValues;
import org.axonframework.eventhandling.saga.AssociationValuesImpl;
import org.axonframework.eventsourcing.GenericDomainEventMessage;
import org.axonframework.messaging.MetaData;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Abstract implementation for XStream based serializers. It provides some helper methods and configuration features
* independent of the actual format used to marshal to.
*
* @author Allard Buijze
* @since 2.0
*/
public abstract class AbstractXStreamSerializer implements Serializer {
private static final Charset DEFAULT_CHARSET_NAME = Charset.forName("UTF-8");
private final XStream xStream;
private final Charset charset;
private final RevisionResolver revisionResolver;
private final Converter converter;
/**
* Initialize a generic serializer using the UTF-8 character set. The provided XStream instance is used to perform
* the serialization.
* <p/>
* An {@link AnnotationRevisionResolver} is used to resolve the revision for serialized objects.
*
* @param xStream XStream instance to use
*/
protected AbstractXStreamSerializer(XStream xStream) {
this(xStream, new AnnotationRevisionResolver());
}
/**
* Initialize a generic serializer using the UTF-8 character set. The provided XStream instance is used to perform
* the serialization.
*
* @param xStream XStream instance to use
* @param revisionResolver The strategy to use to resolve the revision of an object
*/
protected AbstractXStreamSerializer(XStream xStream, RevisionResolver revisionResolver) {
this(DEFAULT_CHARSET_NAME, xStream, revisionResolver);
}
/**
* Initialize the serializer using the given {@code charset} and {@code xStream} instance. The
* {@code xStream} instance is configured with several converters for the most common types in Axon.
* <p/>
* An {@link AnnotationRevisionResolver} is used to resolve revision for serialized objects.
*
* @param charset The character set to use
* @param xStream The XStream instance to use
*/
protected AbstractXStreamSerializer(Charset charset, XStream xStream) {
this(charset, xStream, new AnnotationRevisionResolver(), new ChainingConverter());
}
/**
* Initialize the serializer using the given {@code charset} and {@code xStream} instance. The
* {@code xStream} instance is configured with several converters for the most common types in Axon.
*
* @param charset The character set to use
* @param xStream The XStream instance to use
* @param revisionResolver The strategy to use to resolve the revision of an object
*/
protected AbstractXStreamSerializer(Charset charset, XStream xStream, RevisionResolver revisionResolver) {
this(charset, xStream, revisionResolver, new ChainingConverter());
}
/**
* Initialize the serializer using the given {@code charset}, {@code xStream} instance,
* {@code revisionResolver} and {@code converter}. The {@code xStream} instance is configured
* with several converters for the most common types in Axon.
*
* @param charset The character set to use
* @param xStream The XStream instance to use
* @param revisionResolver The strategy to use to resolve the revision of an object
* @param converter The converter providing the necessary content converters
*/
protected AbstractXStreamSerializer(Charset charset, XStream xStream, RevisionResolver revisionResolver,
Converter converter) {
Assert.notNull(charset, () -> "charset may not be null");
Assert.notNull(xStream, () -> "xStream may not be null");
Assert.notNull(converter, () -> "converter may not be null");
Assert.notNull(revisionResolver, () -> "revisionResolver may not be null");
this.charset = charset;
this.xStream = xStream;
this.converter = converter;
this.revisionResolver = revisionResolver;
if (converter instanceof ChainingConverter) {
registerConverters((ChainingConverter) converter);
}
xStream.addImmutableType(UUID.class, true);
// Message serialization
xStream.alias("domain-event", GenericDomainEventMessage.class);
xStream.alias("event", GenericEventMessage.class);
xStream.alias("command", GenericCommandMessage.class);
// Configuration to enhance Saga serialization
xStream.addDefaultImplementation(AssociationValuesImpl.class, AssociationValues.class);
xStream.aliasField("associations", AnnotatedSaga.class, "associationValues");
xStream.alias("association", AssociationValue.class);
xStream.aliasField("key", AssociationValue.class, "propertyKey");
xStream.aliasField("value", AssociationValue.class, "propertyValue");
// for backward compatibility
xStream.alias("uuid", UUID.class);
xStream.alias("meta-data", MetaData.class);
xStream.registerConverter(new MetaDataConverter(xStream.getMapper()));
}
/**
* Registers any converters that are specific to the type of content written by this serializer.
*
* @param converter the Converter to register the converters with
*/
protected abstract void registerConverters(ChainingConverter converter);
@Override
public <T> boolean canSerializeTo(Class<T> expectedRepresentation) {
return converter.canConvert(byte[].class, expectedRepresentation);
}
@Override
public <T> SerializedObject<T> serialize(Object object, Class<T> expectedType) {
T result = doSerialize(object, expectedType, xStream);
return new SimpleSerializedObject<>(result, expectedType, typeForClass(object.getClass()));
}
/**
* Serialize the given {@code object} to the given {@code expectedFormat}. The subclass may use {@link
* #convert(Object, Class, Class)} to convert the result of the serialization to the expected type.
*
* @param object The object to serialize
* @param expectedFormat The format in which the serialized object must be returned
* @param xStream The XStream instance to serialize with
* @param <T> The format in which the serialized object must be returned
* @return The serialized object
*/
protected abstract <T> T doSerialize(Object object, Class<T> expectedFormat, XStream xStream);
/**
* Deserialize the given {@code serializedObject}.
*
* @param serializedObject The instance containing the serialized format of the object
* @param xStream The XStream instance to deserialize with
* @return the deserialized object
*/
protected abstract Object doDeserialize(SerializedObject serializedObject, XStream xStream);
/**
* Convert the given {@code source}, of type {@code sourceType} to the given {@code targetType}.
*
* @param <S> The type of data that needs to be converted
* @param <T> The target type of the conversion
* @param source The object to convert
* @param sourceType The source type of the conversion
* @param targetType The target type of the conversion
* @return The converted object
*/
protected <S, T> T convert(S source, Class<S> sourceType, Class<T> targetType) {
return getConverter().convert(source, sourceType, targetType);
}
private String revisionOf(Class<?> type) {
return revisionResolver.revisionOf(type);
}
@SuppressWarnings("unchecked")
@Override
public <S, T> T deserialize(SerializedObject<S> serializedObject) {
return (T) doDeserialize(serializedObject, xStream);
}
@Override
public Class classForType(SerializedType type) {
try {
return xStream.getMapper().realClass(type.getName());
} catch (CannotResolveClassException e) {
throw new UnknownSerializedTypeException(type, e);
}
}
@Override
public SerializedType typeForClass(Class type) {
return new SimpleSerializedType(typeIdentifierOf(type), revisionOf(type));
}
/**
* Adds an alias to use instead of the fully qualified class name.
*
* @param name The alias to use
* @param type The Class to use the alias for
* @see XStream#alias(String, Class)
*/
public void addAlias(String name, Class type) {
xStream.alias(name, type);
}
/**
* Add an alias for a package. This allows long package names to be shortened considerably. Will also use the alias
* for sub-packages of the provided package.
* <p/>
* E.g. an alias of "axoncore" for the package "org.axonframework.core" will use "axoncore.repository" for the
* package "org.axonframework.core.repository".
*
* @param alias The alias to use.
* @param pkgName The package to use the alias for
* @see XStream#aliasPackage(String, String)
*/
public void addPackageAlias(String alias, String pkgName) {
xStream.aliasPackage(alias, pkgName);
}
/**
* Adds an alias to use for a given field in the given class.
*
* @param alias The alias to use instead of the original field name
* @param definedIn The class that defines the field.
* @param fieldName The name of the field to use the alias for
* @see XStream#aliasField(String, Class, String)
*/
public void addFieldAlias(String alias, Class definedIn, String fieldName) {
xStream.aliasField(alias, definedIn, fieldName);
}
/**
* Returns a reference to the underlying {@link com.thoughtworks.xstream.XStream} instance, that does the actual
* serialization.
*
* @return the XStream instance that does the actual (de)serialization.
* @see com.thoughtworks.xstream.XStream
*/
public XStream getXStream() {
return xStream;
}
/**
* Returns the character set used to convert character to bytes and vice versa.
*
* @return the character set used to convert character to bytes and vice versa
*/
public Charset getCharset() {
return charset;
}
/**
* Returns the Converter used by this serialized. The converter factory allows registration of
* ContentTypeConverters needed by the upcasters.
*
* @return the Converter used by this serializer
*/
@Override
public Converter getConverter() {
return converter;
}
/**
* Returns the type identifier for the given {@code type}. It uses the aliasing rules configured in XStream.
*
* @param type The type to get the type identifier of
* @return A String containing the type identifier of the given class
*/
private String typeIdentifierOf(Class<?> type) {
return xStream.getMapper().serializedClass(type);
}
/**
* Class that marshals MetaData in the least verbose way.
*/
private static final class MetaDataConverter extends MapConverter {
public MetaDataConverter(Mapper mapper) {
super(mapper);
}
@Override
public boolean canConvert(Class type) {
return MetaData.class.equals(type);
}
@Override
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
MetaData metaData = (MetaData) source;
if (!metaData.isEmpty()) {
super.marshal(new HashMap<>(metaData), writer, context);
}
}
@Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
if (!reader.hasMoreChildren()) {
return MetaData.emptyInstance();
}
Map<String, Object> contents = new HashMap<>();
populateMap(reader, context, contents);
if (contents.isEmpty()) {
return MetaData.emptyInstance();
} else {
return MetaData.from(contents);
}
}
}
}