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 b5da614..4090ad1 100644 --- a/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaDeserializers.java +++ b/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaDeserializers.java @@ -1,5 +1,9 @@ package com.fasterxml.jackson.datatype.guava; +import com.fasterxml.jackson.databind.deser.std.StdDelegatingDeserializer; +import com.fasterxml.jackson.datatype.guava.deser.table.MapToArrayTableConverter; +import com.fasterxml.jackson.datatype.guava.deser.table.MapToHashBasedTableConverter; +import com.fasterxml.jackson.datatype.guava.deser.table.MapToImmutableTableConverter; import com.google.common.base.Optional; import com.google.common.collect.*; import com.google.common.hash.HashCode; @@ -262,6 +266,18 @@ public JsonDeserializer findBeanDeserializer(final JavaType type, Deserializa if (raw == HashCode.class) { return HashCodeDeserializer.std; } + if (raw == Table.class || raw == ImmutableTable.class) { + return new StdDelegatingDeserializer>( + new MapToImmutableTableConverter(type)); + } + if (raw == HashBasedTable.class) { + return new StdDelegatingDeserializer>( + new MapToHashBasedTableConverter(type)); + } + if (raw == ArrayTable.class) { + return new StdDelegatingDeserializer>( + new MapToArrayTableConverter(type)); + } 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 9c15a4f..17bcf1e 100644 --- a/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaSerializers.java +++ b/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaSerializers.java @@ -6,12 +6,14 @@ import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.databind.util.ArrayBuilders; import com.fasterxml.jackson.databind.util.StdConverter; +import com.fasterxml.jackson.datatype.guava.ser.TableToMapConverter; import com.google.common.base.Optional; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilderSpec; 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; @@ -73,6 +75,9 @@ public JsonSerializer findSerializer(SerializationConfig config, JavaType typ // JavaType delegate = config.getTypeFactory().constructParametrizedType(FluentIterable.class, Iterable.class, vt); return new StdDelegatingSerializer(FluentConverter.instance, delegate, null); } + if (Table.class.isAssignableFrom(raw)) { + return new StdDelegatingSerializer(new TableToMapConverter(type)); + } return super.findSerializer(config, type, beanDesc); } diff --git a/src/main/java/com/fasterxml/jackson/datatype/guava/deser/table/MapToArrayTableConverter.java b/src/main/java/com/fasterxml/jackson/datatype/guava/deser/table/MapToArrayTableConverter.java new file mode 100644 index 0000000..a9cff37 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/datatype/guava/deser/table/MapToArrayTableConverter.java @@ -0,0 +1,56 @@ +package com.fasterxml.jackson.datatype.guava.deser.table; + +import com.fasterxml.jackson.databind.JavaType; +import com.google.common.collect.ArrayTable; +import com.google.common.collect.Table; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * Converts {@link Map} instances into {@link ArrayTable} instances during JSON + * deserialization. + * + * @param the type of the table row keys + * @param the type of the table column keys + * @param the type of the mapped values + * + * @author Michael Hixson + */ +public final class MapToArrayTableConverter + extends MapToTableConverter +{ + /** + * Constructs a new converter with the provided table type. + * + * @param tableType the type of the table being deserialized + */ + public MapToArrayTableConverter(JavaType tableType) + { + super(tableType); + } + + @Override + public Table convert(Map> map) + { + Set rowKeys = map.keySet(); + Set columnKeys = new LinkedHashSet(); + for (Map columnMap : map.values()) + { + columnKeys.addAll(columnMap.keySet()); + } + Table table = ArrayTable.create(rowKeys, columnKeys); + for (Map.Entry> rowEntry : map.entrySet()) + { + for (Map.Entry columnEntry : rowEntry.getValue().entrySet()) + { + R rowKey = rowEntry.getKey(); + C columnKey = columnEntry.getKey(); + V value = columnEntry.getValue(); + table.put(rowKey, columnKey, value); + } + } + return table; + } +} diff --git a/src/main/java/com/fasterxml/jackson/datatype/guava/deser/table/MapToHashBasedTableConverter.java b/src/main/java/com/fasterxml/jackson/datatype/guava/deser/table/MapToHashBasedTableConverter.java new file mode 100644 index 0000000..549698e --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/datatype/guava/deser/table/MapToHashBasedTableConverter.java @@ -0,0 +1,48 @@ +package com.fasterxml.jackson.datatype.guava.deser.table; + +import com.fasterxml.jackson.databind.JavaType; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; + +import java.util.Map; + +/** + * Converts {@link Map} instances into {@link HashBasedTable} instances during + * JSON deserialization. + * + * @param the type of the table row keys + * @param the type of the table column keys + * @param the type of the mapped values + * + * @author Michael Hixson + */ +public final class MapToHashBasedTableConverter + extends MapToTableConverter +{ + /** + * Constructs a new converter with the provided table type. + * + * @param tableType the type of the table being deserialized + */ + public MapToHashBasedTableConverter(JavaType tableType) + { + super(tableType); + } + + @Override + public Table convert(Map> map) + { + Table table = HashBasedTable.create(); + for (Map.Entry> rowEntry : map.entrySet()) + { + for (Map.Entry columnEntry : rowEntry.getValue().entrySet()) + { + R rowKey = rowEntry.getKey(); + C columnKey = columnEntry.getKey(); + V value = columnEntry.getValue(); + table.put(rowKey, columnKey, value); + } + } + return table; + } +} diff --git a/src/main/java/com/fasterxml/jackson/datatype/guava/deser/table/MapToImmutableTableConverter.java b/src/main/java/com/fasterxml/jackson/datatype/guava/deser/table/MapToImmutableTableConverter.java new file mode 100644 index 0000000..285f0e7 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/datatype/guava/deser/table/MapToImmutableTableConverter.java @@ -0,0 +1,48 @@ +package com.fasterxml.jackson.datatype.guava.deser.table; + +import com.fasterxml.jackson.databind.JavaType; +import com.google.common.collect.ImmutableTable; +import com.google.common.collect.Table; + +import java.util.Map; + +/** + * Converts {@link Map} instances into {@link ImmutableTable} instances during + * JSON deserialization. + * + * @param the type of the table row keys + * @param the type of the table column keys + * @param the type of the mapped values + * + * @author Michael Hixson + */ +public final class MapToImmutableTableConverter + extends MapToTableConverter +{ + /** + * Constructs a new converter with the provided table type. + * + * @param tableType the type of the table being deserialized + */ + public MapToImmutableTableConverter(JavaType tableType) + { + super(tableType); + } + + @Override + public Table convert(Map> map) + { + ImmutableTable.Builder table = ImmutableTable.builder(); + for (Map.Entry> rowEntry : map.entrySet()) + { + for (Map.Entry columnEntry : rowEntry.getValue().entrySet()) + { + R rowKey = rowEntry.getKey(); + C columnKey = columnEntry.getKey(); + V value = columnEntry.getValue(); + table.put(rowKey, columnKey, value); + } + } + return table.build(); + } +} diff --git a/src/main/java/com/fasterxml/jackson/datatype/guava/deser/table/MapToTableConverter.java b/src/main/java/com/fasterxml/jackson/datatype/guava/deser/table/MapToTableConverter.java new file mode 100644 index 0000000..de5936a --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/datatype/guava/deser/table/MapToTableConverter.java @@ -0,0 +1,63 @@ +package com.fasterxml.jackson.datatype.guava.deser.table; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.databind.util.Converter; +import com.google.common.collect.Table; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * An abstract base class for converting {@link Map} instances into + * {@link Table} instances during JSON deserialization. This class assumes that + * the maps mirror the structure of {@link Table#rowMap()}. The maps are + * interpreted as {@link LinkedHashMap} to preserve cell ordering, in case the + * table implementation also preserves cell ordering during insertion. + * + * @param the type of the table row keys + * @param the type of the table column keys + * @param the type of the mapped values + * + * @author Michael Hixson + */ +abstract class MapToTableConverter + implements Converter>, Table> +{ + private final JavaType tableType; + + /** + * Constructs a new converter with the provided table type. + * + * @param tableType the type of the table being deserialized + */ + MapToTableConverter(JavaType tableType) + { + if (tableType == null) + { + throw new NullPointerException("tableType must not be null"); + } + this.tableType = tableType; + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) + { + JavaType rowKeyType = tableType.containedTypeOrUnknown(0); + JavaType columnKeyType = tableType.containedTypeOrUnknown(1); + JavaType valueType = tableType.containedTypeOrUnknown(2); + return typeFactory.constructMapType( + LinkedHashMap.class, + rowKeyType, + typeFactory.constructMapType( + LinkedHashMap.class, + columnKeyType, + valueType)); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) + { + return tableType; + } +} diff --git a/src/main/java/com/fasterxml/jackson/datatype/guava/ser/TableToMapConverter.java b/src/main/java/com/fasterxml/jackson/datatype/guava/ser/TableToMapConverter.java new file mode 100644 index 0000000..5280f82 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/datatype/guava/ser/TableToMapConverter.java @@ -0,0 +1,54 @@ +package com.fasterxml.jackson.datatype.guava.ser; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.databind.util.Converter; +import com.google.common.collect.Table; + +import java.util.Map; + +/** + * Converts {@link Table} instances into {@link Map} instances during JSON + * serialization, using {@link Table#rowMap()}. + * + * @param the type of the table row keys + * @param the type of the table column keys + * @param the type of the mapped values + */ +public final class TableToMapConverter + implements Converter, Map>> +{ + private final JavaType tableType; + + /** + * Constructs a new converter with the provided table type. + * + * @param tableType the type of the table being serialized + */ + public TableToMapConverter(JavaType tableType) + { + if (tableType == null) + { + throw new NullPointerException("tableType must not be null"); + } + this.tableType = tableType; + } + + @Override + public Map> convert(Table table) + { + return table.rowMap(); + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) + { + return tableType; + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) + { + return typeFactory.constructRawMapType(Map.class); + } +} diff --git a/src/test/java/com/fasterxml/jackson/datatype/guava/TestTables.java b/src/test/java/com/fasterxml/jackson/datatype/guava/TestTables.java new file mode 100644 index 0000000..b9d23a5 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/datatype/guava/TestTables.java @@ -0,0 +1,220 @@ +package com.fasterxml.jackson.datatype.guava; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Supplier; +import com.google.common.collect.ArrayTable; +import com.google.common.collect.ForwardingTable; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableTable; +import com.google.common.collect.Iterables; +import com.google.common.collect.Table; +import com.google.common.collect.Tables; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Unit test to verify serialization and deserialization of {@link Table}. + * + * @author Michael Hixson + */ +public final class TestTables extends ModuleTestBase +{ + private final ObjectMapper MAPPER = mapperWithModule(); + + /** + * Tests that we can convert a {@link Table} instance (the interface, not one + * of the concrete implementations provided by Guava) to and from JSON. + * + *

Serialization must preserve cell ordering. + */ + public void testDefaultTables() throws Exception + { + Table original = Tables.newCustomTable( + new LinkedHashMap>(), + new Supplier>() + { + @Override + public Map get() + { + return new LinkedHashMap(); + } + }); + original.put(3, 3.0, "three"); + original.put(2, 2.0, "two"); + original.put(1, 1.0, "one"); + String serialized = MAPPER.writeValueAsString(original); + assertEquals( + "{\"3\":{\"3.0\":\"three\"}," + + "\"2\":{\"2.0\":\"two\"}," + + "\"1\":{\"1.0\":\"one\"}}", + serialized); + Table deserialized = MAPPER.readValue( + serialized, + new TypeReference>() {}); + assertTrue( + "Expected = " + original.cellSet() + + ", actual = " + deserialized.cellSet(), + Iterables.elementsEqual( + original.cellSet(), + deserialized.cellSet())); + } + + /** + * Tests that we can convert a {@link ImmutableTable} instance to and from + * JSON. + * + *

Serialization must preserve cell ordering. + */ + public void testImmutableTables() throws Exception + { + ImmutableTable original = ImmutableTable + .builder() + .put(3, 3.0, "three") + .put(2, 2.0, "two") + .put(1, 1.0, "one") + .build(); + String serialized = MAPPER.writeValueAsString(original); + assertEquals( + "{\"3\":{\"3.0\":\"three\"}," + + "\"2\":{\"2.0\":\"two\"}," + + "\"1\":{\"1.0\":\"one\"}}", + serialized); + ImmutableTable deserialized = MAPPER.readValue( + serialized, + new TypeReference>() {}); + assertTrue( + "Expected = " + original.cellSet() + + ", actual = " + deserialized.cellSet(), + Iterables.elementsEqual( + original.cellSet(), + deserialized.cellSet())); + } + + /** + * Tests that we can convert a {@link HashBasedTable} instance to and from + * JSON. + * + *

Serialization might not preserve cell ordering. We can't test for the + * exact serialized form of the table because {@link HashBasedTable} makes no + * guarantees on the iteration order of its cells. So unlike the other tests + * involving {@link Table}, we don't make assertions about the serialized JSON + * string or the ordering of cells in the tables. + */ + public void testHashBasedTables() throws Exception + { + HashBasedTable original = HashBasedTable.create(); + original.put(3, 3.0, "three"); + original.put(2, 2.0, "two"); + original.put(1, 1.0, "one"); + String serialized = MAPPER.writeValueAsString(original); + HashBasedTable deserialized = MAPPER.readValue( + serialized, + new TypeReference>() {}); + assertEquals(original, deserialized); + } + + /** + * Tests that we can convert a {@link ArrayTable} instance to and from JSON. + * + *

Serialization must preserve cell ordering. + * + *

We intentionally leave at least one row and column blank to ensure that + * the row and column sets are preserved in the JSON, which would not be the + * case if we omitted {@code null} values from the JSON. + */ + public void testArrayTables() throws Exception + { + ArrayTable original = ArrayTable.create( + ImmutableSet.of(3, 2, 1), + ImmutableSet.of(3.0, 2.0, 1.0)); + original.put(3, 3.0, "three"); + original.put(2, 2.0, "two"); + // Intentionally omit values for r=1, c=1.0 + String serialized = MAPPER.writeValueAsString(original); + assertEquals( + "{\"3\":{\"3.0\":\"three\",\"2.0\":null,\"1.0\":null}," + + "\"2\":{\"3.0\":null,\"2.0\":\"two\",\"1.0\":null}," + + "\"1\":{\"3.0\":null,\"2.0\":null,\"1.0\":null}}", + serialized); + ArrayTable deserialized = MAPPER.readValue( + serialized, + new TypeReference>() {}); + assertTrue( + "Expected = " + original.cellSet() + + ", actual = " + deserialized.cellSet(), + Iterables.elementsEqual( + original.cellSet(), + deserialized.cellSet())); + } + + /** + * Tests that tables lacking generic type information will interpret rows and + * column keys as strings during deserialization. A wrong implementation of + * the table deserializers might throw a null pointer exception due to missing + * generic type parameters. This test ensures that we treat missing type + * parameters as "unknown" rather than null. + * + *

Serialization must preserve cell ordering. + */ + // This unit test is specifically about how tables serialize and deserialize + // when using raw types. Of course we have unchecked casts and raw types. + @SuppressWarnings({"unchecked", "rawtypes"}) + public void testRawTypedTables() throws Exception + { + Table original = Tables.newCustomTable( + new LinkedHashMap>(), + new Supplier>() + { + @Override + public Map get() + { + return new LinkedHashMap(); + } + }); + original.put(3, 3.0, "three"); + original.put(2, 2.0, "two"); + original.put(1, 1.0, "one"); + String serialized = MAPPER.writeValueAsString(original); + assertEquals( + "{\"3\":{\"3.0\":\"three\"}," + + "\"2\":{\"2.0\":\"two\"}," + + "\"1\":{\"1.0\":\"one\"}}", + serialized); + Table deserialized = MAPPER.readValue( + serialized, + new TypeReference() {}); + assertFalse( + "Since we were using raw types, deserialization should have converted " + + "all the Integer row keys and Double column keys to strings", + original.equals(deserialized)); + ImmutableList> expected = + ImmutableList.of( + Tables.immutableCell("3", "3.0", "three"), + Tables.immutableCell("2", "2.0", "two"), + Tables.immutableCell("1", "1.0", "one")); + assertTrue( + "Expected = " + expected + ", actual = " + deserialized.cellSet(), + Iterables.elementsEqual(expected, deserialized.cellSet())); + } + + /** + * Tests that deserialization into a {@link Table} implementation that is not + * one of the supported types fails. + */ + public void testUnsupportedTables() throws Exception + { + try + { + MAPPER.readValue( + "{}", + new TypeReference>() {}); + fail("Deserialization of a non-supported table type should fail"); + } + catch (JsonMappingException ignored) {} + } +}