Skip to content

Commit

Permalink
Correct handling of a single element batch geocoding response
Browse files Browse the repository at this point in the history
  • Loading branch information
DzmitryFomchyn committed Jul 28, 2020
1 parent 50c9d96 commit 3f9c4b2
Show file tree
Hide file tree
Showing 6 changed files with 399 additions and 6 deletions.
Expand Up @@ -74,6 +74,7 @@ protected GsonBuilder getGsonBuilder() {
return new GsonBuilder()
.registerTypeAdapterFactory(GeocodingAdapterFactory.create())
.registerTypeAdapterFactory(GeometryAdapterFactory.create())
.registerTypeAdapterFactory(SingleElementSafeListTypeAdapter.FACTORY)
.registerTypeAdapter(BoundingBox.class, new BoundingBoxTypeAdapter());
}

Expand Down
@@ -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();
}
}
Expand Up @@ -20,6 +20,7 @@ public class GeocodingTestUtils extends TestUtils {
private static final String FORWARD_INVALID = "forward_invalid.json";
private static final String FORWARD_VALID_ZH = "forward_valid_zh.json";
private static final String FORWARD_BATCH_GEOCODING = "geocoding_batch.json";
private static final String FORWARD_BATCH_SINGLE_ITEM_GEOCODING = "geocoding_batch_single_object.json";
private static final String FORWARD_INTERSECTION = "forward_intersection.json";

private MockWebServer server;
Expand All @@ -35,16 +36,19 @@ public void setUp() throws Exception {
@Override
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
try {
String response;
if (request.getPath().contains(GeocodingCriteria.MODE_PLACES_PERMANENT)) {
final String response;
final String path = request.getPath();
if (path.contains(GeocodingCriteria.MODE_PLACES_PERMANENT) && path.contains(";")) {
response = loadJsonFixture(FORWARD_BATCH_GEOCODING);
} else if (request.getPath().contains("1600") && !request.getPath().contains("nw")) {
} else if (path.contains(GeocodingCriteria.MODE_PLACES_PERMANENT) && !path.contains(";")) {
response = loadJsonFixture(FORWARD_BATCH_SINGLE_ITEM_GEOCODING);
} else if (path.contains("1600") && !path.contains("nw")) {
response = loadJsonFixture(FORWARD_VALID);
} else if (request.getPath().contains("nw")) {
} else if (path.contains("nw")) {
response = loadJsonFixture(FORWARD_GEOCODING);
} else if (request.getPath().contains("sandy")) {
} else if (path.contains("sandy")) {
response = loadJsonFixture(FORWARD_INVALID);
} else if (request.getPath().contains("%20and%20")) {
} else if (path.contains("%20and%20")) {
response = loadJsonFixture(FORWARD_INTERSECTION);
} else {
response = loadJsonFixture(FORWARD_VALID_ZH);
Expand Down
Expand Up @@ -45,6 +45,19 @@ public void sanity_batchGeocodeRequest() throws Exception {
assertEquals(200, response.code());
}

@Test
public void sanity_batchGeocodeSingleItemRequest() throws Exception {
MapboxGeocoding mapboxGeocoding = MapboxGeocoding.builder()
.mode(GeocodingCriteria.MODE_PLACES_PERMANENT)
.accessToken(ACCESS_TOKEN)
.query("20001")
.baseUrl(mockUrl.toString())
.build();
assertNotNull(mapboxGeocoding);
Response<List<GeocodingResponse>> response = mapboxGeocoding.executeBatchCall();
assertEquals(200, response.code());
}

@Test
public void executeBatchCall_exceptionThrownWhenModeNotSetCorrectly() throws Exception {
thrown.expect(ServicesException.class);
Expand Down
@@ -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;
}
}
}

0 comments on commit 3f9c4b2

Please sign in to comment.