Skip to content

Commit 1f7a240

Browse files
Correct handling of a single element batch geocoding response (#1160)
* Correct handling of a single element batch geocoding response * Clarify error message
1 parent 50c9d96 commit 1f7a240

6 files changed

Lines changed: 403 additions & 8 deletions

File tree

services-geocoding/src/main/java/com/mapbox/api/geocoding/v5/MapboxGeocoding.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ protected GsonBuilder getGsonBuilder() {
7474
return new GsonBuilder()
7575
.registerTypeAdapterFactory(GeocodingAdapterFactory.create())
7676
.registerTypeAdapterFactory(GeometryAdapterFactory.create())
77+
.registerTypeAdapterFactory(SingleElementSafeListTypeAdapter.FACTORY)
7778
.registerTypeAdapter(BoundingBox.class, new BoundingBoxTypeAdapter());
7879
}
7980

@@ -106,7 +107,9 @@ private Call<List<GeocodingResponse>> getBatchCall() {
106107
}
107108

108109
if (mode().equals(GeocodingCriteria.MODE_PLACES)) {
109-
throw new ServicesException("Use getCall() for non-batch calls.");
110+
throw new ServicesException(
111+
"Use getCall() for non-batch calls or set the mode to `permanent` for batch requests."
112+
);
110113
}
111114

112115
batchCall = getService().getBatchCall(
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package com.mapbox.api.geocoding.v5;
2+
3+
import com.google.gson.Gson;
4+
import com.google.gson.TypeAdapter;
5+
import com.google.gson.TypeAdapterFactory;
6+
import com.google.gson.internal.$Gson$Types;
7+
import com.google.gson.reflect.TypeToken;
8+
import com.google.gson.stream.JsonReader;
9+
import com.google.gson.stream.JsonToken;
10+
import com.google.gson.stream.JsonWriter;
11+
import com.google.gson.stream.MalformedJsonException;
12+
13+
import java.io.IOException;
14+
import java.lang.reflect.Type;
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
18+
/**
19+
* Similar to {@link com.google.gson.internal.bind.CollectionTypeAdapterFactory},
20+
* safely adapts single element list represented as Json object or primitive.
21+
*
22+
* Note: unlike {@link com.google.gson.internal.bind.CollectionTypeAdapterFactory},
23+
* this adapter does not perform advanced type analyse and always returns instance of ArrayList
24+
* which may not work if it is used to deserialize JSON elements into another subtype of List
25+
* (LinkedList for example).
26+
*
27+
* @param <E> collection element type
28+
*
29+
* @since 5.4.0
30+
*/
31+
class SingleElementSafeListTypeAdapter<E> extends TypeAdapter<List<E>> {
32+
33+
static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() {
34+
35+
@Override
36+
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
37+
final Class<? super T> rawType = typeToken.getRawType();
38+
if (!List.class.isAssignableFrom(rawType)) {
39+
return null;
40+
}
41+
42+
final Type elementType = $Gson$Types.getCollectionElementType(typeToken.getType(), rawType);
43+
final TypeAdapter<?> elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType));
44+
45+
@SuppressWarnings("unchecked")
46+
final TypeAdapter<T> adapter =
47+
(TypeAdapter<T>) new SingleElementSafeListTypeAdapter<>(elementTypeAdapter);
48+
return adapter;
49+
}
50+
};
51+
52+
private final TypeAdapter<E> elementTypeAdapter;
53+
54+
private SingleElementSafeListTypeAdapter(final TypeAdapter<E> elementTypeAdapter) {
55+
this.elementTypeAdapter = elementTypeAdapter;
56+
}
57+
58+
@Override
59+
public List<E> read(final JsonReader in) throws IOException {
60+
final JsonToken token = in.peek();
61+
final List<E> elements = new ArrayList<>();
62+
switch (token) {
63+
case BEGIN_ARRAY:
64+
in.beginArray();
65+
while (in.hasNext()) {
66+
elements.add(elementTypeAdapter.read(in));
67+
}
68+
in.endArray();
69+
return elements;
70+
case BEGIN_OBJECT:
71+
case STRING:
72+
case NUMBER:
73+
case BOOLEAN:
74+
elements.add(elementTypeAdapter.read(in));
75+
return elements;
76+
case NULL:
77+
in.nextNull();
78+
return null;
79+
case NAME:
80+
case END_ARRAY:
81+
case END_OBJECT:
82+
case END_DOCUMENT:
83+
throw new MalformedJsonException("Unexpected token: " + token);
84+
default:
85+
throw new IllegalStateException("Unprocessed token: " + token);
86+
}
87+
}
88+
89+
@Override
90+
public void write(final JsonWriter out, final List<E> value) throws IOException {
91+
if (value == null) {
92+
out.nullValue();
93+
return;
94+
}
95+
96+
out.beginArray();
97+
for (E element : value) {
98+
elementTypeAdapter.write(out, element);
99+
}
100+
out.endArray();
101+
}
102+
}

services-geocoding/src/test/java/com/mapbox/api/geocoding/v5/GeocodingTestUtils.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class GeocodingTestUtils extends TestUtils {
2020
private static final String FORWARD_INVALID = "forward_invalid.json";
2121
private static final String FORWARD_VALID_ZH = "forward_valid_zh.json";
2222
private static final String FORWARD_BATCH_GEOCODING = "geocoding_batch.json";
23+
private static final String FORWARD_BATCH_SINGLE_ITEM_GEOCODING = "geocoding_batch_single_object.json";
2324
private static final String FORWARD_INTERSECTION = "forward_intersection.json";
2425

2526
private MockWebServer server;
@@ -35,16 +36,19 @@ public void setUp() throws Exception {
3536
@Override
3637
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
3738
try {
38-
String response;
39-
if (request.getPath().contains(GeocodingCriteria.MODE_PLACES_PERMANENT)) {
39+
final String response;
40+
final String path = request.getPath();
41+
if (path.contains(GeocodingCriteria.MODE_PLACES_PERMANENT) && path.contains(";")) {
4042
response = loadJsonFixture(FORWARD_BATCH_GEOCODING);
41-
} else if (request.getPath().contains("1600") && !request.getPath().contains("nw")) {
43+
} else if (path.contains(GeocodingCriteria.MODE_PLACES_PERMANENT) && !path.contains(";")) {
44+
response = loadJsonFixture(FORWARD_BATCH_SINGLE_ITEM_GEOCODING);
45+
} else if (path.contains("1600") && !path.contains("nw")) {
4246
response = loadJsonFixture(FORWARD_VALID);
43-
} else if (request.getPath().contains("nw")) {
47+
} else if (path.contains("nw")) {
4448
response = loadJsonFixture(FORWARD_GEOCODING);
45-
} else if (request.getPath().contains("sandy")) {
49+
} else if (path.contains("sandy")) {
4650
response = loadJsonFixture(FORWARD_INVALID);
47-
} else if (request.getPath().contains("%20and%20")) {
51+
} else if (path.contains("%20and%20")) {
4852
response = loadJsonFixture(FORWARD_INTERSECTION);
4953
} else {
5054
response = loadJsonFixture(FORWARD_VALID_ZH);

services-geocoding/src/test/java/com/mapbox/api/geocoding/v5/MapboxGeocodingTest.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,23 @@ public void sanity_batchGeocodeRequest() throws Exception {
4545
assertEquals(200, response.code());
4646
}
4747

48+
@Test
49+
public void sanity_batchGeocodeSingleItemRequest() throws Exception {
50+
MapboxGeocoding mapboxGeocoding = MapboxGeocoding.builder()
51+
.mode(GeocodingCriteria.MODE_PLACES_PERMANENT)
52+
.accessToken(ACCESS_TOKEN)
53+
.query("20001")
54+
.baseUrl(mockUrl.toString())
55+
.build();
56+
assertNotNull(mapboxGeocoding);
57+
Response<List<GeocodingResponse>> response = mapboxGeocoding.executeBatchCall();
58+
assertEquals(200, response.code());
59+
}
60+
4861
@Test
4962
public void executeBatchCall_exceptionThrownWhenModeNotSetCorrectly() throws Exception {
5063
thrown.expect(ServicesException.class);
51-
thrown.expectMessage(startsWith("Use getCall() for non-batch calls."));
64+
thrown.expectMessage(startsWith("Use getCall() for non-batch calls or set the mode to `permanent` for batch requests."));
5265
MapboxGeocoding mapboxGeocoding = MapboxGeocoding.builder()
5366
.accessToken(ACCESS_TOKEN)
5467
.query("1600 pennsylvania ave nw")
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package com.mapbox.api.geocoding.v5;
2+
3+
import com.google.gson.Gson;
4+
import com.google.gson.GsonBuilder;
5+
import com.google.gson.annotations.SerializedName;
6+
import com.google.gson.reflect.TypeToken;
7+
8+
import org.junit.Test;
9+
10+
import java.util.Arrays;
11+
import java.util.Collections;
12+
import java.util.List;
13+
14+
import static org.junit.Assert.assertEquals;
15+
import static org.junit.Assert.assertNull;
16+
17+
public class SingleElementSafeListTypeAdapterTest {
18+
19+
@Test
20+
public void parseEmptyJson() {
21+
final Gson gson = createGson();
22+
assertNull(gson.fromJson("", List.class));
23+
}
24+
25+
@Test
26+
public void parseStringArrayWithMultipleElements() {
27+
final Gson gson = createGson();
28+
final List parsed = gson.fromJson("[\"a\", \"b\", \"c\"]", List.class);
29+
assertEquals(Arrays.asList("a", "b", "c"), parsed);
30+
}
31+
32+
@Test
33+
public void parseStringArrayWithSingleElement() {
34+
final Gson gson = createGson();
35+
final List parsed = gson.fromJson("\"a\"", List.class);
36+
assertEquals(Collections.singletonList("a"), parsed);
37+
}
38+
39+
@Test
40+
public void parseNullJson() {
41+
final Gson gson = createGson();
42+
assertNull(gson.fromJson("null", List.class));
43+
}
44+
45+
@Test
46+
public void parseArrayOfNulls() {
47+
final Gson gson = createGson();
48+
final List parsed = gson.fromJson("[null, null, null]", List.class);
49+
assertEquals(Arrays.asList(null, null, null), parsed);
50+
}
51+
52+
@Test
53+
public void parseBooleanArrayWithMultipleElements() {
54+
final Gson gson = createGson();
55+
final List parsed = gson.fromJson("[true, false]", List.class);
56+
assertEquals(Arrays.asList(true, false), parsed);
57+
}
58+
59+
@Test
60+
public void parseBooleanArrayWithSingleElement() {
61+
final Gson gson = createGson();
62+
final List parsed = gson.fromJson("true", List.class);
63+
assertEquals(Collections.singletonList(true), parsed);
64+
}
65+
66+
@Test
67+
public void parseNumberArrayWithMultipleElements() {
68+
final Gson gson = createGson();
69+
final List parsed = gson.fromJson("[1, 2, 3]", List.class);
70+
assertEquals(Arrays.asList(1.0, 2.0, 3.0), parsed);
71+
}
72+
73+
@Test
74+
public void parseNumberArrayWithSingleElement() {
75+
final Gson gson = createGson();
76+
final List parsed = gson.fromJson("5", List.class);
77+
assertEquals(Collections.singletonList(5.0), parsed);
78+
}
79+
80+
@Test
81+
public void parseCustomTypeArrayWithMultipleElements() {
82+
final Gson gson = createGson();
83+
84+
final String inputJson = "[" +
85+
"{\"string_field\":\"abc\",\"boolean_field\":true,\"int_field\":1}," +
86+
"{\"string_field\":\"def\",\"boolean_field\":false,\"int_field\":11}" +
87+
"]";
88+
89+
final TypeToken typeToken = TypeToken.getParameterized(List.class, TestType.class);
90+
final List parsed = gson.fromJson(inputJson, typeToken.getType());
91+
92+
final List<TestType> expectedList = Arrays.asList(
93+
new TestType("abc", true, 1),
94+
new TestType("def", false, 11)
95+
);
96+
assertEquals(expectedList, parsed);
97+
}
98+
99+
@Test
100+
public void parseCustomTypeArrayWithSingleElement() {
101+
final Gson gson = createGson();
102+
103+
final String inputJson = "{\"string_field\":\"abc\",\"boolean_field\":true,\"int_field\":1}";
104+
105+
final TypeToken typeToken = TypeToken.getParameterized(List.class, TestType.class);
106+
final List parsed = gson.fromJson(inputJson, typeToken.getType());
107+
108+
final List<TestType> expectedList =
109+
Collections.singletonList(new TestType("abc", true, 1));
110+
assertEquals(expectedList, parsed);
111+
}
112+
113+
@Test
114+
public void serializeCustomTypeArrayWithMultipleElements() {
115+
final Gson gson = createGson();
116+
117+
final List<TestType> testData = Arrays.asList(
118+
new TestType("abc", true, 1),
119+
new TestType("def", false, 11)
120+
);
121+
122+
final String serialized = gson.toJson(testData);
123+
124+
final String expectedJson = "[" +
125+
"{\"string_field\":\"abc\",\"boolean_field\":true,\"int_field\":1}," +
126+
"{\"string_field\":\"def\",\"boolean_field\":false,\"int_field\":11}" +
127+
"]";
128+
129+
assertEquals(expectedJson, serialized);
130+
}
131+
132+
@Test
133+
public void serializeCustomTypeArrayWithSingleElement() {
134+
final Gson gson = createGson();
135+
136+
final TestType testData = new TestType("abc", true, 1);
137+
final String serialized = gson.toJson(testData);
138+
139+
final String expectedJson = "{\"string_field\":\"abc\",\"boolean_field\":true,\"int_field\":1}";
140+
assertEquals(expectedJson, serialized);
141+
}
142+
143+
private static Gson createGson() {
144+
return new GsonBuilder()
145+
.registerTypeAdapterFactory(SingleElementSafeListTypeAdapter.FACTORY)
146+
.create();
147+
}
148+
149+
private static class TestType {
150+
151+
@SerializedName("string_field")
152+
private final String stringField;
153+
154+
@SerializedName("boolean_field")
155+
private final boolean booleanField;
156+
157+
@SerializedName("int_field")
158+
private final int intField;
159+
160+
TestType(String stringField, boolean booleanField, int intField) {
161+
this.stringField = stringField;
162+
this.booleanField = booleanField;
163+
this.intField = intField;
164+
}
165+
166+
@Override
167+
public boolean equals(Object o) {
168+
if (this == o) return true;
169+
if (o == null || getClass() != o.getClass()) return false;
170+
171+
TestType testType = (TestType) o;
172+
173+
if (booleanField != testType.booleanField) return false;
174+
if (intField != testType.intField) return false;
175+
return stringField != null ? stringField.equals(testType.stringField) : testType.stringField == null;
176+
}
177+
178+
@Override
179+
public int hashCode() {
180+
int result = stringField != null ? stringField.hashCode() : 0;
181+
result = 31 * result + (booleanField ? 1 : 0);
182+
result = 31 * result + intField;
183+
return result;
184+
}
185+
}
186+
}

0 commit comments

Comments
 (0)