diff --git a/src/main/java/org/embulk/util/json/CapturingDirectMemberNameList.java b/src/main/java/org/embulk/util/json/CapturingDirectMemberNameList.java index 86fd799..c14e356 100644 --- a/src/main/java/org/embulk/util/json/CapturingDirectMemberNameList.java +++ b/src/main/java/org/embulk/util/json/CapturingDirectMemberNameList.java @@ -47,14 +47,9 @@ static CapturingDirectMemberNameList of(final List memberNames) { JsonValue[] captureFromParser( final JsonParser jacksonParser, final InternalJsonValueReader valueReader) throws IOException { + final JsonToken firstToken; try { - final JsonToken firstToken = jacksonParser.nextToken(); - if (firstToken == null) { - return null; - } - if (firstToken != JsonToken.START_OBJECT) { - throw new JsonParseException("Failed to parse JSON: Expected JSON Object, but " + firstToken.toString()); - } + firstToken = jacksonParser.nextToken(); } catch (final com.fasterxml.jackson.core.JsonParseException ex) { throw new JsonParseException("Failed to parse JSON", ex); } catch (final IOException ex) { @@ -65,6 +60,15 @@ JsonValue[] captureFromParser( throw new JsonParseException("Failed to parse JSON", ex); } + if (firstToken == null) { + return null; + } + // The value must be always a JSON object regardless of |onlyJsonObjects| when capturing by direct member names. + if (firstToken != JsonToken.START_OBJECT) { + valueReader.skipJsonValue(jacksonParser, firstToken); + throw new InvalidJsonValueException("Expected JSON Object, but " + firstToken.toString()); + } + final JsonValue[] values = new JsonValue[this.size]; for (int i = 0; i < values.length; i++) { values[i] = null; diff --git a/src/main/java/org/embulk/util/json/CapturingJsonPointerList.java b/src/main/java/org/embulk/util/json/CapturingJsonPointerList.java index 07bc8e9..2912e61 100644 --- a/src/main/java/org/embulk/util/json/CapturingJsonPointerList.java +++ b/src/main/java/org/embulk/util/json/CapturingJsonPointerList.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.core.JsonToken; import java.io.IOException; import java.util.List; import org.embulk.spi.json.JsonValue; @@ -71,6 +72,7 @@ static CapturingJsonPointerList of(final List pointers) { * @param parser {@link com.fasterxml.jackson.core.JsonParser} to read from * @return an array of captured JSON values * @throws IOException when failing to read + * @throws InvalidJsonValueException if the JSON value is not a JSON object while it is configured to accept only JSON objects */ @Override JsonValue[] captureFromParser(final JsonParser parser, final InternalJsonValueReader valueReader) throws IOException { @@ -89,6 +91,10 @@ JsonValue[] captureFromParser(final JsonParser parser, final InternalJsonValueRe ; } + if (valueReader.isOnlyJsonObjects() && capturer.firstToken() != JsonToken.START_OBJECT) { + throw new InvalidJsonValueException("Expected JSON Object, but " + capturer.firstToken()); + } + return capturer.peekValues(); } diff --git a/src/main/java/org/embulk/util/json/CapturingPointerToRoot.java b/src/main/java/org/embulk/util/json/CapturingPointerToRoot.java index 6cd729b..f14eb42 100644 --- a/src/main/java/org/embulk/util/json/CapturingPointerToRoot.java +++ b/src/main/java/org/embulk/util/json/CapturingPointerToRoot.java @@ -32,6 +32,9 @@ JsonValue[] captureFromParser( if (value == null) { return null; } + if (valueReader.isOnlyJsonObjects() && !value.isJsonObject()) { + throw new InvalidJsonValueException("Expected JSON Object, but " + value.getEntityType().toString()); + } final JsonValue[] values = new JsonValue[1]; values[0] = value; diff --git a/src/main/java/org/embulk/util/json/InternalJsonValueReader.java b/src/main/java/org/embulk/util/json/InternalJsonValueReader.java index dd11b98..a3a0c13 100644 --- a/src/main/java/org/embulk/util/json/InternalJsonValueReader.java +++ b/src/main/java/org/embulk/util/json/InternalJsonValueReader.java @@ -32,16 +32,22 @@ final class InternalJsonValueReader { InternalJsonValueReader( + final boolean isOnlyJsonObjects, final boolean hasLiteralsWithNumbers, final boolean hasFallbacksForUnparsableNumbers, final double defaultDouble, final long defaultLong) { + this.isOnlyJsonObjects = isOnlyJsonObjects; this.hasLiteralsWithNumbers = hasLiteralsWithNumbers; this.hasFallbacksForUnparsableNumbers = hasFallbacksForUnparsableNumbers; this.defaultDouble = defaultDouble; this.defaultLong = defaultLong; } + boolean isOnlyJsonObjects() { + return this.isOnlyJsonObjects; + } + boolean hasLiteralsWithNumbers() { return this.hasLiteralsWithNumbers; } @@ -203,7 +209,7 @@ private JsonValue readJsonValue(final JsonParser jacksonParser, final JsonToken } } - private void skipJsonValue(final JsonParser jacksonParser, final JsonToken token) throws IOException { + void skipJsonValue(final JsonParser jacksonParser, final JsonToken token) throws IOException { switch (token) { case VALUE_NULL: case VALUE_TRUE: @@ -295,6 +301,7 @@ private long getLongValue(final JsonParser jacksonParser) throws IOException { } } + private final boolean isOnlyJsonObjects; private final boolean hasLiteralsWithNumbers; private final boolean hasFallbacksForUnparsableNumbers; private final double defaultDouble; diff --git a/src/main/java/org/embulk/util/json/InvalidJsonValueException.java b/src/main/java/org/embulk/util/json/InvalidJsonValueException.java new file mode 100644 index 0000000..e38777b --- /dev/null +++ b/src/main/java/org/embulk/util/json/InvalidJsonValueException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 The Embulk project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.embulk.util.json; + +import org.embulk.spi.DataException; + +/** + * Represents an Exception for a JSON value that is invalid against the requirement. + */ +public class InvalidJsonValueException extends DataException { + /** + * Constructs a new {@link InvalidJsonValueException} with the specified detail message. + * + * @param message the detail message + */ + public InvalidJsonValueException(final String message) { + super(message); + } + + /** + * Constructs a new {@link InvalidJsonValueException} with the specified detail message and cause. + * + * @param message the detail message + * @param cause the cause + */ + public InvalidJsonValueException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/embulk/util/json/JsonValueParser.java b/src/main/java/org/embulk/util/json/JsonValueParser.java index 06a502f..399aea4 100644 --- a/src/main/java/org/embulk/util/json/JsonValueParser.java +++ b/src/main/java/org/embulk/util/json/JsonValueParser.java @@ -34,6 +34,7 @@ public final class JsonValueParser implements Closeable { private JsonValueParser( final com.fasterxml.jackson.core.JsonParser jacksonParser, + final boolean onlyJsonObjects, final int depthToFlattenJsonArrays, final boolean hasLiteralsWithNumbers, final boolean hasFallbacksForUnparsableNumbers, @@ -41,7 +42,8 @@ private JsonValueParser( final long defaultLong) { this.jacksonParser = Objects.requireNonNull(jacksonParser); this.valueReader = new InternalJsonValueReader( - hasLiteralsWithNumbers, hasFallbacksForUnparsableNumbers, defaultDouble, defaultLong); + onlyJsonObjects, hasLiteralsWithNumbers, hasFallbacksForUnparsableNumbers, defaultDouble, defaultLong); + this.onlyJsonObjects = onlyJsonObjects; this.depthToFlattenJsonArrays = depthToFlattenJsonArrays; this.hasLiteralsWithNumbers = hasLiteralsWithNumbers; this.hasFallbacksForUnparsableNumbers = hasFallbacksForUnparsableNumbers; @@ -56,6 +58,7 @@ public static final class Builder { Builder(final JsonFactory factory) { this.factory = Objects.requireNonNull(factory); this.root = null; + this.onlyJsonObjects = false; this.depthToFlattenJsonArrays = 0; this.hasLiteralsWithNumbers = false; this.hasFallbacksForUnparsableNumbers = false; @@ -87,6 +90,11 @@ public Builder root(final String root) { return this; } + public Builder onlyJsonObjects() { + this.onlyJsonObjects = true; + return this; + } + /** * Sets the depth to flatten JSON Arrays to parse. * @@ -140,6 +148,7 @@ public Builder fallbackForUnparsableNumbers(final double defaultDouble, final lo public JsonValueParser build(final String json) throws IOException { return new JsonValueParser( buildJacksonParser(json), + this.onlyJsonObjects, this.depthToFlattenJsonArrays, this.hasLiteralsWithNumbers, this.hasFallbacksForUnparsableNumbers, @@ -156,6 +165,7 @@ public JsonValueParser build(final String json) throws IOException { public JsonValueParser build(final InputStream jsonStream) throws IOException { return new JsonValueParser( buildJacksonParser(jsonStream), + this.onlyJsonObjects, this.depthToFlattenJsonArrays, this.hasLiteralsWithNumbers, this.hasFallbacksForUnparsableNumbers, @@ -195,6 +205,7 @@ private com.fasterxml.jackson.core.JsonParser extendJacksonParser(final com.fast private final JsonFactory factory; private JsonPointer root; + private boolean onlyJsonObjects; private int depthToFlattenJsonArrays; private boolean hasLiteralsWithNumbers; private boolean hasFallbacksForUnparsableNumbers; @@ -240,9 +251,14 @@ public static Builder builder(final JsonFactory jsonFactory) { * @return the JSON value, or {@code null} if the parser reaches at the end of input in the beginning * @throws IOException if failing to read JSON * @throws JsonParseException if failing to parse JSON + * @throws InvalidJsonValueException if the JSON value is not a JSON object while it is configured to accept only JSON objects */ public JsonValue readJsonValue() throws IOException { - return this.valueReader.read(this.jacksonParser); + final JsonValue value = this.valueReader.read(this.jacksonParser); + if (this.onlyJsonObjects && !value.isJsonObject()) { + throw new InvalidJsonValueException("Expected JSON Object, but " + value.getEntityType().toString()); + } + return value; } /** @@ -251,6 +267,7 @@ public JsonValue readJsonValue() throws IOException { * @return an array of the captured JSON values, or {@code null} if the parser reaches at the end of input in the beginning * @throws IOException if failing to read JSON * @throws JsonParseException if failing to parse JSON + * @throws InvalidJsonValueException if the JSON value is not a JSON object while it is configured to accept only JSON objects */ public JsonValue[] captureJsonValues(final CapturingPointers capturingPointers) throws IOException { return capturingPointers.captureFromParser(this.jacksonParser, this.valueReader); @@ -269,6 +286,7 @@ public final void close() throws IOException { private final com.fasterxml.jackson.core.JsonParser jacksonParser; private final InternalJsonValueReader valueReader; + private final boolean onlyJsonObjects; private final int depthToFlattenJsonArrays; private final boolean hasLiteralsWithNumbers; private final boolean hasFallbacksForUnparsableNumbers; diff --git a/src/main/java/org/embulk/util/json/TreeBasedCapturer.java b/src/main/java/org/embulk/util/json/TreeBasedCapturer.java index 353c4c7..bd9d5dd 100644 --- a/src/main/java/org/embulk/util/json/TreeBasedCapturer.java +++ b/src/main/java/org/embulk/util/json/TreeBasedCapturer.java @@ -56,6 +56,7 @@ class TreeBasedCapturer { this.builderStack = new ArrayDeque<>(); this.hasFinished = false; + this.firstToken = null; this.values = new JsonValue[size]; for (int i = 0; i < this.values.length; i++) { @@ -98,7 +99,9 @@ boolean next() throws IOException { // Deepen the pointer stack when the token is a scalar value, START_ARRAY, or START_OBJECT. if (token.isScalarValue() || token.isStructStart()) { - if (!this.parsingStack.isEmpty()) { + if (this.parsingStack.isEmpty()) { + this.firstToken = token; + } else { final ParsingContext context = this.parsingStack.getFirst(); if (context.isObject()) { final String propertyName = context.getPropertyName(); @@ -252,6 +255,10 @@ JsonValue[] peekValues() { return this.values; } + JsonToken firstToken() { + return this.firstToken; + } + private double getDoubleValue() throws IOException { try { return this.parser.getDoubleValue(); @@ -452,4 +459,5 @@ private static Map.Entry[] toArray(final ArrayList