From 669d8d68d45b8fc70cd0d9bdaa144592cfd096b8 Mon Sep 17 00:00:00 2001 From: Steven Hood Date: Tue, 27 Jan 2015 03:17:41 +0000 Subject: [PATCH] Support Tables Guava's Tables don't quite fit under map-like types, though they're often used in place of nested maps. In particular, ImmutableTables don't construct themselves as nested maps; instead, they construct cell-by-cell. Therefore, handle them as paramterized beans. --- .../datatype/guava/GuavaDeserializers.java | 14 +- .../datatype/guava/GuavaSerializers.java | 8 + .../datatype/guava/GuavaTypeModifier.java | 8 + .../GuavaImmutableTableDeserializer.java | 66 ++++++ .../guava/deser/GuavaTableDeserializer.java | 128 ++++++++++++ .../deser/ImmutableTableDeserializer.java | 44 ++++ .../datatype/guava/ser/TableSerializer.java | 192 ++++++++++++++++++ .../ImmutableTableSerializationTest.java | 172 ++++++++++++++++ 8 files changed, 627 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaImmutableTableDeserializer.java create mode 100644 src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaTableDeserializer.java create mode 100644 src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableTableDeserializer.java create mode 100644 src/main/java/com/fasterxml/jackson/datatype/guava/ser/TableSerializer.java create mode 100644 src/test/java/com/fasterxml/jackson/datatype/guava/ImmutableTableSerializationTest.java diff --git a/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaDeserializers.java b/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaDeserializers.java index c728611..2dc52f8 100644 --- a/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaDeserializers.java +++ b/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaDeserializers.java @@ -5,7 +5,6 @@ import com.google.common.hash.HashCode; import com.google.common.net.HostAndPort; import com.google.common.net.InternetDomainName; - import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.Deserializers; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; @@ -206,10 +205,6 @@ public JsonDeserializer findMapLikeDeserializer(MapLikeType type, elementTypeDeserializer, elementDeserializer); } - if (Table.class.isAssignableFrom(raw)) { - // !!! TODO - } - return null; } @@ -241,6 +236,15 @@ public JsonDeserializer findBeanDeserializer(final JavaType type, Deserializa if (raw == HashCode.class) { return HashCodeDeserializer.std; } + + // Tables don't fit in the MapLike method because they have three type parameters + // since 2.5.1 + if (Table.class.isAssignableFrom(raw)) { + if (ImmutableTable.class.isAssignableFrom(raw)) { + return new ImmutableTableDeserializer(type); + } + // TODO: Other Table types + } return super.findBeanDeserializer(type, config, beanDesc); } } diff --git a/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaSerializers.java b/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaSerializers.java index bff2fad..fd70f9a 100644 --- a/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaSerializers.java +++ b/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaSerializers.java @@ -12,6 +12,7 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.Multimap; import com.google.common.collect.Range; +import com.google.common.collect.Table; import com.google.common.hash.HashCode; import com.google.common.net.HostAndPort; import com.google.common.net.InternetDomainName; @@ -24,6 +25,7 @@ import com.fasterxml.jackson.datatype.guava.ser.GuavaOptionalSerializer; import com.fasterxml.jackson.datatype.guava.ser.MultimapSerializer; import com.fasterxml.jackson.datatype.guava.ser.RangeSerializer; +import com.fasterxml.jackson.datatype.guava.ser.TableSerializer; public class GuavaSerializers extends Serializers.Base { @@ -70,6 +72,12 @@ public JsonSerializer findSerializer(SerializationConfig config, JavaType typ // JavaType delegate = config.getTypeFactory().constructParametrizedType(FluentIterable.class, Iterable.class, vt); return new StdDelegatingSerializer(FluentConverter.instance, delegate, null); } + + // since 2.5.1 + if (Table.class.isAssignableFrom(raw)) { + return new TableSerializer(config, type); + } + return super.findSerializer(config, type, beanDesc); } diff --git a/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaTypeModifier.java b/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaTypeModifier.java index 88ce34e..c7985e8 100644 --- a/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaTypeModifier.java +++ b/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaTypeModifier.java @@ -11,6 +11,7 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.Multimap; import com.google.common.collect.Range; +import com.google.common.collect.Table; public class GuavaTypeModifier extends TypeModifier { @@ -82,6 +83,13 @@ public JavaType modifyType(JavaType type, Type jdkType, TypeBindings context, Ty return typeFactory.constructParametrizedType(raw, target, t); } } + if (Table.class.isAssignableFrom(type.getRawClass())) { + final JavaType rowType = (type.containedType(0)) == null ? typeFactory.constructType(Object.class) : type.containedType(0); + final JavaType columnType = (type.containedType(1)) == null ? typeFactory.constructType(Object.class) : type.containedType(1); + final JavaType contentType = (type.containedType(2)) == null ? typeFactory.constructType(Object.class) : type.containedType(2); + + return typeFactory.constructParametrizedType(type.getRawClass(), type.getRawClass(), rowType, columnType, contentType); + } return type; } } diff --git a/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaImmutableTableDeserializer.java b/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaImmutableTableDeserializer.java new file mode 100644 index 0000000..cec9e8e --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaImmutableTableDeserializer.java @@ -0,0 +1,66 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; + +import com.google.common.collect.ImmutableTable; + +abstract class GuavaImmutableTableDeserializer> extends GuavaTableDeserializer +{ + GuavaImmutableTableDeserializer( final JavaType javaType ) + { + super(javaType); + } + + protected abstract ImmutableTable.Builder createBuilder(); + + @Override + protected T _deserializeEntries( final JsonParser jp, final DeserializationContext ctxt ) throws IOException, JsonProcessingException + { + final KeyDeserializer rowKeyDes = this._rowKeyDeserializer; + final KeyDeserializer columnKeyDes = this._columnKeyDeserializer; + final JsonDeserializer valueDes = this._valueDeserializer; + final TypeDeserializer typeDeser = this._typeDeserializerForValue; + + final ImmutableTable.Builder builder = this.createBuilder(); + for ( ; jp.getCurrentToken() == JsonToken.FIELD_NAME; jp.nextToken() ) { + // Must point to row now + final String rowName = jp.getCurrentName(); + final Object row = (rowKeyDes == null) ? rowName : rowKeyDes.deserializeKey(rowName, ctxt); + // And then the {column => value} start token... + jp.nextToken(); + // Now pointing to column + jp.nextToken(); + + for ( ; jp.getCurrentToken() == JsonToken.FIELD_NAME; jp.nextToken() ) { + // Must point to column now + final String columnName = jp.getCurrentName(); + final Object column = (columnKeyDes == null) ? columnName : columnKeyDes.deserializeKey(columnName, ctxt); + // And then the value... + final JsonToken tValue = jp.nextToken(); + // 28-Nov-2010, tatu: Should probably support "ignorable properties" in future... + Object value; + if (tValue == JsonToken.VALUE_NULL) { + value = null; + } + else { + value = (typeDeser == null) ? valueDes.deserialize(jp, ctxt) : valueDes.deserializeWithType(jp, ctxt, typeDeser); + builder.put(row, column, value); + } + } + } + // No class outside of the package will be able to subclass us, + // and we provide the proper builder for the subclasses we implement. + @SuppressWarnings( "unchecked" ) + final T table = (T) builder.build(); + return table; + } +} diff --git a/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaTableDeserializer.java b/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaTableDeserializer.java new file mode 100644 index 0000000..c55acaa --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaTableDeserializer.java @@ -0,0 +1,128 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; + +public abstract class GuavaTableDeserializer extends JsonDeserializer implements ContextualDeserializer +{ + protected final JavaType _javaType; + + /** + * Row key deserializer used, if not null. If null, String from JSON content is used as is. + */ + protected KeyDeserializer _rowKeyDeserializer; + + /** + * Column key deserializer used, if not null. If null, String from JSON content is used as is. + */ + protected KeyDeserializer _columnKeyDeserializer; + + /** + * Value deserializer. + */ + protected JsonDeserializer _valueDeserializer; + + /** + * If value instances have polymorphic type information, this is the type deserializer that can handle it. + */ + protected TypeDeserializer _typeDeserializerForValue; + + /* + * Life-cycle + */ + + protected GuavaTableDeserializer( final JavaType javaType ) + { + this._javaType = javaType; + } + + /** + * Overridable fluent factory method used for creating contextual instances. + */ + public abstract GuavaTableDeserializer withResolved( final KeyDeserializer rowKeyDeser, + final KeyDeserializer columnKeyDeser, + final TypeDeserializer typeDeser, + final JsonDeserializer valueDeser ); + + /* + * Validation, post-processing + */ + + /** + * Method called to finalize setup of this deserializer, after deserializer itself has been registered. This is needed to handle recursive and + * transitive dependencies. + */ + @Override + public JsonDeserializer createContextual( final DeserializationContext ctxt, final BeanProperty property ) throws JsonMappingException + { + KeyDeserializer rowKeyDeser = this._rowKeyDeserializer; + KeyDeserializer columnKeyDeser = this._columnKeyDeserializer; + JsonDeserializer deser = this._valueDeserializer; + TypeDeserializer typeDeser = this._typeDeserializerForValue; + // Do we need any contextualization? + if ((rowKeyDeser != null) && (columnKeyDeser != null) && (deser != null) && (typeDeser == null)) { // nope + return this; + } + if (rowKeyDeser == null) { + rowKeyDeser = ctxt.findKeyDeserializer(this._javaType.containedType(0), property); + } + if (columnKeyDeser == null) { + columnKeyDeser = ctxt.findKeyDeserializer(this._javaType.containedType(1), property); + } + if (deser == null) { + deser = ctxt.findContextualValueDeserializer(this._javaType.containedType(2), property); + } + if (typeDeser != null) { + typeDeser = typeDeser.forProperty(property); + } + return this.withResolved(rowKeyDeser, columnKeyDeser, typeDeser, deser); + } + + /* + * Deserialization interface + */ + + /** + * Base implementation that does not assume specific type inclusion mechanism. Sub-classes are expected to override this method if they are to + * handle type information. + */ + @Override + public Object deserializeWithType( final JsonParser jp, final DeserializationContext ctxt, final TypeDeserializer typeDeserializer ) + throws IOException, JsonProcessingException + { + // note: call "...FromObject" because expected output structure + // for value is JSON Object (regardless of contortions used for type id) + return typeDeserializer.deserializeTypedFromObject(jp, ctxt); + } + + @Override + public T deserialize( final JsonParser jp, final DeserializationContext ctxt ) throws IOException, JsonProcessingException + { + // Ok: must point to START_OBJECT or FIELD_NAME + JsonToken t = jp.getCurrentToken(); + if (t == JsonToken.START_OBJECT) { // If START_OBJECT, move to next; may also be END_OBJECT + t = jp.nextToken(); + } + if (t != JsonToken.FIELD_NAME && t != JsonToken.END_OBJECT) { + throw ctxt.mappingException(this._javaType.getRawClass()); + } + return this._deserializeEntries(jp, ctxt); + } + + /* + * Abstract methods for impl classes + */ + + protected abstract T _deserializeEntries( final JsonParser jp, final DeserializationContext ctxt ) throws IOException, JsonProcessingException; +} diff --git a/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableTableDeserializer.java b/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableTableDeserializer.java new file mode 100644 index 0000000..46cf2f4 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableTableDeserializer.java @@ -0,0 +1,44 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; + +import com.google.common.collect.ImmutableTable; + +public class ImmutableTableDeserializer extends GuavaImmutableTableDeserializer> +{ + public ImmutableTableDeserializer( final JavaType javaType ) + { + super(javaType); + } + + public ImmutableTableDeserializer( final JavaType javaType, + final KeyDeserializer rowKeyDeser, + final KeyDeserializer columnKeyDeser, + final TypeDeserializer typeDeser, + final JsonDeserializer deser ) + { + super(javaType); + this._rowKeyDeserializer = rowKeyDeser; + this._columnKeyDeserializer = columnKeyDeser; + this._valueDeserializer = deser; + this._typeDeserializerForValue = typeDeser; + } + + @Override + public ImmutableTableDeserializer withResolved( final KeyDeserializer rowKeyDeser, + final KeyDeserializer columnKeyDeser, + final TypeDeserializer typeDeser, + final JsonDeserializer valueDeser ) + { + return new ImmutableTableDeserializer(this._javaType, rowKeyDeser, columnKeyDeser, typeDeser, valueDeser); + } + + @Override + protected ImmutableTable.Builder createBuilder() + { + return ImmutableTable.builder(); + } +} diff --git a/src/main/java/com/fasterxml/jackson/datatype/guava/ser/TableSerializer.java b/src/main/java/com/fasterxml/jackson/datatype/guava/ser/TableSerializer.java new file mode 100644 index 0000000..aab31ac --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/datatype/guava/ser/TableSerializer.java @@ -0,0 +1,192 @@ +package com.fasterxml.jackson.datatype.guava.ser; + +import java.io.IOException; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.ser.ContainerSerializer; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.fasterxml.jackson.databind.ser.std.MapSerializer; +import com.fasterxml.jackson.databind.type.MapType; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import com.google.common.collect.Table; + +public class TableSerializer extends ContainerSerializer> implements ContextualSerializer +{ + private final JavaType _type; + private final TypeFactory _typeFactory; + private final BeanProperty _property; + private final JsonSerializer _rowSerializer; + private final JsonSerializer _columnSerializer; + private final TypeSerializer _valueTypeSerializer; + private final JsonSerializer _valueSerializer; + + private final MapSerializer _rowMapSerializer; + private final JsonSerializer _columnAndValueSerializer; + + public TableSerializer( final SerializationConfig config, final JavaType type ) + { + super(type.getRawClass(), false); + this._type = type; + this._typeFactory = config.getTypeFactory(); + this._property = null; + this._rowSerializer = null; + this._columnSerializer = null; + this._valueTypeSerializer = null; + this._valueSerializer = null; + + this._rowMapSerializer = null; + this._columnAndValueSerializer = null; + } + + @SuppressWarnings( "unchecked" ) + protected TableSerializer( final TableSerializer src, + final BeanProperty property, + final JsonSerializer rowKeySerializer, + final JsonSerializer columnKeySerializer, + final TypeSerializer valueTypeSerializer, + final JsonSerializer valueSerializer ) + { + super(src); + this._type = src._type; + this._typeFactory = src._typeFactory; + this._property = property; + this._rowSerializer = (JsonSerializer) rowKeySerializer; + this._columnSerializer = (JsonSerializer) columnKeySerializer; + this._valueTypeSerializer = valueTypeSerializer; + this._valueSerializer = (JsonSerializer) valueSerializer; + + final MapType columnAndValueType = this._typeFactory.constructMapType(Map.class, this._type.containedType(1), this._type.containedType(2)); + this._columnAndValueSerializer = + MapSerializer.construct(null, + columnAndValueType, + false, + this._valueTypeSerializer, + this._columnSerializer, + this._valueSerializer, + null); + + final MapType rowMapType = this._typeFactory.constructMapType(Map.class, this._type.containedType(0), columnAndValueType); + this._rowMapSerializer = + MapSerializer.construct(null, + rowMapType, + false, + this._valueTypeSerializer, + this._rowSerializer, + (JsonSerializer) this._columnAndValueSerializer, + null); + + } + + protected TableSerializer withResolved( final BeanProperty property, + final JsonSerializer rowKeySer, + final JsonSerializer columnKeySer, + final TypeSerializer vts, + final JsonSerializer valueSer ) + { + return new TableSerializer(this, property, rowKeySer, columnKeySer, vts, valueSer); + } + + @Override + protected ContainerSerializer _withValueTypeSerializer( final TypeSerializer typeSer ) + { + return new TableSerializer(this, this._property, this._rowSerializer, this._columnSerializer, typeSer, this._valueSerializer); + } + + @Override + public JsonSerializer createContextual( final SerializerProvider provider, final BeanProperty property ) throws JsonMappingException + { + JsonSerializer valueSer = this._valueSerializer; + if (valueSer == null) { // if type is final, can actually resolve: + final JavaType valueType = this._type.containedType(2); + if (valueType.isFinal()) { + valueSer = provider.findValueSerializer(valueType, property); + } + } + else if (valueSer instanceof ContextualSerializer) { + valueSer = ((ContextualSerializer) valueSer).createContextual(provider, property); + } + JsonSerializer rowKeySer = this._rowSerializer; + if (rowKeySer == null) { + rowKeySer = provider.findKeySerializer(this._type.containedType(0), property); + } + else if (rowKeySer instanceof ContextualSerializer) { + rowKeySer = ((ContextualSerializer) rowKeySer).createContextual(provider, property); + } + JsonSerializer columnKeySer = this._columnSerializer; + if (columnKeySer == null) { + columnKeySer = provider.findKeySerializer(this._type.containedType(1), property); + } + else if (columnKeySer instanceof ContextualSerializer) { + columnKeySer = ((ContextualSerializer) columnKeySer).createContextual(provider, property); + } + // finally, TypeSerializers may need contextualization as well + TypeSerializer typeSer = this._valueTypeSerializer; + if (typeSer != null) { + typeSer = typeSer.forProperty(property); + } + return this.withResolved(property, rowKeySer, columnKeySer, typeSer, valueSer); + } + + @Override + public JavaType getContentType() + { + return this._type.getContentType(); + } + + @Override + public JsonSerializer getContentSerializer() + { + return this._valueSerializer; + } + + @Override + public boolean isEmpty( final Table table ) + { + return table.isEmpty(); + } + + @Override + public boolean hasSingleElement( final Table table ) + { + return table.size() == 1; + } + + @Override + public void serialize( final Table value, final JsonGenerator jgen, final SerializerProvider provider ) + throws IOException, JsonGenerationException + { + jgen.writeStartObject(); + if ( !value.isEmpty()) { + this.serializeFields(value, jgen, provider); + } + jgen.writeEndObject(); + } + + @Override + public void serializeWithType( final Table value, + final JsonGenerator jgen, + final SerializerProvider provider, + final TypeSerializer typeSer ) throws IOException, JsonGenerationException + { + typeSer.writeTypePrefixForObject(value, jgen); + this.serializeFields(value, jgen, provider); + typeSer.writeTypeSuffixForObject(value, jgen); + } + + private final void serializeFields( final Table table, final JsonGenerator jgen, final SerializerProvider provider ) + throws IOException, JsonProcessingException + { + this._rowMapSerializer.serializeFields(table.rowMap(), jgen, provider); + } +} diff --git a/src/test/java/com/fasterxml/jackson/datatype/guava/ImmutableTableSerializationTest.java b/src/test/java/com/fasterxml/jackson/datatype/guava/ImmutableTableSerializationTest.java new file mode 100644 index 0000000..231ebfc --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/datatype/guava/ImmutableTableSerializationTest.java @@ -0,0 +1,172 @@ +package com.fasterxml.jackson.datatype.guava; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; + +import org.junit.Before; +import org.junit.Test; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.google.common.collect.ImmutableTable; + +public class ImmutableTableSerializationTest +{ + final ObjectMapper mapper = new ObjectMapper(); + + @Before + public void setup() + { + this.mapper.registerModule(new GuavaModule()); + this.mapper.registerModule(new ComplexKeyModule()); + } + + @Test + public void testSimpleKeyImmutableTableSerde() throws IOException + { + final ImmutableTable.Builder builder = ImmutableTable.builder(); + builder.put(Integer.valueOf(42), "column42", "some value 42"); + builder.put(Integer.valueOf(45), "column45", "some value 45"); + final ImmutableTable simpleTable = builder.build(); + + final String simpleJson = this.mapper.writeValueAsString(simpleTable); + assertEquals("{\"42\":{\"column42\":\"some value 42\"},\"45\":{\"column45\":\"some value 45\"}}", simpleJson); + + final ImmutableTable reconstitutedTable = + this.mapper.readValue(simpleJson, new TypeReference>() {}); + assertEquals(simpleTable, reconstitutedTable); + } + + /** + * This test illustrates one way to use objects as keys in Tables. + * + * @throws IOException + */ + @Test + public void testComplexKeyImmutableTableSerde() throws IOException + { + final ImmutableTable.Builder ckBuilder = ImmutableTable.builder(); + ckBuilder.put(Integer.valueOf(42), new ComplexKey("field1", "field2"), "some value 42"); + ckBuilder.put(Integer.valueOf(45), new ComplexKey("field1", "field2"), "some value 45"); + final ImmutableTable complexKeyTable = ckBuilder.build(); + + final TypeReference> tableType = new TypeReference>() + {}; + + final String ckJson = this.mapper.writerFor(tableType).writeValueAsString(complexKeyTable); + assertEquals("{\"42\":{\"field1:field2\":\"some value 42\"},\"45\":{\"field1:field2\":\"some value 45\"}}", ckJson); + + final ImmutableTable reconstitutedTable = this.mapper.readValue(ckJson, tableType); + assertEquals(complexKeyTable, reconstitutedTable); + } + + static class ComplexKeyModule extends SimpleModule + { + private static final long serialVersionUID = 1L; + + public ComplexKeyModule() + { + this.addKeySerializer(ComplexKey.class, new JsonSerializer() { + @Override + public void serialize( final ComplexKey value, final JsonGenerator jgen, final SerializerProvider provider ) + throws IOException, JsonProcessingException + { + jgen.writeFieldName(value.getKey1() + ":" + value.getKey2()); + } + }); + + this.addKeyDeserializer(ComplexKey.class, new KeyDeserializer() { + @Override + public Object deserializeKey( final String key, final DeserializationContext ctxt ) throws IOException, JsonProcessingException + { + final String[] split = key.split(":"); + return new ComplexKey(split[0], split[1]); + } + }); + } + } + + static class ComplexKey + { + private String key1; + private String key2; + + public ComplexKey( final String key1, final String key2 ) + { + super(); + this.key1 = key1; + this.key2 = key2; + } + + public String getKey1() + { + return this.key1; + } + + public void setKey1( final String key1 ) + { + this.key1 = key1; + } + + public String getKey2() + { + return this.key2; + } + + public void setKey2( final String key2 ) + { + this.key2 = key2; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((this.key1 == null) ? 0 : this.key1.hashCode()); + result = prime * result + ((this.key2 == null) ? 0 : this.key2.hashCode()); + return result; + } + + @Override + public boolean equals( final Object obj ) + { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if ( !(obj instanceof ComplexKey)) { + return false; + } + final ComplexKey other = (ComplexKey) obj; + if (this.key1 == null) { + if (other.key1 != null) { + return false; + } + } + else if ( !this.key1.equals(other.key1)) { + return false; + } + if (this.key2 == null) { + if (other.key2 != null) { + return false; + } + } + else if ( !this.key2.equals(other.key2)) { + return false; + } + return true; + } + + } +}