Skip to content

Commit 426d638

Browse files
qcdyxdavidgamez
andauthored
feat: 1929 duplicated element (#1970)
* add json map type adapter and catched JsonSyntaxException Co-authored-by: David Gamez Diaz <[email protected]>
1 parent 02c5307 commit 426d638

File tree

6 files changed

+199
-10
lines changed

6 files changed

+199
-10
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.mobilitydata.gtfsvalidator.notice;
2+
3+
import static org.mobilitydata.gtfsvalidator.notice.SeverityLevel.ERROR;
4+
5+
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice;
6+
7+
/** Duplicated elements in locations.geojson file. */
8+
@GtfsValidationNotice(severity = ERROR)
9+
public class GeoJsonDuplicatedElementNotice extends ValidationNotice {
10+
/** The name of the file where the duplicated element was found. */
11+
private final String filename;
12+
13+
/** The duplicated element in the GeoJSON file. */
14+
private final String duplicatedElement;
15+
16+
public GeoJsonDuplicatedElementNotice(String filename, String duplicatedElement) {
17+
this.filename = filename;
18+
this.duplicatedElement = duplicatedElement;
19+
}
20+
}

main/src/main/java/org/mobilitydata/gtfsvalidator/table/GeoJsonFileLoader.java

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
package org.mobilitydata.gtfsvalidator.table;
22

33
import com.google.common.flogger.FluentLogger;
4-
import com.google.gson.JsonArray;
5-
import com.google.gson.JsonElement;
6-
import com.google.gson.JsonObject;
7-
import com.google.gson.JsonParseException;
8-
import com.google.gson.JsonParser;
4+
import com.google.gson.*;
5+
import com.google.gson.reflect.TypeToken;
96
import java.io.IOException;
107
import java.io.InputStream;
118
import java.io.InputStreamReader;
129
import java.util.ArrayList;
1310
import java.util.List;
11+
import java.util.Map;
1412
import org.locationtech.jts.geom.*;
1513
import org.mobilitydata.gtfsvalidator.notice.*;
16-
import org.mobilitydata.gtfsvalidator.util.geojson.GeoJsonGeometryValidator;
17-
import org.mobilitydata.gtfsvalidator.util.geojson.GeometryType;
18-
import org.mobilitydata.gtfsvalidator.util.geojson.UnparsableGeoJsonFeatureException;
14+
import org.mobilitydata.gtfsvalidator.notice.GeoJsonDuplicatedElementNotice;
15+
import org.mobilitydata.gtfsvalidator.util.geojson.*;
1916
import org.mobilitydata.gtfsvalidator.validator.ValidatorProvider;
2017

2118
/**
@@ -70,8 +67,20 @@ public List<GtfsGeoJsonFeature> extractFeaturesFromStream(
7067
throws IOException, UnparsableGeoJsonFeatureException {
7168
List<GtfsGeoJsonFeature> features = new ArrayList<>();
7269
boolean hasUnparsableFeature = false;
70+
GsonBuilder gsonBuilder = new GsonBuilder();
71+
// Using the MapJsonTypeAdapter to be able to parse JSON objects with duplicate keys and
72+
// unsupported Gson library features
73+
gsonBuilder.registerTypeAdapter(
74+
new TypeToken<Map<String, Object>>() {}.getType(), new MapJsonTypeAdapter());
75+
Gson gson = gsonBuilder.create();
76+
7377
try (InputStreamReader reader = new InputStreamReader(inputStream)) {
74-
JsonObject jsonObject = JsonParser.parseReader(reader).getAsJsonObject();
78+
JsonElement root =
79+
gson.toJsonTree(gson.fromJson(reader, new TypeToken<Map<String, Object>>() {}.getType()));
80+
if (!root.isJsonObject()) {
81+
throw new JsonParseException("Expected a JSON object at the root");
82+
}
83+
JsonObject jsonObject = root.getAsJsonObject();
7584
if (!jsonObject.has("type")) {
7685
noticeContainer.addValidationNotice(new MissingRequiredElementNotice(null, "type", null));
7786
throw new UnparsableGeoJsonFeatureException("Missing required field 'type'");
@@ -93,6 +102,9 @@ public List<GtfsGeoJsonFeature> extractFeaturesFromStream(
93102
features.add(gtfsGeoJsonFeature);
94103
}
95104
}
105+
} catch (DuplicateJsonKeyException exception) {
106+
noticeContainer.addValidationNotice(
107+
new GeoJsonDuplicatedElementNotice(GtfsGeoJsonFeature.FILENAME, exception.getKey()));
96108
}
97109
if (hasUnparsableFeature) {
98110
throw new UnparsableGeoJsonFeatureException("Unparsable GeoJSON feature");
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.mobilitydata.gtfsvalidator.util.geojson;
2+
3+
public class DuplicateJsonKeyException extends RuntimeException {
4+
private String key;
5+
private String message;
6+
7+
public DuplicateJsonKeyException(String key, String message) {
8+
this.key = key;
9+
this.message = message;
10+
}
11+
12+
public String getKey() {
13+
return key;
14+
}
15+
16+
public String getMessage() {
17+
return message;
18+
}
19+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package org.mobilitydata.gtfsvalidator.util.geojson;
2+
3+
import com.google.gson.*;
4+
import com.google.gson.stream.JsonReader;
5+
import com.google.gson.stream.JsonWriter;
6+
import java.io.IOException;
7+
import java.util.LinkedHashMap;
8+
import java.util.Map;
9+
10+
/**
11+
* A custom JSON type adapter for parsing JSON objects with duplicate keys. The target class is
12+
* {@link Map}{@code <String, Object>}. as JSonElement is captured by the default Gson TypeAdapter.
13+
*
14+
* <p>When a JSON object has two keys with the same name at the same level, this type adapter throws
15+
* a {@link DuplicateJsonKeyException}.
16+
*/
17+
public class MapJsonTypeAdapter extends TypeAdapter<Map<String, Object>> {
18+
19+
@Override
20+
public void write(JsonWriter out, Map<String, Object> value) throws IOException {
21+
new Gson().toJson(value, Map.class, out);
22+
}
23+
24+
@Override
25+
public Map<String, Object> read(JsonReader in) throws IOException {
26+
return parseJsonObject(in);
27+
}
28+
29+
private Map<String, Object> parseJsonObject(JsonReader in) throws IOException {
30+
Map<String, Object> map = new LinkedHashMap<>();
31+
32+
in.beginObject();
33+
while (in.hasNext()) {
34+
String key = in.nextName();
35+
36+
if (map.containsKey(key)) {
37+
throw new DuplicateJsonKeyException(key, "Duplicated Key: " + key);
38+
}
39+
40+
Object value = parseJsonValue(in);
41+
map.put(key, value);
42+
}
43+
in.endObject();
44+
45+
return map;
46+
}
47+
48+
private Object parseJsonValue(JsonReader in) throws IOException {
49+
switch (in.peek()) {
50+
case BEGIN_OBJECT:
51+
return parseJsonObject(in);
52+
case BEGIN_ARRAY:
53+
return parseJsonArray(in);
54+
case STRING:
55+
return in.nextString();
56+
case NUMBER:
57+
return in.nextDouble();
58+
case BOOLEAN:
59+
return in.nextBoolean();
60+
case NULL:
61+
in.nextNull();
62+
return null;
63+
default:
64+
throw new JsonParseException("Unexpected JSON token: " + in.peek());
65+
}
66+
}
67+
68+
private Object parseJsonArray(JsonReader in) throws IOException {
69+
JsonArray jsonArray = new JsonArray();
70+
in.beginArray();
71+
while (in.hasNext()) {
72+
jsonArray.add(new Gson().toJsonTree(parseJsonValue(in)));
73+
}
74+
in.endArray();
75+
return jsonArray;
76+
}
77+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package org.mobilitydata.gtfsvalidator.util.geojson;
2+
3+
import static org.junit.Assert.assertThrows;
4+
5+
import com.google.gson.Gson;
6+
import com.google.gson.GsonBuilder;
7+
import com.google.gson.reflect.TypeToken;
8+
import com.google.gson.stream.JsonReader;
9+
import java.io.StringReader;
10+
import java.util.Map;
11+
import org.junit.Before;
12+
import org.junit.Test;
13+
14+
public class GeoJsonTypeAdapterTest {
15+
16+
Gson gson;
17+
18+
@Before
19+
public void before() {
20+
gson =
21+
(new GsonBuilder())
22+
.registerTypeAdapter(
23+
new TypeToken<Map<String, Object>>() {}.getType(), new MapJsonTypeAdapter())
24+
.create();
25+
}
26+
27+
/**
28+
* Test that the custom JSON type adapter can handle a throws DuplicateJsonKeyException when: - A
29+
* JSON object has two keys with the same name at the same level.
30+
*/
31+
@Test
32+
public void testDuplicateKeyExceptionSameLevel() {
33+
final var json = "{ \"type\": 1, \"type\": 2 }";
34+
JsonReader reader = new JsonReader(new StringReader(json));
35+
MapJsonTypeAdapter adapter = new MapJsonTypeAdapter();
36+
assertThrows(
37+
DuplicateJsonKeyException.class,
38+
() -> {
39+
adapter.read(reader);
40+
});
41+
}
42+
43+
/**
44+
* Test that the custom JSON type adapter can handle a simple JSON object that don't contain
45+
* duplicate keys.
46+
*/
47+
@Test
48+
public void testDuplicateKeyExceptionNestedLevel() {
49+
String json =
50+
"{\"type\": \"Alice\", \"features\": { \"properties\": \"Bob\", \"properties\": \"abc\" }}";
51+
JsonReader reader = new JsonReader(new StringReader(json));
52+
MapJsonTypeAdapter adapter = new MapJsonTypeAdapter();
53+
54+
assertThrows(
55+
DuplicateJsonKeyException.class,
56+
() -> {
57+
adapter.read(reader);
58+
});
59+
}
60+
}

main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,8 @@ public void testNoticeClassFieldNames() {
221221
"tripIdB",
222222
"tripIdFieldName",
223223
"validator",
224-
"value");
224+
"value",
225+
"duplicatedElement");
225226
}
226227

227228
private static List<String> discoverValidationNoticeFieldNames() {

0 commit comments

Comments
 (0)