From 795f2621064727de41e5ecd38be2122097acbdf4 Mon Sep 17 00:00:00 2001 From: jwilson Date: Mon, 23 Mar 2015 23:15:03 -0400 Subject: [PATCH] Map adapter. Limited to string keys for now. --- .../com/squareup/moshi/ArrayJsonAdapter.java | 2 +- .../java/com/squareup/moshi/ClassFactory.java | 6 +- ...lassAdapter.java => ClassJsonAdapter.java} | 6 +- .../squareup/moshi/CollectionJsonAdapter.java | 4 +- .../com/squareup/moshi/MapJsonAdapter.java | 69 ++++++++++ ...terTest.java => ClassJsonAdapterTest.java} | 28 ++-- .../squareup/moshi/MapJsonAdapterTest.java | 129 ++++++++++++++++++ 7 files changed, 221 insertions(+), 23 deletions(-) rename moshi/src/main/java/com/squareup/moshi/{ClassAdapter.java => ClassJsonAdapter.java} (96%) create mode 100644 moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.java rename moshi/src/test/java/com/squareup/moshi/{ClassAdapterTest.java => ClassJsonAdapterTest.java} (92%) create mode 100644 moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java diff --git a/moshi/src/main/java/com/squareup/moshi/ArrayJsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/ArrayJsonAdapter.java index 7c2b933..a05da0b 100644 --- a/moshi/src/main/java/com/squareup/moshi/ArrayJsonAdapter.java +++ b/moshi/src/main/java/com/squareup/moshi/ArrayJsonAdapter.java @@ -46,7 +46,7 @@ class ArrayJsonAdapter extends JsonAdapter { } @Override public Object fromJson(JsonReader reader) throws IOException { - List list = new ArrayList(); + List list = new ArrayList<>(); reader.beginArray(); while (reader.hasNext()) { list.add(elementAdapter.fromJson(reader)); diff --git a/moshi/src/main/java/com/squareup/moshi/ClassFactory.java b/moshi/src/main/java/com/squareup/moshi/ClassFactory.java index 3f55581..685e0c1 100644 --- a/moshi/src/main/java/com/squareup/moshi/ClassFactory.java +++ b/moshi/src/main/java/com/squareup/moshi/ClassFactory.java @@ -46,7 +46,7 @@ abstract class ClassFactory { return (T) constructor.newInstance(args); } }; - } catch (NoSuchMethodException noNoArgsConstructor) { + } catch (NoSuchMethodException ignored) { // No no-args constructor. Fall back to something more magical... } @@ -68,7 +68,7 @@ abstract class ClassFactory { }; } catch (IllegalAccessException e) { throw new AssertionError(); - } catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException notJvm) { + } catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException ignored) { // Not the expected version of the Oracle Java library! } @@ -95,7 +95,7 @@ abstract class ClassFactory { throw new AssertionError(); } catch (InvocationTargetException e) { throw new RuntimeException(e); - } catch (NoSuchMethodException notLibcore) { + } catch (NoSuchMethodException ignored) { // Not the expected version of Dalvik/libcore! } diff --git a/moshi/src/main/java/com/squareup/moshi/ClassAdapter.java b/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java similarity index 96% rename from moshi/src/main/java/com/squareup/moshi/ClassAdapter.java rename to moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java index 3293674..90eb092 100644 --- a/moshi/src/main/java/com/squareup/moshi/ClassAdapter.java +++ b/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java @@ -29,7 +29,7 @@ import java.util.TreeMap; * of classes in {@code java.*}, {@code javax.*} and {@code android.*} are omitted from both * serialization and deserialization unless they are either public or protected. */ -final class ClassAdapter extends JsonAdapter { +final class ClassJsonAdapter extends JsonAdapter { public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() { @Override public JsonAdapter create(Type type, AnnotatedElement annotations, Moshi moshi) { Class rawType = Types.getRawType(type); @@ -54,7 +54,7 @@ final class ClassAdapter extends JsonAdapter { for (Type t = type; t != Object.class; t = Types.getGenericSuperclass(t)) { createFieldBindings(moshi, t, fields); } - return new ClassAdapter<>(classFactory, fields).nullSafe(); + return new ClassJsonAdapter<>(classFactory, fields).nullSafe(); } /** Creates a field binding for each of declared field of {@code type}. */ @@ -103,7 +103,7 @@ final class ClassAdapter extends JsonAdapter { private final ClassFactory classFactory; private final Map> jsonFields; - private ClassAdapter(ClassFactory classFactory, Map> jsonFields) { + private ClassJsonAdapter(ClassFactory classFactory, Map> jsonFields) { this.classFactory = classFactory; this.jsonFields = jsonFields; } diff --git a/moshi/src/main/java/com/squareup/moshi/CollectionJsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/CollectionJsonAdapter.java index 25acb3a..f3df24d 100644 --- a/moshi/src/main/java/com/squareup/moshi/CollectionJsonAdapter.java +++ b/moshi/src/main/java/com/squareup/moshi/CollectionJsonAdapter.java @@ -49,7 +49,7 @@ abstract class CollectionJsonAdapter, T> extends JsonAda JsonAdapter elementAdapter = moshi.adapter(elementType); return new CollectionJsonAdapter, T>(elementAdapter) { @Override Collection newCollection() { - return new ArrayList(); + return new ArrayList<>(); } }; } @@ -59,7 +59,7 @@ abstract class CollectionJsonAdapter, T> extends JsonAda JsonAdapter elementAdapter = moshi.adapter(elementType); return new CollectionJsonAdapter, T>(elementAdapter) { @Override Set newCollection() { - return new LinkedHashSet(); + return new LinkedHashSet<>(); } }; } diff --git a/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.java new file mode 100644 index 0000000..e497236 --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * 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 com.squareup.moshi; + +import java.io.IOException; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Type; +import java.util.Map; + +/** + * Converts maps with string keys to JSON objects. + * + * TODO: support maps with other key types and convert to/from strings. + */ +final class MapJsonAdapter extends JsonAdapter> { + public static final Factory FACTORY = new Factory() { + @Override public JsonAdapter create(Type type, AnnotatedElement annotations, Moshi moshi) { + Class rawType = Types.getRawType(type); + if (rawType != Map.class) return null; + Type[] keyAndValue = Types.mapKeyAndValueTypes(type, rawType); + if (keyAndValue[0] != String.class) return null; + return new MapJsonAdapter<>(moshi, keyAndValue[1]).nullSafe(); + }; + }; + + private final JsonAdapter valueAdapter; + + public MapJsonAdapter(Moshi moshi, Type valueType) { + this.valueAdapter = moshi.adapter(valueType); + } + + @Override public void toJson(JsonWriter writer, Map map) throws IOException { + writer.beginObject(); + for (Map.Entry entry : map.entrySet()) { + writer.name((String) entry.getKey()); + valueAdapter.toJson(writer, entry.getValue()); + } + writer.endObject(); + } + + @Override public Map fromJson(JsonReader reader) throws IOException { + LinkedHashTreeMap result = new LinkedHashTreeMap<>(); + reader.beginObject(); + while (reader.hasNext()) { + @SuppressWarnings("unchecked") // Currently 'K' is always 'String'. + K name = (K) reader.nextName(); + V value = valueAdapter.fromJson(reader); + V replaced = result.put(name, value); + if (replaced != null) { + throw new IllegalArgumentException("object property '" + name + "' has multiple values"); + } + } + reader.endObject(); + return result; + } +} diff --git a/moshi/src/test/java/com/squareup/moshi/ClassAdapterTest.java b/moshi/src/test/java/com/squareup/moshi/ClassJsonAdapterTest.java similarity index 92% rename from moshi/src/test/java/com/squareup/moshi/ClassAdapterTest.java rename to moshi/src/test/java/com/squareup/moshi/ClassJsonAdapterTest.java index f929a03..6e2568f 100644 --- a/moshi/src/test/java/com/squareup/moshi/ClassAdapterTest.java +++ b/moshi/src/test/java/com/squareup/moshi/ClassJsonAdapterTest.java @@ -28,7 +28,7 @@ import static com.squareup.moshi.Util.NO_ANNOTATIONS; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; -public final class ClassAdapterTest { +public final class ClassJsonAdapterTest { private final Moshi moshi = new Moshi.Builder().build(); static class BasicPizza { @@ -156,12 +156,12 @@ public final class ClassAdapterTest { @Test public void fieldNameCollision() throws Exception { try { - ClassAdapter.FACTORY.create(ExtendsBaseA.class, NO_ANNOTATIONS, moshi); + ClassJsonAdapter.FACTORY.create(ExtendsBaseA.class, NO_ANNOTATIONS, moshi); fail(); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessage("field name collision: 'a' declared by both " - + "com.squareup.moshi.ClassAdapterTest$ExtendsBaseA and " - + "superclass com.squareup.moshi.ClassAdapterTest$BaseA"); + + "com.squareup.moshi.ClassJsonAdapterTest$ExtendsBaseA and " + + "superclass com.squareup.moshi.ClassJsonAdapterTest$BaseA"); } } @@ -302,17 +302,17 @@ public final class ClassAdapterTest { @Test public void nonStaticNestedClassNotSupported() throws Exception { try { - ClassAdapter.FACTORY.create(NonStatic.class, NO_ANNOTATIONS, moshi); + ClassJsonAdapter.FACTORY.create(NonStatic.class, NO_ANNOTATIONS, moshi); fail(); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessage("cannot serialize non-static nested class " - + "com.squareup.moshi.ClassAdapterTest$NonStatic"); + + "com.squareup.moshi.ClassJsonAdapterTest$NonStatic"); } } @Test public void platformClassNotSupported() throws Exception { - assertThat(ClassAdapter.FACTORY.create(UUID.class, NO_ANNOTATIONS, moshi)).isNull(); - assertThat(ClassAdapter.FACTORY.create(KeyGenerator.class, NO_ANNOTATIONS, moshi)).isNull(); + assertThat(ClassJsonAdapter.FACTORY.create(UUID.class, NO_ANNOTATIONS, moshi)).isNull(); + assertThat(ClassJsonAdapter.FACTORY.create(KeyGenerator.class, NO_ANNOTATIONS, moshi)).isNull(); } @Test public void anonymousClassNotSupported() throws Exception { @@ -322,7 +322,7 @@ public final class ClassAdapterTest { } }; try { - ClassAdapter.FACTORY.create(c.getClass(), NO_ANNOTATIONS, moshi); + ClassJsonAdapter.FACTORY.create(c.getClass(), NO_ANNOTATIONS, moshi); fail(); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessage("cannot serialize anonymous class " + c.getClass().getName()); @@ -330,7 +330,7 @@ public final class ClassAdapterTest { } @Test public void interfaceNotSupported() throws Exception { - assertThat(ClassAdapter.FACTORY.create(Runnable.class, NO_ANNOTATIONS, moshi)).isNull(); + assertThat(ClassJsonAdapter.FACTORY.create(Runnable.class, NO_ANNOTATIONS, moshi)).isNull(); } static abstract class Abstract { @@ -338,11 +338,11 @@ public final class ClassAdapterTest { @Test public void abstractClassNotSupported() throws Exception { try { - ClassAdapter.FACTORY.create(Abstract.class, NO_ANNOTATIONS, moshi); + ClassJsonAdapter.FACTORY.create(Abstract.class, NO_ANNOTATIONS, moshi); fail(); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessage("cannot serialize abstract class " - + "com.squareup.moshi.ClassAdapterTest$Abstract"); + + "com.squareup.moshi.ClassJsonAdapterTest$Abstract"); } } @@ -390,7 +390,7 @@ public final class ClassAdapterTest { private String toJson(Class type, T value) throws IOException { @SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument. - JsonAdapter jsonAdapter = (JsonAdapter) ClassAdapter.FACTORY.create( + JsonAdapter jsonAdapter = (JsonAdapter) ClassJsonAdapter.FACTORY.create( type, NO_ANNOTATIONS, moshi); // Wrap in an array to avoid top-level object warnings without going completely lenient. @@ -408,7 +408,7 @@ public final class ClassAdapterTest { private T fromJson(Class type, String json) throws IOException { @SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument. - JsonAdapter jsonAdapter = (JsonAdapter) ClassAdapter.FACTORY.create( + JsonAdapter jsonAdapter = (JsonAdapter) ClassJsonAdapter.FACTORY.create( type, NO_ANNOTATIONS, moshi); // Wrap in an array to avoid top-level object warnings without going completely lenient. JsonReader jsonReader = new JsonReader("[" + json + "]"); diff --git a/moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java b/moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java new file mode 100644 index 0000000..40f6f3c --- /dev/null +++ b/moshi/src/test/java/com/squareup/moshi/MapJsonAdapterTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * 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 com.squareup.moshi; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import okio.Buffer; +import org.assertj.core.data.MapEntry; +import org.junit.Test; + +import static com.squareup.moshi.Util.NO_ANNOTATIONS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +public final class MapJsonAdapterTest { + private final Moshi moshi = new Moshi.Builder().build(); + + @Test public void map() throws Exception { + Map map = new LinkedHashMap<>(); + map.put("a", true); + map.put("b", false); + map.put("c", null); + + String toJson = toJson(String.class, Boolean.class, map); + assertThat(toJson).isEqualTo("{\"a\":true,\"b\":false,\"c\":null}"); + + Map fromJson = fromJson( + String.class, Boolean.class, "{\"a\":true,\"b\":false,\"c\":null}"); + assertThat(fromJson).containsExactly( + MapEntry.entry("a", true), MapEntry.entry("b", false), MapEntry.entry("c", null)); + } + + @Test public void mapWithNullKeyFailsToEmit() throws Exception { + Map map = new LinkedHashMap<>(); + map.put(null, true); + + try { + toJson(String.class, Boolean.class, map); + fail(); + } catch (NullPointerException expected) { + } + } + + @Test public void emptyMap() throws Exception { + Map map = new LinkedHashMap<>(); + + String toJson = toJson(String.class, Boolean.class, map); + assertThat(toJson).isEqualTo("{}"); + + Map fromJson = fromJson(String.class, Boolean.class, "{}"); + assertThat(fromJson).isEmpty(); + } + + @Test public void nullMap() throws Exception { + JsonAdapter jsonAdapter = mapAdapter(String.class, Boolean.class); + + Buffer buffer = new Buffer(); + JsonWriter jsonWriter = new JsonWriter(buffer); + jsonWriter.setLenient(true); + jsonAdapter.toJson(jsonWriter, null); + assertThat(buffer.readUtf8()).isEqualTo("null"); + + JsonReader jsonReader = new JsonReader("null"); + jsonReader.setLenient(true); + assertThat(jsonAdapter.fromJson(jsonReader)).isEqualTo(null); + } + + @Test public void orderIsRetained() throws Exception { + Map map = new LinkedHashMap<>(); + map.put("c", 1); + map.put("a", 2); + map.put("d", 3); + map.put("b", 4); + + String toJson = toJson(String.class, Integer.class, map); + assertThat(toJson).isEqualTo("{\"c\":1,\"a\":2,\"d\":3,\"b\":4}"); + + Map fromJson = fromJson( + String.class, Integer.class, "{\"c\":1,\"a\":2,\"d\":3,\"b\":4}"); + assertThat(new ArrayList(fromJson.keySet())) + .isEqualTo(Arrays.asList("c", "a", "d", "b")); + } + + @Test public void duplicatesAreForbidden() throws Exception { + try { + fromJson(String.class, Integer.class, "{\"c\":1,\"c\":2}"); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessage("object property 'c' has multiple values"); + } + } + + private String toJson(Type keyType, Type valueType, Map value) throws IOException { + JsonAdapter> jsonAdapter = mapAdapter(keyType, valueType); + Buffer buffer = new Buffer(); + JsonWriter jsonWriter = new JsonWriter(buffer); + jsonWriter.setSerializeNulls(true); + jsonAdapter.toJson(jsonWriter, value); + return buffer.readUtf8(); + } + + @SuppressWarnings("unchecked") // It's the caller's responsibility to make sure K and V match. + private JsonAdapter> mapAdapter(Type keyType, Type valueType) { + return (JsonAdapter>) MapJsonAdapter.FACTORY.create( + Types.newParameterizedType(Map.class, keyType, valueType), NO_ANNOTATIONS, moshi); + } + + private Map fromJson(Type keyType, Type valueType, String json) throws IOException { + JsonAdapter> mapJsonAdapter = mapAdapter(keyType, valueType); + return mapJsonAdapter.fromJson(json); + } +}