diff --git a/adapters/src/main/java/com/squareup/moshi/adapters/RuntimeJsonAdapterFactory.java b/adapters/src/main/java/com/squareup/moshi/adapters/RuntimeJsonAdapterFactory.java index 3a74f82..7ff47b1 100644 --- a/adapters/src/main/java/com/squareup/moshi/adapters/RuntimeJsonAdapterFactory.java +++ b/adapters/src/main/java/com/squareup/moshi/adapters/RuntimeJsonAdapterFactory.java @@ -24,7 +24,10 @@ import com.squareup.moshi.Types; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.CheckReturnValue; @@ -39,8 +42,8 @@ import javax.annotation.CheckReturnValue; * * Moshi moshi = new Moshi.Builder() * .add(RuntimeJsonAdapterFactory.of(Message.class, "type") - * .registerSubtype(Success.class, "success") - * .registerSubtype(Error.class, "error")) + * .withSubtype(Success.class, "success") + * .withSubtype(Error.class, "error")) * .build(); * } */ @@ -48,7 +51,16 @@ import javax.annotation.CheckReturnValue; final class RuntimeJsonAdapterFactory implements JsonAdapter.Factory { final Class baseType; final String labelKey; - final Map labelToType = new LinkedHashMap<>(); + final List labels; + final List subtypes; + + RuntimeJsonAdapterFactory( + Class baseType, String labelKey, List labels, List subtypes) { + this.baseType = baseType; + this.labelKey = labelKey; + this.labels = labels; + this.subtypes = subtypes; + } /** * @param baseType The base type for which this factory will create adapters. Cannot be Object. @@ -63,27 +75,26 @@ final class RuntimeJsonAdapterFactory implements JsonAdapter.Factory { throw new IllegalArgumentException( "The base type must not be Object. Consider using a marker interface."); } - return new RuntimeJsonAdapterFactory<>(baseType, labelKey); - } - - RuntimeJsonAdapterFactory(Class baseType, String labelKey) { - this.baseType = baseType; - this.labelKey = labelKey; + return new RuntimeJsonAdapterFactory<>( + baseType, labelKey, Collections.emptyList(), Collections.emptyList()); } /** - * Register the subtype that can be created based on the label. When an unknown type is found + * Returns a new factory that decodes instances of {@code subtype}. When an unknown type is found * during encoding an {@linkplain IllegalArgumentException} will be thrown. When an unknown label * is found during decoding a {@linkplain JsonDataException} will be thrown. */ - public RuntimeJsonAdapterFactory registerSubtype(Class subtype, String label) { + public RuntimeJsonAdapterFactory withSubtype(Class subtype, String label) { if (subtype == null) throw new NullPointerException("subtype == null"); if (label == null) throw new NullPointerException("label == null"); - if (labelToType.containsKey(label) || labelToType.containsValue(subtype)) { + if (labels.contains(label) || subtypes.contains(subtype)) { throw new IllegalArgumentException("Subtypes and labels must be unique."); } - labelToType.put(label, subtype); - return this; + List newLabels = new ArrayList<>(labels); + newLabels.add(label); + List newSubtypes = new ArrayList<>(subtypes); + newSubtypes.add(subtype); + return new RuntimeJsonAdapterFactory<>(baseType, labelKey, newLabels, newSubtypes); } @Override @@ -91,84 +102,90 @@ final class RuntimeJsonAdapterFactory implements JsonAdapter.Factory { if (Types.getRawType(type) != baseType || !annotations.isEmpty()) { return null; } - int size = labelToType.size(); - Map> labelToAdapter = new LinkedHashMap<>(size); - Map typeToLabel = new LinkedHashMap<>(size); - for (Map.Entry entry : labelToType.entrySet()) { - String label = entry.getKey(); - Type typeValue = entry.getValue(); - typeToLabel.put(typeValue, label); - labelToAdapter.put(label, moshi.adapter(typeValue)); + + List> jsonAdapters = new ArrayList<>(); + for (int i = 0, size = subtypes.size(); i < size; i++) { + jsonAdapters.add(moshi.adapter(subtypes.get(i))); } + JsonAdapter objectJsonAdapter = moshi.adapter(Object.class); - return new RuntimeJsonAdapter(labelKey, labelToAdapter, typeToLabel, - objectJsonAdapter).nullSafe(); + return new RuntimeJsonAdapter( + labelKey, labels, subtypes, jsonAdapters, objectJsonAdapter).nullSafe(); } static final class RuntimeJsonAdapter extends JsonAdapter { final String labelKey; - final Map> labelToAdapter; - final Map typeToLabel; + final List labels; + final List subtypes; + final List> jsonAdapters; final JsonAdapter objectJsonAdapter; - RuntimeJsonAdapter(String labelKey, Map> labelToAdapter, - Map typeToLabel, JsonAdapter objectJsonAdapter) { + /** Single-element options containing the label's key only. */ + final JsonReader.Options labelKeyOptions; + /** Corresponds to subtypes. */ + final JsonReader.Options labelOptions; + + RuntimeJsonAdapter(String labelKey, List labels, + List subtypes, List> jsonAdapters, + JsonAdapter objectJsonAdapter) { this.labelKey = labelKey; - this.labelToAdapter = labelToAdapter; - this.typeToLabel = typeToLabel; + this.labels = labels; + this.subtypes = subtypes; + this.jsonAdapters = jsonAdapters; this.objectJsonAdapter = objectJsonAdapter; + + this.labelKeyOptions = JsonReader.Options.of(labelKey); + this.labelOptions = JsonReader.Options.of(labels.toArray(new String[0])); } @Override public Object fromJson(JsonReader reader) throws IOException { - JsonReader.Token peekedToken = reader.peek(); - if (peekedToken != JsonReader.Token.BEGIN_OBJECT) { - throw new JsonDataException("Expected BEGIN_OBJECT but was " + peekedToken - + " at path " + reader.getPath()); + int labelIndex = labelIndex(reader.peekJson()); + return jsonAdapters.get(labelIndex).fromJson(reader); + } + + private int labelIndex(JsonReader reader) throws IOException { + reader.beginObject(); + while (reader.hasNext()) { + if (reader.selectName(labelKeyOptions) == -1) { + reader.skipName(); + reader.skipValue(); + continue; + } + + int labelIndex = reader.selectString(labelOptions); + if (labelIndex == -1) { + throw new JsonDataException("Expected one of " + + labels + + " for key '" + + labelKey + + "' but found '" + + reader.nextString() + + "'. Register a subtype for this label."); + } + reader.close(); + return labelIndex; } - Object jsonValue = reader.readJsonValue(); - Map jsonObject = (Map) jsonValue; - Object label = jsonObject.get(labelKey); - if (label == null) { - throw new JsonDataException("Missing label for " + labelKey); - } - if (!(label instanceof String)) { - throw new JsonDataException("Label for '" - + labelKey - + "' must be a string but was " - + label - + ", a " - + label.getClass()); - } - JsonAdapter adapter = labelToAdapter.get(label); - if (adapter == null) { - throw new JsonDataException("Expected one of " - + labelToAdapter.keySet() - + " for key '" - + labelKey - + "' but found '" - + label - + "'. Register a subtype for this label."); - } - return adapter.fromJsonValue(jsonValue); + + throw new JsonDataException("Missing label for " + labelKey); } @Override public void toJson(JsonWriter writer, Object value) throws IOException { Class type = value.getClass(); - String label = typeToLabel.get(type); - if (label == null) { + int labelIndex = subtypes.indexOf(type); + if (labelIndex == -1) { throw new IllegalArgumentException("Expected one of " - + typeToLabel.keySet() + + subtypes + " but found " + value + ", a " + value.getClass() + ". Register this subtype."); } - JsonAdapter adapter = labelToAdapter.get(label); + JsonAdapter adapter = jsonAdapters.get(labelIndex); Map jsonValue = (Map) adapter.toJsonValue(value); Map valueWithLabel = new LinkedHashMap<>(1 + jsonValue.size()); - valueWithLabel.put(labelKey, label); + valueWithLabel.put(labelKey, labels.get(labelIndex)); valueWithLabel.putAll(jsonValue); objectJsonAdapter.toJson(writer, valueWithLabel); } diff --git a/adapters/src/test/java/com/squareup/moshi/adapters/RuntimeJsonAdapterFactoryTest.java b/adapters/src/test/java/com/squareup/moshi/adapters/RuntimeJsonAdapterFactoryTest.java index 2998aaa..7491db9 100644 --- a/adapters/src/test/java/com/squareup/moshi/adapters/RuntimeJsonAdapterFactoryTest.java +++ b/adapters/src/test/java/com/squareup/moshi/adapters/RuntimeJsonAdapterFactoryTest.java @@ -33,8 +33,8 @@ public final class RuntimeJsonAdapterFactoryTest { @Test public void fromJson() throws IOException { Moshi moshi = new Moshi.Builder() .add(RuntimeJsonAdapterFactory.of(Message.class, "type") - .registerSubtype(Success.class, "success") - .registerSubtype(Error.class, "error")) + .withSubtype(Success.class, "success") + .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); @@ -47,8 +47,8 @@ public final class RuntimeJsonAdapterFactoryTest { @Test public void toJson() { Moshi moshi = new Moshi.Builder() .add(RuntimeJsonAdapterFactory.of(Message.class, "type") - .registerSubtype(Success.class, "success") - .registerSubtype(Error.class, "error")) + .withSubtype(Success.class, "success") + .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); @@ -61,8 +61,8 @@ public final class RuntimeJsonAdapterFactoryTest { @Test public void unregisteredLabelValue() throws IOException { Moshi moshi = new Moshi.Builder() .add(RuntimeJsonAdapterFactory.of(Message.class, "type") - .registerSubtype(Success.class, "success") - .registerSubtype(Error.class, "error")) + .withSubtype(Success.class, "success") + .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); @@ -75,14 +75,14 @@ public final class RuntimeJsonAdapterFactoryTest { assertThat(expected).hasMessage("Expected one of [success, error] for key 'type' but found" + " 'data'. Register a subtype for this label."); } - assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT); } @Test public void unregisteredSubtype() { Moshi moshi = new Moshi.Builder() .add(RuntimeJsonAdapterFactory.of(Message.class, "type") - .registerSubtype(Success.class, "success") - .registerSubtype(Error.class, "error")) + .withSubtype(Success.class, "success") + .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); @@ -101,8 +101,8 @@ public final class RuntimeJsonAdapterFactoryTest { @Test public void nonStringLabelValue() throws IOException { Moshi moshi = new Moshi.Builder() .add(RuntimeJsonAdapterFactory.of(Message.class, "type") - .registerSubtype(Success.class, "success") - .registerSubtype(Error.class, "error")) + .withSubtype(Success.class, "success") + .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); @@ -110,16 +110,15 @@ public final class RuntimeJsonAdapterFactoryTest { adapter.fromJson("{\"type\":{},\"value\":\"Okay!\"}"); fail(); } catch (JsonDataException expected) { - assertThat(expected).hasMessage("Label for 'type' must be a string but was {}," - + " a class com.squareup.moshi.LinkedHashTreeMap"); + assertThat(expected).hasMessage("Expected a string but was BEGIN_OBJECT at path $.type"); } } @Test public void nonObjectDoesNotConsume() throws IOException { Moshi moshi = new Moshi.Builder() .add(RuntimeJsonAdapterFactory.of(Message.class, "type") - .registerSubtype(Success.class, "success") - .registerSubtype(Error.class, "error")) + .withSubtype(Success.class, "success") + .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); @@ -137,9 +136,9 @@ public final class RuntimeJsonAdapterFactoryTest { @Test public void uniqueSubtypes() { RuntimeJsonAdapterFactory factory = RuntimeJsonAdapterFactory.of(Message.class, "type") - .registerSubtype(Success.class, "success"); + .withSubtype(Success.class, "success"); try { - factory.registerSubtype(Success.class, "data"); + factory.withSubtype(Success.class, "data"); fail(); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessage("Subtypes and labels must be unique."); @@ -149,9 +148,9 @@ public final class RuntimeJsonAdapterFactoryTest { @Test public void uniqueLabels() { RuntimeJsonAdapterFactory factory = RuntimeJsonAdapterFactory.of(Message.class, "type") - .registerSubtype(Success.class, "data"); + .withSubtype(Success.class, "data"); try { - factory.registerSubtype(Error.class, "data"); + factory.withSubtype(Error.class, "data"); fail(); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessage("Subtypes and labels must be unique."); @@ -161,8 +160,8 @@ public final class RuntimeJsonAdapterFactoryTest { @Test public void nullSafe() throws IOException { Moshi moshi = new Moshi.Builder() .add(RuntimeJsonAdapterFactory.of(Message.class, "type") - .registerSubtype(Success.class, "success") - .registerSubtype(Error.class, "error")) + .withSubtype(Success.class, "success") + .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class);