-
Notifications
You must be signed in to change notification settings - Fork 123
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Correct handling of a single element batch geocoding response
- Loading branch information
1 parent
50c9d96
commit 3f9c4b2
Showing
6 changed files
with
399 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 102 additions & 0 deletions
102
...geocoding/src/main/java/com/mapbox/api/geocoding/v5/SingleElementSafeListTypeAdapter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
package com.mapbox.api.geocoding.v5; | ||
|
||
import com.google.gson.Gson; | ||
import com.google.gson.TypeAdapter; | ||
import com.google.gson.TypeAdapterFactory; | ||
import com.google.gson.internal.$Gson$Types; | ||
import com.google.gson.reflect.TypeToken; | ||
import com.google.gson.stream.JsonReader; | ||
import com.google.gson.stream.JsonToken; | ||
import com.google.gson.stream.JsonWriter; | ||
import com.google.gson.stream.MalformedJsonException; | ||
|
||
import java.io.IOException; | ||
import java.lang.reflect.Type; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
/** | ||
* Similar to {@link com.google.gson.internal.bind.CollectionTypeAdapterFactory}, | ||
* safely adapts single element list represented as Json object or primitive. | ||
* | ||
* Note: unlike {@link com.google.gson.internal.bind.CollectionTypeAdapterFactory}, | ||
* this adapter does not perform advanced type analyse and always returns instance of ArrayList | ||
* which may not work if it is used to deserialize JSON elements into another subtype of List | ||
* (LinkedList for example). | ||
* | ||
* @param <E> collection element type | ||
* | ||
* @since 5.4.0 | ||
*/ | ||
class SingleElementSafeListTypeAdapter<E> extends TypeAdapter<List<E>> { | ||
|
||
static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() { | ||
|
||
@Override | ||
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) { | ||
final Class<? super T> rawType = typeToken.getRawType(); | ||
if (!List.class.isAssignableFrom(rawType)) { | ||
return null; | ||
} | ||
|
||
final Type elementType = $Gson$Types.getCollectionElementType(typeToken.getType(), rawType); | ||
final TypeAdapter<?> elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType)); | ||
|
||
@SuppressWarnings("unchecked") | ||
final TypeAdapter<T> adapter = | ||
(TypeAdapter<T>) new SingleElementSafeListTypeAdapter<>(elementTypeAdapter); | ||
return adapter; | ||
} | ||
}; | ||
|
||
private final TypeAdapter<E> elementTypeAdapter; | ||
|
||
private SingleElementSafeListTypeAdapter(final TypeAdapter<E> elementTypeAdapter) { | ||
this.elementTypeAdapter = elementTypeAdapter; | ||
} | ||
|
||
@Override | ||
public List<E> read(final JsonReader in) throws IOException { | ||
final JsonToken token = in.peek(); | ||
final List<E> elements = new ArrayList<>(); | ||
switch (token) { | ||
case BEGIN_ARRAY: | ||
in.beginArray(); | ||
while (in.hasNext()) { | ||
elements.add(elementTypeAdapter.read(in)); | ||
} | ||
in.endArray(); | ||
return elements; | ||
case BEGIN_OBJECT: | ||
case STRING: | ||
case NUMBER: | ||
case BOOLEAN: | ||
elements.add(elementTypeAdapter.read(in)); | ||
return elements; | ||
case NULL: | ||
in.nextNull(); | ||
return null; | ||
case NAME: | ||
case END_ARRAY: | ||
case END_OBJECT: | ||
case END_DOCUMENT: | ||
throw new MalformedJsonException("Unexpected token: " + token); | ||
default: | ||
throw new IllegalStateException("Unprocessed token: " + token); | ||
} | ||
} | ||
|
||
@Override | ||
public void write(final JsonWriter out, final List<E> value) throws IOException { | ||
if (value == null) { | ||
out.nullValue(); | ||
return; | ||
} | ||
|
||
out.beginArray(); | ||
for (E element : value) { | ||
elementTypeAdapter.write(out, element); | ||
} | ||
out.endArray(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
186 changes: 186 additions & 0 deletions
186
...oding/src/test/java/com/mapbox/api/geocoding/v5/SingleElementSafeListTypeAdapterTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
package com.mapbox.api.geocoding.v5; | ||
|
||
import com.google.gson.Gson; | ||
import com.google.gson.GsonBuilder; | ||
import com.google.gson.annotations.SerializedName; | ||
import com.google.gson.reflect.TypeToken; | ||
|
||
import org.junit.Test; | ||
|
||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.List; | ||
|
||
import static org.junit.Assert.assertEquals; | ||
import static org.junit.Assert.assertNull; | ||
|
||
public class SingleElementSafeListTypeAdapterTest { | ||
|
||
@Test | ||
public void parseEmptyJson() { | ||
final Gson gson = createGson(); | ||
assertNull(gson.fromJson("", List.class)); | ||
} | ||
|
||
@Test | ||
public void parseStringArrayWithMultipleElements() { | ||
final Gson gson = createGson(); | ||
final List parsed = gson.fromJson("[\"a\", \"b\", \"c\"]", List.class); | ||
assertEquals(Arrays.asList("a", "b", "c"), parsed); | ||
} | ||
|
||
@Test | ||
public void parseStringArrayWithSingleElement() { | ||
final Gson gson = createGson(); | ||
final List parsed = gson.fromJson("\"a\"", List.class); | ||
assertEquals(Collections.singletonList("a"), parsed); | ||
} | ||
|
||
@Test | ||
public void parseNullJson() { | ||
final Gson gson = createGson(); | ||
assertNull(gson.fromJson("null", List.class)); | ||
} | ||
|
||
@Test | ||
public void parseArrayOfNulls() { | ||
final Gson gson = createGson(); | ||
final List parsed = gson.fromJson("[null, null, null]", List.class); | ||
assertEquals(Arrays.asList(null, null, null), parsed); | ||
} | ||
|
||
@Test | ||
public void parseBooleanArrayWithMultipleElements() { | ||
final Gson gson = createGson(); | ||
final List parsed = gson.fromJson("[true, false]", List.class); | ||
assertEquals(Arrays.asList(true, false), parsed); | ||
} | ||
|
||
@Test | ||
public void parseBooleanArrayWithSingleElement() { | ||
final Gson gson = createGson(); | ||
final List parsed = gson.fromJson("true", List.class); | ||
assertEquals(Collections.singletonList(true), parsed); | ||
} | ||
|
||
@Test | ||
public void parseNumberArrayWithMultipleElements() { | ||
final Gson gson = createGson(); | ||
final List parsed = gson.fromJson("[1, 2, 3]", List.class); | ||
assertEquals(Arrays.asList(1.0, 2.0, 3.0), parsed); | ||
} | ||
|
||
@Test | ||
public void parseNumberArrayWithSingleElement() { | ||
final Gson gson = createGson(); | ||
final List parsed = gson.fromJson("5", List.class); | ||
assertEquals(Collections.singletonList(5.0), parsed); | ||
} | ||
|
||
@Test | ||
public void parseCustomTypeArrayWithMultipleElements() { | ||
final Gson gson = createGson(); | ||
|
||
final String inputJson = "[" + | ||
"{\"string_field\":\"abc\",\"boolean_field\":true,\"int_field\":1}," + | ||
"{\"string_field\":\"def\",\"boolean_field\":false,\"int_field\":11}" + | ||
"]"; | ||
|
||
final TypeToken typeToken = TypeToken.getParameterized(List.class, TestType.class); | ||
final List parsed = gson.fromJson(inputJson, typeToken.getType()); | ||
|
||
final List<TestType> expectedList = Arrays.asList( | ||
new TestType("abc", true, 1), | ||
new TestType("def", false, 11) | ||
); | ||
assertEquals(expectedList, parsed); | ||
} | ||
|
||
@Test | ||
public void parseCustomTypeArrayWithSingleElement() { | ||
final Gson gson = createGson(); | ||
|
||
final String inputJson = "{\"string_field\":\"abc\",\"boolean_field\":true,\"int_field\":1}"; | ||
|
||
final TypeToken typeToken = TypeToken.getParameterized(List.class, TestType.class); | ||
final List parsed = gson.fromJson(inputJson, typeToken.getType()); | ||
|
||
final List<TestType> expectedList = | ||
Collections.singletonList(new TestType("abc", true, 1)); | ||
assertEquals(expectedList, parsed); | ||
} | ||
|
||
@Test | ||
public void serializeCustomTypeArrayWithMultipleElements() { | ||
final Gson gson = createGson(); | ||
|
||
final List<TestType> testData = Arrays.asList( | ||
new TestType("abc", true, 1), | ||
new TestType("def", false, 11) | ||
); | ||
|
||
final String serialized = gson.toJson(testData); | ||
|
||
final String expectedJson = "[" + | ||
"{\"string_field\":\"abc\",\"boolean_field\":true,\"int_field\":1}," + | ||
"{\"string_field\":\"def\",\"boolean_field\":false,\"int_field\":11}" + | ||
"]"; | ||
|
||
assertEquals(expectedJson, serialized); | ||
} | ||
|
||
@Test | ||
public void serializeCustomTypeArrayWithSingleElement() { | ||
final Gson gson = createGson(); | ||
|
||
final TestType testData = new TestType("abc", true, 1); | ||
final String serialized = gson.toJson(testData); | ||
|
||
final String expectedJson = "{\"string_field\":\"abc\",\"boolean_field\":true,\"int_field\":1}"; | ||
assertEquals(expectedJson, serialized); | ||
} | ||
|
||
private static Gson createGson() { | ||
return new GsonBuilder() | ||
.registerTypeAdapterFactory(SingleElementSafeListTypeAdapter.FACTORY) | ||
.create(); | ||
} | ||
|
||
private static class TestType { | ||
|
||
@SerializedName("string_field") | ||
private final String stringField; | ||
|
||
@SerializedName("boolean_field") | ||
private final boolean booleanField; | ||
|
||
@SerializedName("int_field") | ||
private final int intField; | ||
|
||
TestType(String stringField, boolean booleanField, int intField) { | ||
this.stringField = stringField; | ||
this.booleanField = booleanField; | ||
this.intField = intField; | ||
} | ||
|
||
@Override | ||
public boolean equals(Object o) { | ||
if (this == o) return true; | ||
if (o == null || getClass() != o.getClass()) return false; | ||
|
||
TestType testType = (TestType) o; | ||
|
||
if (booleanField != testType.booleanField) return false; | ||
if (intField != testType.intField) return false; | ||
return stringField != null ? stringField.equals(testType.stringField) : testType.stringField == null; | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
int result = stringField != null ? stringField.hashCode() : 0; | ||
result = 31 * result + (booleanField ? 1 : 0); | ||
result = 31 * result + intField; | ||
return result; | ||
} | ||
} | ||
} |
Oops, something went wrong.