Explicit XML serialization of base types - java

Using Jackson to serialize a simple type hierarchy:
static class Base {
#JsonProperty
private final int foo = 42;
}
static class Derived extends Base {
#JsonProperty
private final int bar = 39;
}
Using the Jackson XmlMapper I'm getting the following output:
<Derived>
<foo>42</foo>
<bar>39</bar>
</Derived>
What I'd really like to get is XML containing the base type:
<Derived>
<Base>
<foo>42</foo>
</Base>
<bar>39</bar>
</Derived>
I browsed the Jackson API, especially the type annotations and SerializationFeature, but couldn't figure out how to achieve this.
Any ideas?
I'm looking for a generic approach since I'm dealing with several hundred classes in deeply nested hierarchies.
Update
At least I figured out how to do this using XStream:
public class TypeHierarchyConverter implements Converter {
#Override
public boolean canConvert(#SuppressWarnings("rawtypes") final Class clazz) {
return Base.class.isAssignableFrom(clazz);
}
#Override
public void marshal(final Object value, final HierarchicalStreamWriter writer,
final MarshallingContext context) {
try {
enumerateFields(value, writer, context);
} catch (final IllegalAccessException e) {
e.printStackTrace();
}
}
#Override
public Object unmarshal(final HierarchicalStreamReader reader, final UnmarshallingContext context) {
return null;
}
private void enumerateFields(final Object value, final HierarchicalStreamWriter writer,
final MarshallingContext context) throws IllegalAccessException {
enumerateFields(value, writer, context, value.getClass(), false);
}
private void enumerateFields(final Object value, final HierarchicalStreamWriter writer,
final MarshallingContext context, final Class<?> c, final boolean nest) throws IllegalAccessException {
final String name = c.getSimpleName();
if (nest) {
writer.startNode(name);
}
final Class<?> superclass = c.getSuperclass();
if (!Object.class.equals(superclass)) {
enumerateFields(value, writer, context, superclass, true);
}
final Field[] fields = c.getDeclaredFields();
for (final Field f : fields) {
f.setAccessible(true);
writer.startNode(f.getName());
context.convertAnother(f.get(value));
writer.endNode();
f.setAccessible(false);
}
if (nest) {
writer.endNode();
}
}
}
Still no idea hot to do this with Jackson. :(

Related

Xstream : unmarshalling inner classes

I would like to marshall and unmarshall objects whose fields are of their class's inner class (synthetic class if I'm not wrong).
class A {
private B field_b=null;
public static class B {
public static B B1 = new B("b1");
public static B B2 = new B("b2");
private final String name;
private B(String name) {
this.name=name;
}
}
public B getBforName(String name) {
if (B1.name.equals(name) return B1;
else if (B2.name.equals(name) return B2;
else return null;
}
And produce and read from an XML:
<A>
<field_B>b1</field_B>
</A>
The writing part is easy.
The reading part is more complicated.
I would like to write a converter:
public class BConverter implements Converter {
public boolean canConvert(Class type) {
return B.class.isAssignableFrom(type) ;
}
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
// ...
}
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
A parent_v1 = (A) context.getCurrentObject(); // !!! always empty
A parent_v2 = (A) context.get("current_unmarshalled_A");
return parent_v2.getBforName((String)reader.getValue());
}
}
The context.getCurrentObject() returns null and seems to be obsolete (from forums I read).
The context.get("current_unmarshalled_A")would require that I put in this unMarshallingContext that key and the A object being unmarshalled. I don't find to do that without writing an AConverter. And that is not neat as I would loose the default unmarshalling behaviour for the class A.
Anyone has an idea ?
I'm not sure this is cleanest way to that but it works.
I use a static method in the B class ...
class A {
private B field_b=null;
public static class B {
public static B B1 = new B("b1");
public static B B2 = new B("b2");
public static B getForName(String name) {
if (B1.name.equals(name) return B1;
else if (B2.name.equals(name) return B2;
else return null;
}
private final String name;
private B(String name) {
this.name=name;
}
}
... and reflection in the Converter
public class BConverter implements Converter {
public boolean canConvert(Class type) {
return B.class.isAssignableFrom(type) ;
}
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
writer.setValue(((B)source).getName())
}
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
try {
Method method = context.getRequiredType().getMethod("getForName", String.class);
final String v = reader.getValue();
Object b= method.invoke(null, v);
if (b== null)
throw new ConversionException("Could not retrieve a B object for \"" + v + "\"");
return b;
} catch (Exception ex) {
throw new ConversionException("Error while retrieving a B object", ex);
}
}
}

Lower case enum Gson

I need to output enum values using Gson which due to client limitations need to be in lower case.
For example CLOSE_FILE would be close_file.
Is there a simple way of doing this? I have looked at making a class which implements JsonSerializer but it looks like I would have to manually serialize the whole class (which is quite complex) is this the case?
If you have control over the enum type, annotate its members with #SerializedName and give it the appropriate serialized value. For example,
enum Action {
#SerializedName("close_file")
CLOSE_FILE;
}
If you don't have control over the enum, provide a custom TypeAdapter when creating a Gson instance. For example,
Gson gson = new GsonBuilder().registerTypeAdapter(Action.class, new TypeAdapter<Action>() {
#Override
public void write(JsonWriter out, Action value) throws IOException {
out.value(value.name().toLowerCase());
}
#Override
public Action read(JsonReader in) throws IOException {
return Action.valueOf(in.nextString().toUpperCase());
}
}).create();
If you want to serialize all enum to lowercase, you can use this code
Gson gson = new GsonBuilder().registerTypeHierarchyAdapter(Enum.class, new EnumToLowerCaseJsonConverter()).create();
public final class EnumToLowerCaseJsonConverter implements JsonSerializer<Enum<?>>, JsonDeserializer<Enum<?>> {
private static Map<String, Class<? extends Enum<?>>> typesToClass = new HashMap<>();
#Override
public JsonElement serialize(final Enum<?> src, final Type typeOfSrc,
final JsonSerializationContext context) {
if (src == null) {
return JsonNull.INSTANCE;
}
return new JsonPrimitive(src.name().toLowerCase());
}
#SuppressWarnings("unchecked")
#Override
public Enum<?> deserialize(final JsonElement json, final Type typeOfT,
final JsonDeserializationContext context) throws JsonParseException {
if (json == null || json.isJsonNull()) {
return null;
}
if (!json.isJsonPrimitive() || !json.getAsJsonPrimitive().isString()) {
throw new JsonParseException(
"Expecting a String JsonPrimitive, getting " + json.toString());
}
try {
final String enumClassName = typeOfT.getTypeName();
Class<? extends Enum<?>> clazz = typesToClass.get(enumClassName);
if (clazz == null) {
clazz = (Class<? extends Enum<?>>) Class.forName(enumClassName);
typesToClass.put(enumClassName, clazz);
}
return Enum.valueOf((Class) clazz, json.getAsString().toUpperCase());
} catch (final ClassNotFoundException e) {
throw new JsonParseException(e);
}
}
}
An easy to use specific type adapter:
Gson gson = new GsonBuilder().registerTypeAdapter(Action.class, new EnumToLowerCaseTypeAdapter<>(Action.class)).create();
public final class EnumToLowerCaseTypeAdapter<T extends Enum<?>> extends TypeAdapter<T> {
private final Class<T> clazz;
public EnumToLowerCaseTypeAdapter(final Class<T> clazz) {
this.clazz = clazz;
}
#Override
public void write(final JsonWriter out, final T value) throws IOException {
if (value == null) {
out.nullValue();
} else {
out.value(value.name().toLowerCase());
}
}
#SuppressWarnings("unchecked")
#Override
public T read(final JsonReader in) throws IOException {
switch (in.peek()) {
case NULL:
in.nextNull();
return null;
default:
final String value = in.nextString();
return (T) Enum.valueOf((Class) clazz, value.toUpperCase());
}
}
}

Xstream field with converter is not an attribute even with #XStreamAsAttribute

I try to marshal an object and I want all the fields to be attributes. The normal fields are OK with the #XStreamAsAttribute annotation but I have two of them with a converter. For them when I marshal they are converted as field...
#XStreamAlias(value="sinistre")
public class ObjetMetierSinistreDto {
#XStreamAlias(value="S_sinistreEtat")
#XStreamAsAttribute
private String etat;
#XStreamAsAttribute
#XStreamAlias(value="S_sinistreDateSurv")
#XStreamConverter(value=JodaDateConverter.class)
private LocalDate dateSurvenanceDossier;
...
The converter:
public class JodaDateConverter implements Converter {
#Override
#SuppressWarnings("unchecked")
public boolean canConvert(final Class type) {
return (type != null) && LocalDate.class.getPackage().equals(type.getPackage());
}
#Override
public void marshal(final Object source, final HierarchicalStreamWriter writer,
final MarshallingContext context) {
writer.setValue(source.toString().replace("-", "/"));
}
#Override
#SuppressWarnings("unchecked")
public Object unmarshal(final HierarchicalStreamReader reader,
final UnmarshallingContext context) {
try {
final Class requiredType = context.getRequiredType();
final Constructor constructor = requiredType.getConstructor(Object.class);
return constructor.newInstance(reader.getValue());
} catch (final Exception e) {
throw new RuntimeException(String.format(
"Exception while deserializing a Joda Time object: %s", context.getRequiredType().getSimpleName()), e);
}
}
}
and the result:
<sinistre S_sinistreEtat="S">
<S_sinistreDateSurv>2015/02/01</S_sinistreDateSurv>
</sinistre>
and what I like:
<sinistre S_sinistreEtat="S"
S_sinistreDateSurv="2015/02/01"/>
I finally found how to solve this problem!
The JodaDateConverter should not implements Converter but extends AbstractSingleValueConverter (as the DateConverter from XStream)
Then you just need to override canConvert() and fromString() and you are good to go!
Exemple:
public class JodaDateConverter extends AbstractSingleValueConverter {
#Override
#SuppressWarnings("unchecked")
public boolean canConvert(final Class type) {
return (type != null) && LocalDate.class.getPackage().equals(type.getPackage());
}
#Override
public Object fromString(String str) {
String separator;
if(str.contains(":")){
separator = ":";
} else if(str.contains("/")){
separator = "/";
} else if(str.contains("-")){
separator = "-";
} else {
throw new RuntimeException("The date must contains ':' or '/' or '-'");
}
String[] date = str.split(separator);
if(date.length < 3){
throw new RuntimeException("The date must contains hour, minute and second");
}
return new LocalDate(Integer.valueOf(date[0]),Integer.valueOf(date[1]),Integer.valueOf(date[2]));
}
}

GSON won't properly serialise a class that extends HashMap

I have the following code:
public static class A
{
public A() {}
private List<B> bs = new ArrayList<B>();
public List<B> getBs() {
return bs;
}
public void setBs(List<B> bs) {
this.bs = bs;
}
}
public static class B
{
B(String foo){this.foo=foo;}
private String foo;
public String getFoo() {
return foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
}
public static void main(String[] args) throws Exception {
Gson gson = new Gson();
A a = new A();
a.getBs().add(new B("bar"));
System.out.println(gson.toJson(a));
}
and as expected the output is:
{"bs":[{"foo":"bar"}]}
However, if I make A a subclass of HashMap:
public static class A extends HashMap
I get an empty set returned: {}
I have even tried:
System.out.println(gson.toJson(a, new TypeToken<A>(){}.getType()));
and:
System.out.println(gson.toJson(a, new TypeToken<HashMap>(){}.getType()));
Can someone tell me whether/how I can serialise this HashMap subclass using GSON?
Gson works with (default and custom) TypeAdapterFactory instances and the TypeAdapter objects they create to serialize/deserialize your objects.
It goes through the list of registered TypeAdapterFactory objects and picks the first one that can create an appropriate TypeAdapter for the type of the object your are providing. One of these TypeAdapterFactory objects, is one of type MapTypeAdapterFactory which creates a TypeAdapter (of type MapTypeAdapterFactory$Adapter) that serializes/deserializes based on the java.util.Map interface (keys/values). It does nothing about your custom sub type's fields.
If you want Gson to serialize your type as both a Map and a custom type, you will need to register either a custom TypeAdapter directly or a custom TypeAdapterFactory that creates TypeAdapter objects.
Here is the custom TypeAdapterFactory.
Test:
public static void main(String[] args) throws Exception{
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new RetainFieldMapFactory())
.create();
Foo f = gson.fromJson("{'key1':'value1','key2':'value2'}", Foo.class);
System.out.println("in map:\t" + f.toString());
System.out.println("f.key1:\t"+f.key1);
System.out.println("toJson:\t"+gson.toJson(f));
}
public static class Foo extends HashMap<String, String> {
private String key1;
}
Output:
in map: {key2=value2}
f.key1: value1
toJson: {"key2":"value2","key1":"value1"}
RetainFieldMapFactory.java:
/**
* Created by linfaxin on 2015/4/9 009.
* Email: linlinfaxin#163.com
*/
public class RetainFieldMapFactory implements TypeAdapterFactory {
FieldNamingPolicy fieldNamingPolicy = FieldNamingPolicy.IDENTITY;
ConstructorConstructor constructorConstructor = new ConstructorConstructor(Collections.<Type, InstanceCreator<?>>emptyMap());
MapTypeAdapterFactory defaultMapFactory = new MapTypeAdapterFactory(constructorConstructor, false);
ReflectiveFilterMapFieldFactory defaultObjectFactory = new ReflectiveFilterMapFieldFactory(constructorConstructor,
fieldNamingPolicy, Excluder.DEFAULT);
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
final TypeAdapter<T> mapAdapter = defaultMapFactory.create(gson, type);
if(mapAdapter!=null){
return (TypeAdapter<T>) new RetainFieldMapAdapter(mapAdapter, defaultObjectFactory.create(gson, type));
}
return mapAdapter;
}
class RetainFieldMapAdapter extends TypeAdapter<Map<String, Object>>{
TypeAdapter<Map<String, Object>> mapAdapter;
ReflectiveTypeAdapterFactory.Adapter<Map<String, Object>> objectAdapter;
RetainFieldMapAdapter(TypeAdapter mapAdapter, ReflectiveTypeAdapterFactory.Adapter objectAdapter) {
this.mapAdapter = mapAdapter;
this.objectAdapter = objectAdapter;
}
#Override
public void write(final JsonWriter out, Map<String, Object> value) throws IOException {
//1.write object
StringWriter sw = new StringWriter();
objectAdapter.write(new JsonWriter(sw), value);
//2.convert object to a map
Map<String, Object> objectMap = mapAdapter.fromJson(sw.toString());
//3.overwrite fields in object to a copy map
value = new LinkedHashMap<String, Object>(value);
value.putAll(objectMap);
//4.write the copy map
mapAdapter.write(out, value);
}
#Override
public Map<String, Object> read(JsonReader in) throws IOException {
//1.create map, all key-value retain in map
Map<String, Object> map = mapAdapter.read(in);
//2.create object from created map
Map<String, Object> object = objectAdapter.fromJsonTree(mapAdapter.toJsonTree(map));
//3.remove fields in object from map
for(String field : objectAdapter.boundFields.keySet()){
map.remove(field);
}
//4.put map to object
object.putAll(map);
return object;
}
}
/**
* If class is extends from some custom map,
* class should implement this to avoid serialize custom map's fields
*/
public interface RetainFieldFlag {}
static class ReflectiveFilterMapFieldFactory extends ReflectiveTypeAdapterFactory{
public ReflectiveFilterMapFieldFactory(ConstructorConstructor constructorConstructor, FieldNamingStrategy fieldNamingPolicy, Excluder excluder) {
super(constructorConstructor, fieldNamingPolicy, excluder);
}
#Override
protected boolean shouldFindFieldInClass(Class willFindClass, Class<?> originalRaw) {
if(RetainFieldFlag.class.isAssignableFrom(originalRaw)){
return RetainFieldFlag.class.isAssignableFrom(willFindClass);
}else{
Class[] endClasses = new Class[]{Object.class, HashMap.class, LinkedHashMap.class,
LinkedTreeMap.class, Hashtable.class, TreeMap.class, ConcurrentHashMap.class,
IdentityHashMap.class, WeakHashMap.class, EnumMap.class};
for(Class c : endClasses){
if(willFindClass == c) return false;
}
}
return super.shouldFindFieldInClass(willFindClass, originalRaw);
}
}
/**
* below code copy from {#link com.google.gson.internal.bind.ReflectiveTypeAdapterFactory}
* (little modify, in source this class is final)
* Type adapter that reflects over the fields and methods of a class.
*/
static class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
private final ConstructorConstructor constructorConstructor;
private final FieldNamingStrategy fieldNamingPolicy;
private final Excluder excluder;
public ReflectiveTypeAdapterFactory(ConstructorConstructor constructorConstructor,
FieldNamingStrategy fieldNamingPolicy, Excluder excluder) {
this.constructorConstructor = constructorConstructor;
this.fieldNamingPolicy = fieldNamingPolicy;
this.excluder = excluder;
}
public boolean excludeField(Field f, boolean serialize) {
return !excluder.excludeClass(f.getType(), serialize) && !excluder.excludeField(f, serialize);
}
private String getFieldName(Field f) {
SerializedName serializedName = f.getAnnotation(SerializedName.class);
return serializedName == null ? fieldNamingPolicy.translateName(f) : serializedName.value();
}
public <T> Adapter<T> create(Gson gson, final TypeToken<T> type) {
Class<? super T> raw = type.getRawType();
if (!Object.class.isAssignableFrom(raw)) {
return null; // it's a primitive!
}
ObjectConstructor<T> constructor = constructorConstructor.get(type);
return new Adapter<T>(constructor, getBoundFields(gson, type, raw));
}
private ReflectiveTypeAdapterFactory.BoundField createBoundField(
final Gson context, final Field field, final String name,
final TypeToken<?> fieldType, boolean serialize, boolean deserialize) {
final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType());
// special casing primitives here saves ~5% on Android...
return new ReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) {
final TypeAdapter<?> typeAdapter = context.getAdapter(fieldType);
#SuppressWarnings({"unchecked", "rawtypes"}) // the type adapter and field type always agree
#Override void write(JsonWriter writer, Object value)
throws IOException, IllegalAccessException {
Object fieldValue = field.get(value);
TypeAdapter t = new TypeAdapterRuntimeTypeWrapper(context, this.typeAdapter, fieldType.getType());
t.write(writer, fieldValue);
}
#Override void read(JsonReader reader, Object value)
throws IOException, IllegalAccessException {
Object fieldValue = typeAdapter.read(reader);
if (fieldValue != null || !isPrimitive) {
field.set(value, fieldValue);
}
}
};
}
private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw) {
Map<String, BoundField> result = new LinkedHashMap<String, BoundField>();
if (raw.isInterface()) {
return result;
}
Type declaredType = type.getType();
Class<?> originalRaw = type.getRawType();
while (shouldFindFieldInClass(raw, originalRaw)) {
Field[] fields = raw.getDeclaredFields();
for (Field field : fields) {
boolean serialize = excludeField(field, true);
boolean deserialize = excludeField(field, false);
if (!serialize && !deserialize) {
continue;
}
field.setAccessible(true);
Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType());
BoundField boundField = createBoundField(context, field, getFieldName(field),
TypeToken.get(fieldType), serialize, deserialize);
BoundField previous = result.put(boundField.name, boundField);
if (previous != null) {
throw new IllegalArgumentException(declaredType
+ " declares multiple JSON fields named " + previous.name);
}
}
type = TypeToken.get($Gson$Types.resolve(type.getType(), raw, raw.getGenericSuperclass()));
raw = type.getRawType();
}
return result;
}
protected boolean shouldFindFieldInClass(Class willFindClass, Class<?> originalRaw){
return willFindClass != Object.class;
}
static abstract class BoundField {
final String name;
final boolean serialized;
final boolean deserialized;
protected BoundField(String name, boolean serialized, boolean deserialized) {
this.name = name;
this.serialized = serialized;
this.deserialized = deserialized;
}
abstract void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException;
abstract void read(JsonReader reader, Object value) throws IOException, IllegalAccessException;
}
public static final class Adapter<T> extends TypeAdapter<T> {
private final ObjectConstructor<T> constructor;
private final Map<String, BoundField> boundFields;
private Adapter(ObjectConstructor<T> constructor, Map<String, BoundField> boundFields) {
this.constructor = constructor;
this.boundFields = boundFields;
}
#Override public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
T instance = constructor.construct();
try {
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
BoundField field = boundFields.get(name);
if (field == null || !field.deserialized) {
in.skipValue();
} else {
field.read(in, instance);
}
}
} catch (IllegalStateException e) {
throw new JsonSyntaxException(e);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
in.endObject();
return instance;
}
#Override public void write(JsonWriter out, T value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
out.beginObject();
try {
for (BoundField boundField : boundFields.values()) {
if (boundField.serialized) {
out.name(boundField.name);
boundField.write(out, value);
}
}
} catch (IllegalAccessException e) {
throw new AssertionError();
}
out.endObject();
}
}
}
static class TypeAdapterRuntimeTypeWrapper<T> extends TypeAdapter<T> {
private final Gson context;
private final TypeAdapter<T> delegate;
private final Type type;
TypeAdapterRuntimeTypeWrapper(Gson context, TypeAdapter<T> delegate, Type type) {
this.context = context;
this.delegate = delegate;
this.type = type;
}
#Override
public T read(JsonReader in) throws IOException {
return delegate.read(in);
}
#SuppressWarnings({"rawtypes", "unchecked"})
#Override
public void write(JsonWriter out, T value) throws IOException {
// Order of preference for choosing type adapters
// First preference: a type adapter registered for the runtime type
// Second preference: a type adapter registered for the declared type
// Third preference: reflective type adapter for the runtime type (if it is a sub class of the declared type)
// Fourth preference: reflective type adapter for the declared type
TypeAdapter chosen = delegate;
Type runtimeType = getRuntimeTypeIfMoreSpecific(type, value);
if (runtimeType != type) {
TypeAdapter runtimeTypeAdapter = context.getAdapter(TypeToken.get(runtimeType));
if (!(runtimeTypeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter)) {
// The user registered a type adapter for the runtime type, so we will use that
chosen = runtimeTypeAdapter;
} else if (!(delegate instanceof ReflectiveTypeAdapterFactory.Adapter)) {
// The user registered a type adapter for Base class, so we prefer it over the
// reflective type adapter for the runtime type
chosen = delegate;
} else {
// Use the type adapter for runtime type
chosen = runtimeTypeAdapter;
}
}
chosen.write(out, value);
}
/**
* Finds a compatible runtime type if it is more specific
*/
private Type getRuntimeTypeIfMoreSpecific(Type type, Object value) {
if (value != null
&& (type == Object.class || type instanceof TypeVariable<?> || type instanceof Class<?>)) {
type = value.getClass();
}
return type;
}
}
}

Xstream's jodatime Local Date display

I'm using xstrem to serialise a jodatime local date into xml.
However when output the generated xml the LocalDate is not in an easily readable format.
See below:
<date>
<iLocalMillis>1316563200000</iLocalMillis>
<iChronology class="org.joda.time.chrono.ISOChronology" reference="../../tradeDate/iChronology"/>
Any ideas how I can get xstream to display the date in a format that won't drive me up the wall?
Here's what I have used successfully. I believe I used the info at the link mentioned in the first post.
import java.lang.reflect.Constructor;
import org.joda.time.DateTime;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
public final class JodaTimeConverter implements Converter {
#Override
#SuppressWarnings("unchecked")
public boolean canConvert(final Class type) {
return (type != null) && DateTime.class.getPackage().equals(type.getPackage());
}
#Override
public void marshal(final Object source, final HierarchicalStreamWriter writer,
final MarshallingContext context) {
writer.setValue(source.toString());
}
#Override
#SuppressWarnings("unchecked")
public Object unmarshal(final HierarchicalStreamReader reader,
final UnmarshallingContext context) {
try {
final Class requiredType = context.getRequiredType();
final Constructor constructor = requiredType.getConstructor(Object.class);
return constructor.newInstance(reader.getValue());
} catch (final Exception e) {
throw new RuntimeException(String.format(
"Exception while deserializing a Joda Time object: %s", context.getRequiredType().getSimpleName()), e);
}
}
}
You can register it like:
XStream xstream = new XStream(new StaxDriver());
xstream.registerConverter(new JodaTimeConverter());
The version from #Ben Carlson has an issue if your object tree contains other classes from the same package as DateTime.
A more robust version for converting DateTime to XML and back that does not require reflection as well:
public static class JodaTimeConverter implements Converter
{
#Override
#SuppressWarnings("unchecked")
public boolean canConvert( final Class type )
{
return DateTime.class.isAssignableFrom( type );
}
#Override
public void marshal( Object source, HierarchicalStreamWriter writer, MarshallingContext context )
{
writer.setValue( source.toString() );
}
#Override
#SuppressWarnings("unchecked")
public Object unmarshal( HierarchicalStreamReader reader,
UnmarshallingContext context )
{
return new DateTime( reader.getValue() );
}
}
Register the converter with XStream to use it:
XStream xstream = new XStream();
xstream.registerConverter(new JodaTimeConverter());
We needed a to convert a Joda DateTime to / from an XML attribute. For that, converters need to implement interface SingleValueConverter. Our final implementation:
package com.squins.xstream.joda;
import org.joda.time.DateTime;
import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.converters.basic.AbstractSingleValueConverter;
public final class JodaDateTimeConverter extends AbstractSingleValueConverter
{
#Override
public boolean canConvert(final Class type)
{
return DateTime.class.equals(type);
}
#Override
public Object fromString(String str)
{
try
{
return new DateTime(str);
}
catch (final Exception e)
{
throw new ConversionException("Cannot parse date " + str);
}
}
}
You have to implement (or find) a custom converter for xstream, which will handle JodaTime object in a way you find appropriate.
Here is a small example of such converter: http://x-stream.github.io/converter-tutorial.html
I've used the one that it is here. Pasting it for simplicity:
public class JodaTimeConverter implements Converter
{
#Override
public boolean canConvert(Class type) {
return type != null && DateTime.class.getPackage().equals(type.getPackage());
}
#Override
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
writer.setValue(source.toString());
}
#Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
try {
Constructor constructor = context.getRequiredType().getConstructor(Object.class);
return constructor.newInstance(reader.getValue());
} catch (Exception e) { // NOSONAR
throw new SerializationException(String.format(
"An exception occurred while deserializing a Joda Time object: %s",
context.getRequiredType().getSimpleName()), e);
}
}
}
The other samples didn't work.
Cheers!

Categories