Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.mobilitydata.gtfsvalidator.notice;

import static org.mobilitydata.gtfsvalidator.notice.SeverityLevel.ERROR;

import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice;

/** Duplicated elements in locations.geojson file. */
@GtfsValidationNotice(severity = ERROR)
public class GeoJsonDuplicatedElementNotice extends ValidationNotice {
/** The name of the file where the duplicated element was found. */
private final String filename;

/** The duplicated element in the GeoJSON file. */
private final String duplicatedElement;

public GeoJsonDuplicatedElementNotice(String filename, String duplicatedElement) {
this.filename = filename;
this.duplicatedElement = duplicatedElement;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.mobilitydata.gtfsvalidator.notice;

import static org.mobilitydata.gtfsvalidator.notice.SeverityLevel.INFO;

import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice;

/** Unknown elements in locations.geojson file. */
@GtfsValidationNotice(severity = INFO)
public class GeoJsonUnknownElementNotice extends ValidationNotice {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this class if it won't be part of the implementation in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed.

/** The name of the file where the unknown element was found. */
private final String filename;

/** The unknown element in the GeoJSON file. */
private final String unknownElement;

public GeoJsonUnknownElementNotice(String filename, String unknownElement) {
this.filename = filename;
this.unknownElement = unknownElement;
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
package org.mobilitydata.gtfsvalidator.table;

import com.google.common.flogger.FluentLogger;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.locationtech.jts.geom.*;
import org.mobilitydata.gtfsvalidator.notice.*;
import org.mobilitydata.gtfsvalidator.util.geojson.GeoJsonGeometryValidator;
import org.mobilitydata.gtfsvalidator.util.geojson.GeometryType;
import org.mobilitydata.gtfsvalidator.util.geojson.UnparsableGeoJsonFeatureException;
import org.mobilitydata.gtfsvalidator.notice.GeoJsonDuplicatedElementNotice;
import org.mobilitydata.gtfsvalidator.util.geojson.*;
import org.mobilitydata.gtfsvalidator.validator.ValidatorProvider;

/**
Expand Down Expand Up @@ -70,8 +67,20 @@ public List<GtfsGeoJsonFeature> extractFeaturesFromStream(
throws IOException, UnparsableGeoJsonFeatureException {
List<GtfsGeoJsonFeature> features = new ArrayList<>();
boolean hasUnparsableFeature = false;
GsonBuilder gsonBuilder = new GsonBuilder();
// Using the MapJsonTypeAdapter to be able to parse JSON objects with duplicate keys and
// unsupported Gson library features
gsonBuilder.registerTypeAdapter(
new TypeToken<Map<String, Object>>() {}.getType(), new MapJsonTypeAdapter());
Gson gson = gsonBuilder.create();

try (InputStreamReader reader = new InputStreamReader(inputStream)) {
JsonObject jsonObject = JsonParser.parseReader(reader).getAsJsonObject();
JsonElement root =
gson.toJsonTree(gson.fromJson(reader, new TypeToken<Map<String, Object>>() {}.getType()));
if (!root.isJsonObject()) {
throw new JsonParseException("Expected a JSON object at the root");
}
JsonObject jsonObject = root.getAsJsonObject();
if (!jsonObject.has("type")) {
noticeContainer.addValidationNotice(new MissingRequiredElementNotice(null, "type", null));
throw new UnparsableGeoJsonFeatureException("Missing required field 'type'");
Expand All @@ -93,6 +102,9 @@ public List<GtfsGeoJsonFeature> extractFeaturesFromStream(
features.add(gtfsGeoJsonFeature);
}
}
} catch (DuplicateJsonKeyException exception) {
noticeContainer.addValidationNotice(
new GeoJsonDuplicatedElementNotice(GtfsGeoJsonFeature.FILENAME, exception.getKey()));
}
if (hasUnparsableFeature) {
throw new UnparsableGeoJsonFeatureException("Unparsable GeoJSON feature");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.mobilitydata.gtfsvalidator.util.geojson;

public class DuplicateJsonKeyException extends RuntimeException {
private String key;
private String message;

public DuplicateJsonKeyException(String key, String message) {
this.key = key;
this.message = message;
}

public String getKey() {
return key;
}

public String getMessage() {
return message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.mobilitydata.gtfsvalidator.util.geojson;

import com.google.gson.*;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* A custom JSON type adapter for parsing JSON objects with duplicate keys. The target class is
* {@link Map}{@code <String, Object>}. as JSonElement is captured by the default Gson TypeAdapter.
*
* <p>When a JSON object has two keys with the same name at the same level, this type adapter throws
* a {@link DuplicateJsonKeyException}.
*/
public class MapJsonTypeAdapter extends TypeAdapter<Map<String, Object>> {

@Override
public void write(JsonWriter out, Map<String, Object> value) throws IOException {
new Gson().toJson(value, Map.class, out);
}

@Override
public Map<String, Object> read(JsonReader in) throws IOException {
return parseJsonObject(in);
}

private Map<String, Object> parseJsonObject(JsonReader in) throws IOException {
Map<String, Object> map = new LinkedHashMap<>();

in.beginObject();
while (in.hasNext()) {
String key = in.nextName();

if (map.containsKey(key)) {
throw new DuplicateJsonKeyException(key, "Duplicated Key: " + key);
}

Object value = parseJsonValue(in);
map.put(key, value);
}
in.endObject();

return map;
}

private Object parseJsonValue(JsonReader in) throws IOException {
switch (in.peek()) {
case BEGIN_OBJECT:
return parseJsonObject(in);
case BEGIN_ARRAY:
return parseJsonArray(in);
case STRING:
return in.nextString();
case NUMBER:
return in.nextDouble();
case BOOLEAN:
return in.nextBoolean();
case NULL:
in.nextNull();
return null;
default:
throw new JsonParseException("Unexpected JSON token: " + in.peek());
}
}

private Object parseJsonArray(JsonReader in) throws IOException {
JsonArray jsonArray = new JsonArray();
in.beginArray();
while (in.hasNext()) {
jsonArray.add(new Gson().toJsonTree(parseJsonValue(in)));
}
in.endArray();
return jsonArray;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.mobilitydata.gtfsvalidator.util.geojson;

public class UnknownJsonKeyException extends RuntimeException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this class if it won't be part of the implementation in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

private final String message;
private String key;

public UnknownJsonKeyException(String key, String message) {
this.key = key;
this.message = message;
}

public String getKey() {
return key;
}

public String getMessage() {
return message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.mobilitydata.gtfsvalidator.util.geojson;

import static org.junit.Assert.assertThrows;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import java.io.StringReader;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;

public class GeoJsonTypeAdapterTest {

Gson gson;

@Before
public void before() {
gson =
(new GsonBuilder())
.registerTypeAdapter(
new TypeToken<Map<String, Object>>() {}.getType(), new MapJsonTypeAdapter())
.create();
}

/**
* Test that the custom JSON type adapter can handle a throws DuplicateJsonKeyException when: - A
* JSON object has two keys with the same name at the same level.
*/
@Test
public void testDuplicateKeyExceptionSameLevel() {
final var json = "{ \"type\": 1, \"type\": 2 }";
JsonReader reader = new JsonReader(new StringReader(json));
MapJsonTypeAdapter adapter = new MapJsonTypeAdapter();
assertThrows(
DuplicateJsonKeyException.class,
() -> {
adapter.read(reader);
});
}

/**
* Test that the custom JSON type adapter can handle a simple JSON object that don't contain
* duplicate keys.
*/
@Test
public void testDuplicateKeyExceptionNestedLevel() {
String json =
"{\"type\": \"Alice\", \"features\": { \"properties\": \"Bob\", \"properties\": \"abc\" }}";
JsonReader reader = new JsonReader(new StringReader(json));
MapJsonTypeAdapter adapter = new MapJsonTypeAdapter();

assertThrows(
DuplicateJsonKeyException.class,
() -> {
adapter.read(reader);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,9 @@ public void testNoticeClassFieldNames() {
"tripIdB",
"tripIdFieldName",
"validator",
"value");
"value",
"duplicatedElement",
"unknownElement");
}

private static List<String> discoverValidationNoticeFieldNames() {
Expand Down
Loading