This is an automated email from the ASF dual-hosted git repository.
afs pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git
The following commit(s) were added to refs/heads/main by this push:
new b8babed569 GH-3936: Check for trailing comma in JSON arrays for vars
and bindings
b8babed569 is described below
commit b8babed5693191564ca16eceab9c25ed1dfc3ee1
Author: Andy Seaborne <[email protected]>
AuthorDate: Sat May 16 16:11:21 2026 +0100
GH-3936: Check for trailing comma in JSON arrays for vars and bindings
---
.../rowset/rw/rs_json/RowSetJSONStreaming.java | 62 +++++++++++++++-------
.../jena/riot/rowset/rw/TestRowSetReader.java | 57 ++++++++++++++++++--
jena-cmds/src/main/java/arq/rset.java | 10 +++-
3 files changed, 105 insertions(+), 24 deletions(-)
diff --git
a/jena-arq/src/main/java/org/apache/jena/riot/rowset/rw/rs_json/RowSetJSONStreaming.java
b/jena-arq/src/main/java/org/apache/jena/riot/rowset/rw/rs_json/RowSetJSONStreaming.java
index 2a2599137e..0eeead8606 100644
---
a/jena-arq/src/main/java/org/apache/jena/riot/rowset/rw/rs_json/RowSetJSONStreaming.java
+++
b/jena-arq/src/main/java/org/apache/jena/riot/rowset/rw/rs_json/RowSetJSONStreaming.java
@@ -35,9 +35,7 @@ import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;
-import com.google.gson.Gson;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
+import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
@@ -82,7 +80,6 @@ public class RowSetJSONStreaming<E> extends
IteratorSlotted<Binding> implements
Gson gson = new Gson();
JsonReader reader = gson.newJsonReader(new InputStreamReader(in,
StandardCharsets.UTF_8));
-
// Set up handling of unexpected json elements
UnexpectedJsonEltHandler unexpectedJsonHandler = (_gson, _reader) -> {
ErrorHandlers.relay(errorHandler,
validationSettings.unexpectedJsonElementSeverity, () ->
@@ -94,7 +91,7 @@ public class RowSetJSONStreaming<E> extends
IteratorSlotted<Binding> implements
// Experiments with an decoder/encoder that returned Bindings directly
without
// wrapping them as RsJsonEltDft did not show a significant
performance difference
// that would justify supporting alternative encoder/decoder pairs.
- RsJsonEltEncoder<RsJsonEltDft> eltEncoder = new
RsJsonEltEncoderDft(labelMap, null, unexpectedJsonHandler);
+ RsJsonEltEncoder<RsJsonEltDft> eltEncoder = new
RsJsonEltEncoderDft(labelMap, null, unexpectedJsonHandler, errorHandler);
RsJsonEltDecoder<RsJsonEltDft> eltDecoder =
RsJsonEltDecoderDft.INSTANCE;
IteratorRsJSON<RsJsonEltDft> eltIt = new IteratorRsJSON<>(gson,
reader, eltEncoder);
@@ -335,37 +332,60 @@ public class RowSetJSONStreaming<E> extends
IteratorSlotted<Binding> implements
/* Parsing (gson-based) ------------------------------------------------ */
+ private static Type stringListType = new TypeToken<List<String>>()
{}.getType();
/** Parse the vars element from head - may return null */
- static List<Var> parseHeadVars(Gson gson, JsonReader reader) throws
IOException {
+ static List<Var> parseHeadVars(Gson gson, JsonReader reader, ErrorHandler
errorHandler) throws IOException {
List<Var> result = null;
- Type stringListType = new TypeToken<List<String>>() {}.getType();
- JsonObject headJson = gson.fromJson(reader, JsonObject.class);
+ JsonObject headJson = fromJsonReader(gson, reader, jsonObjectToken,
errorHandler);
JsonElement varsJson = headJson.get(kVars);
if (varsJson != null) {
- List<String> varNames = gson.fromJson(varsJson, stringListType);
+ // A list ending in "," (bad JSON) cause a java null at the end of
the array.
+ List<String> varNames = fromGsonElement(gson, varsJson,
stringListType, errorHandler);
+ // GSON in lenient mode parses trailing comma and returns a java
null.
+ if ( ! varNames.isEmpty() && varNames.getLast() == null ) {
+ errorHandler.warning("Bad JSON: \"vars\" array ends in a
comma", -1, -1);
+ //throw new ResultSetException("Bad JSON: Array ends in a
comma");
+ varNames.removeLast();
+ }
result = Var.varList(varNames);
}
return result;
}
+ static TypeToken<JsonObject> jsonObjectToken =
TypeToken.get(JsonObject.class);
static Binding parseBinding(
Gson gson, JsonReader reader, LabelToNode labelMap,
- Function<JsonObject, Node> onUnknownRdfTermType) throws
IOException {
- JsonObject obj = gson.fromJson(reader, JsonObject.class);
-
+ Function<JsonObject, Node> onUnknownRdfTermType,
+ ErrorHandler errorHandler) throws IOException {
+ JsonObject obj = fromJsonReader(gson, reader, jsonObjectToken,
errorHandler);
BindingBuilder bb = BindingFactory.builder();
-
for (Entry<String, JsonElement> e : obj.entrySet()) {
Var v = Var.alloc(e.getKey());
JsonElement nodeElt = e.getValue();
-
Node node = parseOneTerm(nodeElt, labelMap, onUnknownRdfTermType);
bb.add(v, node);
}
-
return bb.build();
}
+ static JsonObject fromJsonReader(Gson gson, JsonReader reader,
TypeToken<JsonObject> typeOfT, ErrorHandler errorHandler) {
+ try {
+ return gson.fromJson(reader, typeOfT);
+ } catch (JsonParseException ex) {
+ errorHandler.fatal("JSON Syntax error: "+ex.getMessage(), -1, -1);
+ return null;
+ }
+ }
+
+ static <T> T fromGsonElement(Gson gson, JsonElement jsonElement, Type
type, ErrorHandler errorHandler) {
+ try {
+ return gson.fromJson(jsonElement, type);
+ } catch (JsonParseException ex) {
+ errorHandler.fatal("JSON Syntax error: "+ex.getMessage(), -1, -1);
+ return null;
+ }
+ }
+
static Node parseOneTerm(JsonElement jsonElt, LabelToNode labelMap,
Function<JsonObject, Node> onUnknownRdfTermType) {
if (jsonElt == null) {
@@ -557,19 +577,23 @@ public class RowSetJSONStreaming<E> extends
IteratorSlotted<Binding> implements
protected LabelToNode labelMap;
protected Function<JsonObject, Node> unknownRdfTermTypeHandler;
protected UnexpectedJsonEltHandler unexpectedJsonHandler;
+ protected final ErrorHandler errorHandler;
public RsJsonEltEncoderDft(LabelToNode labelMap,
Function<JsonObject, Node> unknownRdfTermTypeHandler,
- UnexpectedJsonEltHandler unexpectedJsonHandler) {
+ UnexpectedJsonEltHandler unexpectedJsonHandler,
+ ErrorHandler errorHandler
+ ) {
super();
this.labelMap = labelMap;
this.unknownRdfTermTypeHandler = unknownRdfTermTypeHandler;
this.unexpectedJsonHandler = unexpectedJsonHandler;
+ this.errorHandler = errorHandler;
}
@Override
public RsJsonEltDft newHeadElt(Gson gson, JsonReader reader) throws
IOException {
- List<Var> vars = parseHeadVars(gson, reader);
+ List<Var> vars = parseHeadVars(gson, reader, errorHandler);
return new RsJsonEltDft(vars);
}
@@ -581,7 +605,7 @@ public class RowSetJSONStreaming<E> extends
IteratorSlotted<Binding> implements
@Override
public RsJsonEltDft newBindingElt(Gson gson, JsonReader reader) throws
IOException {
- Binding binding = parseBinding(gson, reader, labelMap,
unknownRdfTermTypeHandler);
+ Binding binding = parseBinding(gson, reader, labelMap,
unknownRdfTermTypeHandler, errorHandler);
return new RsJsonEltDft(binding);
}
@@ -592,7 +616,7 @@ public class RowSetJSONStreaming<E> extends
IteratorSlotted<Binding> implements
@Override
public RsJsonEltDft newUnknownElt(Gson gson, JsonReader reader) throws
IOException {
- JsonElement jsonElement = unexpectedJsonHandler == null
+ JsonElement jsonElement = (unexpectedJsonHandler == null)
? null
: unexpectedJsonHandler.apply(gson, reader);
return new RsJsonEltDft(jsonElement);
diff --git
a/jena-arq/src/test/java/org/apache/jena/riot/rowset/rw/TestRowSetReader.java
b/jena-arq/src/test/java/org/apache/jena/riot/rowset/rw/TestRowSetReader.java
index 04606860d7..286fce7e2d 100644
---
a/jena-arq/src/test/java/org/apache/jena/riot/rowset/rw/TestRowSetReader.java
+++
b/jena-arq/src/test/java/org/apache/jena/riot/rowset/rw/TestRowSetReader.java
@@ -23,30 +23,38 @@ package org.apache.jena.riot.rowset.rw;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import org.junit.jupiter.api.Test;
+
+import org.apache.commons.lang3.Strings;
import org.apache.jena.atlas.io.IOX;
import org.apache.jena.atlas.lib.Bytes;
+import org.apache.jena.atlas.logging.LogCtl;
import org.apache.jena.query.ARQ;
import org.apache.jena.riot.Lang;
import org.apache.jena.riot.resultset.ResultSetLang;
import org.apache.jena.riot.rowset.RowSetReader;
import org.apache.jena.sparql.exec.RowSet;
import org.apache.jena.sparql.exec.RowSetOps;
+import org.apache.jena.sparql.resultset.ResultSetException;
import org.apache.jena.sys.JenaSystem;
-import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Addition tests for result readers.
- * The SPARQl test suite cover most usage. This class adds tests
+ * The SPARQL test suite cover most usage. This class adds tests.
*/
public class TestRowSetReader {
static { JenaSystem.init(); }
- // Check "abc"^^rdf:langString and "abc"^^rdf:dirLangString and "
+ // Check "abc"^^rdf:langString and "abc"^^rdf:dirLangString - i.e.
incomplete forms.
@Test public void resultSet_json_1() {
String r = """
{
@@ -71,6 +79,49 @@ public class TestRowSetReader {
assertEquals(2, RowSetOps.count(rowset));
}
+ // Check error handling of bad JSON
+ @Test public void resultSet_json_bad_vars() {
+ // Trailing comma in "vars" array. This is only a warning.
+ String r = """
+ { "head": { "vars": [ "x", "y" , ] } ,
+ "results": {
+ "bindings": [
+ {
+ "x": { "type": "literal" , "value": "A" } ,
+ "y": { "type": "literal" , "value": "B" }
+ }
+ ]
+ }
+ }
+ """;
+ Logger log = LoggerFactory.getLogger(RowSetReaderJSONStreaming.class);
+ LogCtl.withLevel(log, "ERROR", ()->{
+ RowSet rowset = read(r, ResultSetLang.RS_JSON);
+ assertNotNull(rowset);
+ assertEquals(1, RowSetOps.count(rowset));
+ });
+ }
+
+ // Check error handling of bad JSON
+ @Test public void resultSet_json_bad_bindings() {
+ // Trailing comma in "bindings" array. This is an error. It might be
file truncation.
+ String r = """
+ { "head": { "vars": [ "x", "y" ] } ,
+ "results": {
+ "bindings": [
+ {
+ "x": { "type": "literal" , "value": "A" } ,
+ "y": { "type": "literal" , "value": "B" } ,
+ }
+ ]
+ }
+ }
+ """;
+ ResultSetException ex = assertThrows(ResultSetException.class, ()->
read(r, ResultSetLang.RS_JSON));
+ String msg = ex.getMessage();
+ assertTrue(Strings.CI.containsAny(msg, "JSON Syntax error:"));
+ }
+
@Test public void resultSet_xml_1() {
String r = """
<?xml version="1.0"?>
diff --git a/jena-cmds/src/main/java/arq/rset.java
b/jena-cmds/src/main/java/arq/rset.java
index 3efd228ed1..780e2ea73b 100644
--- a/jena-cmds/src/main/java/arq/rset.java
+++ b/jena-cmds/src/main/java/arq/rset.java
@@ -22,6 +22,7 @@
package arq;
import org.apache.jena.query.ResultSet;
+import org.apache.jena.sparql.resultset.ResultSetException;
import org.apache.jena.sparql.util.QueryExecUtils;
import arq.cmdline.CmdARQ;
import arq.cmdline.ModResultsIn;
@@ -57,8 +58,13 @@ public class rset extends CmdARQ {
@Override
protected void exec() {
- ResultSet rs = modInput.getResultSet();
- QueryExecUtils.outputResultSet(rs, null, modOutput.getResultsFormat(),
System.out);
+ try {
+ ResultSet rs = modInput.getResultSet();
+ QueryExecUtils.outputResultSet(rs, null,
modOutput.getResultsFormat(), System.out);
+ } catch (ResultSetException ex) {
+ System.err.println(ex.getMessage());
+ }
+
}
@Override