diff --git a/impl/src/main/java/org/eclipse/parsson/JsonParserImpl.java b/impl/src/main/java/org/eclipse/parsson/JsonParserImpl.java index b60c7fb..676746b 100644 --- a/impl/src/main/java/org/eclipse/parsson/JsonParserImpl.java +++ b/impl/src/main/java/org/eclipse/parsson/JsonParserImpl.java @@ -22,15 +22,10 @@ import java.io.Reader; import java.math.BigDecimal; import java.nio.charset.Charset; -import java.util.AbstractMap; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.function.Consumer; import java.util.stream.Stream; -import java.util.stream.StreamSupport; import jakarta.json.JsonArray; import jakarta.json.JsonArrayBuilder; @@ -60,7 +55,7 @@ * @author Jitendra Kotamraju * @author Kin-man Chung */ -public class JsonParserImpl implements JsonParser { +class JsonParserImpl implements JsonParser { private Context currentContext = new NoneContext(); private Event currentEvent; @@ -71,20 +66,24 @@ public class JsonParserImpl implements JsonParser { private final JsonContext jsonContext; - public JsonParserImpl(Reader reader, JsonContext jsonContext) { + private final JsonParserStreamCreator streamCreator = new JsonParserStreamCreator(this, true, () -> currentEvent, + () -> currentContext instanceof NoneContext); + + + JsonParserImpl(Reader reader, JsonContext jsonContext) { this.jsonContext = jsonContext; stack = new Stack(jsonContext.depthLimit()); this.tokenizer = new JsonTokenizer(reader, jsonContext); } - public JsonParserImpl(InputStream in, JsonContext jsonContext) { + JsonParserImpl(InputStream in, JsonContext jsonContext) { this.jsonContext = jsonContext; stack = new Stack(jsonContext.depthLimit()); UnicodeDetectingInputStream uin = new UnicodeDetectingInputStream(in); this.tokenizer = new JsonTokenizer(new InputStreamReader(uin, uin.getCharset()), jsonContext); } - public JsonParserImpl(InputStream in, Charset encoding, JsonContext jsonContext) { + JsonParserImpl(InputStream in, Charset encoding, JsonContext jsonContext) { this.jsonContext = jsonContext; stack = new Stack(jsonContext.depthLimit()); this.tokenizer = new JsonTokenizer(new InputStreamReader(in, encoding), jsonContext); @@ -194,103 +193,17 @@ public JsonValue getValue() { @Override public Stream getArrayStream() { - if (currentEvent != Event.START_ARRAY) { - throw new IllegalStateException( - JsonMessages.PARSER_GETARRAY_ERR(currentEvent)); - } - Spliterator spliterator = - new Spliterators.AbstractSpliterator(Long.MAX_VALUE, Spliterator.ORDERED) { - @Override - public Spliterator trySplit() { - return null; - } - - @Override - public boolean tryAdvance(Consumer action) { - if (action == null) { - throw new NullPointerException(); - } - if (!hasNext()) { - return false; - } - if (next() == JsonParser.Event.END_ARRAY) { - return false; - } - action.accept(getValue()); - return true; - } - }; - return StreamSupport.stream(spliterator, false); + return streamCreator.getArrayStream(); } @Override public Stream> getObjectStream() { - if (currentEvent != Event.START_OBJECT) { - throw new IllegalStateException( - JsonMessages.PARSER_GETOBJECT_ERR(currentEvent)); - } - Spliterator> spliterator = - new Spliterators.AbstractSpliterator>(Long.MAX_VALUE, Spliterator.ORDERED) { - @Override - public Spliterator> trySplit() { - return null; - } - - @Override - public boolean tryAdvance(Consumer> action) { - if (action == null) { - throw new NullPointerException(); - } - if (!hasNext()) { - return false; - } - JsonParser.Event e = next(); - if (e == JsonParser.Event.END_OBJECT) { - return false; - } - if (e != JsonParser.Event.KEY_NAME) { - throw new JsonException(JsonMessages.INTERNAL_ERROR()); - } - String key = getString(); - if (!hasNext()) { - throw new JsonException(JsonMessages.INTERNAL_ERROR()); - } - next(); - JsonValue value = getValue(); - action.accept(new AbstractMap.SimpleImmutableEntry<>(key, value)); - return true; - } - }; - return StreamSupport.stream(spliterator, false); + return streamCreator.getObjectStream(); } @Override public Stream getValueStream() { - if (! (currentContext instanceof NoneContext)) { - throw new IllegalStateException( - JsonMessages.PARSER_GETVALUESTREAM_ERR()); - } - Spliterator spliterator = - new Spliterators.AbstractSpliterator(Long.MAX_VALUE, Spliterator.ORDERED) { - @Override - public Spliterator trySplit() { - return null; - } - - @Override - public boolean tryAdvance(Consumer action) { - if (action == null) { - throw new NullPointerException(); - } - if (!hasNext()) { - return false; - } - next(); - action.accept(getValue()); - return true; - } - }; - return StreamSupport.stream(spliterator, false); + return streamCreator.getValueStream(); } @Override diff --git a/impl/src/main/java/org/eclipse/parsson/JsonParserStreamCreator.java b/impl/src/main/java/org/eclipse/parsson/JsonParserStreamCreator.java new file mode 100644 index 0000000..d38e4dc --- /dev/null +++ b/impl/src/main/java/org/eclipse/parsson/JsonParserStreamCreator.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.eclipse.parsson; + +import java.util.AbstractMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import jakarta.json.JsonException; +import jakarta.json.JsonValue; +import jakarta.json.stream.JsonParser; +import jakarta.json.stream.JsonParser.Event; + +class JsonParserStreamCreator { + + private final JsonParser parser; + private final boolean nextBeforeCreationOfValueStream; + private final Supplier currenEventSupplier; + private final Supplier canProduceValueStream; + + JsonParserStreamCreator(JsonParser parser, boolean nextBeforeCreationOfValueStream, Supplier currenEventSupplier, + Supplier canProduceValueStream) { + + this.parser = Objects.requireNonNull(parser); + this.nextBeforeCreationOfValueStream = nextBeforeCreationOfValueStream; + this.currenEventSupplier = Objects.requireNonNull(currenEventSupplier); + this.canProduceValueStream = Objects.requireNonNull(canProduceValueStream); + } + + /** + * Creates new {@link Stream} from values from {@link Supplier}. The stream delivers the values as long as supplier delivers non-null values + * + * @param supplier supplier of the values + * @param type of the values which are delivered by the supplier and the stream + * @return stream of values from given supplier + */ + private static Stream streamFromSupplier(Supplier supplier) { + return StreamCreator.iterate(Objects.requireNonNull(supplier).get(), Objects::nonNull, value -> supplier.get()); + } + + public Stream getArrayStream() { + if (currenEventSupplier.get() == Event.START_ARRAY) { + return streamFromSupplier(() -> (parser.hasNext() && parser.next() != Event.END_ARRAY) ? parser.getValue() : null); + } else { + throw new IllegalStateException(JsonMessages.PARSER_GETARRAY_ERR(parser.currentEvent())); + } + } + + public Stream> getObjectStream() { + if (currenEventSupplier.get() == Event.START_OBJECT) { + return streamFromSupplier(() -> { + if (!parser.hasNext()) { + return null; + } + Event e = parser.next(); + if (e == Event.END_OBJECT) { + return null; + } else if (e != Event.KEY_NAME) { + throw new JsonException(JsonMessages.INTERNAL_ERROR()); + } else { + String key = parser.getString(); + if (!parser.hasNext()) { + throw new JsonException(JsonMessages.INTERNAL_ERROR()); + } else { + parser.next(); + return new AbstractMap.SimpleImmutableEntry<>(key, parser.getValue()); + } + } + }); + } else { + throw new IllegalStateException(JsonMessages.PARSER_GETOBJECT_ERR(parser.currentEvent())); + } + } + + public Stream getValueStream() { + if (canProduceValueStream.get()) { + if (nextBeforeCreationOfValueStream) { + parser.next(); + } + + return streamFromSupplier(() -> { + if (parser.hasNext()) { + return parser.getValue(); + } else { + return null; + } + }); + } else { + throw new IllegalStateException(JsonMessages.PARSER_GETVALUESTREAM_ERR()); + } + } +} diff --git a/impl/src/main/java/org/eclipse/parsson/JsonStructureParser.java b/impl/src/main/java/org/eclipse/parsson/JsonStructureParser.java index 3fed7c0..d16ef38 100644 --- a/impl/src/main/java/org/eclipse/parsson/JsonStructureParser.java +++ b/impl/src/main/java/org/eclipse/parsson/JsonStructureParser.java @@ -25,6 +25,7 @@ import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; +import java.util.stream.Stream; /** * {@link JsonParser} implementation on top of JsonArray/JsonObject @@ -33,9 +34,13 @@ */ class JsonStructureParser implements JsonParser { - private Scope current; + private Scope current; private Event state; - private final Deque scopeStack = new ArrayDeque<>(); + private final Deque> scopeStack = new ArrayDeque<>(); + + //JsonParserImpl delivers the whole object - so we have to call next() before creation of the stream + private final JsonParserStreamCreator streamCreator = new JsonParserStreamCreator(this, true, () -> state, scopeStack::isEmpty); + JsonStructureParser(JsonArray array) { current = new ArrayScope(array); @@ -96,6 +101,55 @@ public JsonLocation getLocation() { return JsonLocationImpl.UNKNOWN; } + @Override + public JsonObject getObject() { + if (state != Event.START_OBJECT) { + throw new IllegalStateException( + JsonMessages.PARSER_GETOBJECT_ERR(state)); + } + if (current == null) { + throw new NoSuchElementException(JsonMessages.INTERNAL_ERROR()); + } + state = Event.END_OBJECT; + return current.getJsonValue().asJsonObject(); + + } + + @Override + public JsonValue getValue() { + if (current == null) { + throw new IllegalStateException(JsonMessages.INTERNAL_ERROR()); + } + + switch (state) { + case START_OBJECT: + return getObject(); + case START_ARRAY: + return getArray(); + case KEY_NAME: + return Json.createValue(((ObjectScope)current).key); + case END_OBJECT: + case END_ARRAY: + throw new IllegalStateException(JsonMessages.INTERNAL_ERROR()); + default: + return current.getJsonValue(); + } + } + + @Override + public JsonArray getArray() { + if (state != Event.START_ARRAY) { + throw new IllegalStateException( + JsonMessages.PARSER_GETARRAY_ERR(state)); + } + Scope topOfTheStack = scopeStack.isEmpty() ? current : scopeStack.pop(); + if (topOfTheStack == null) { + throw new NoSuchElementException(JsonMessages.INTERNAL_ERROR()); + } + state = Event.END_ARRAY; + return topOfTheStack.getJsonValue().asJsonArray(); + } + @Override public boolean hasNext() { return !((state == Event.END_OBJECT || state == Event.END_ARRAY) && scopeStack.isEmpty()); @@ -153,59 +207,37 @@ public void close() { // no-op } + @Override + public Event currentEvent() { + return state; + } + + @Override + public Stream getArrayStream() { + return streamCreator.getArrayStream(); + } + + @Override + public Stream> getObjectStream() { + return streamCreator.getObjectStream(); + } + + @Override + public Stream getValueStream() { + return streamCreator.getValueStream(); + } + @Override public void skipObject() { if (current instanceof ObjectScope) { - int depth = 1; - do { - if (state == Event.KEY_NAME) { - state = getState(current.getJsonValue()); - switch (state) { - case START_OBJECT: - depth++; - break; - case END_OBJECT: - depth--; - break; - default: - //no-op - } - } else { - if (current.hasNext()) { - current.next(); - state = Event.KEY_NAME; - } else { - state = Event.END_OBJECT; - depth--; - } - } - } while (state != Event.END_OBJECT && depth > 0); + state = Event.END_OBJECT; } } @Override public void skipArray() { if (current instanceof ArrayScope) { - int depth = 1; - do { - if (current.hasNext()) { - current.next(); - state = getState(current.getJsonValue()); - switch (state) { - case START_ARRAY: - depth++; - break; - case END_ARRAY: - depth--; - break; - default: - //no-op - } - } else { - state = Event.END_ARRAY; - depth--; - } - } while (!(state == Event.END_ARRAY && depth == 0)); + state = Event.END_ARRAY; } } @@ -230,10 +262,13 @@ private static Event getState(JsonValue value) { } } - private static abstract class Scope implements Iterator { + private static abstract class Scope implements Iterator { + @Override + public final void remove() {throw new UnsupportedOperationException(); } + abstract JsonValue getJsonValue(); - static Scope createScope(JsonValue value) { + static Scope createScope(JsonValue value) { if (value instanceof JsonArray) { return new ArrayScope((JsonArray)value); } else if (value instanceof JsonObject) { @@ -243,7 +278,7 @@ static Scope createScope(JsonValue value) { } } - private static class ArrayScope extends Scope { + private static class ArrayScope extends Scope { private final Iterator it; private JsonValue value; @@ -262,11 +297,6 @@ public JsonValue next() { return value; } - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - @Override JsonValue getJsonValue() { return value; @@ -274,12 +304,14 @@ JsonValue getJsonValue() { } - private static class ObjectScope extends Scope { + private static class ObjectScope extends Scope> { + private final JsonObject object; private final Iterator> it; private JsonValue value; private String key; ObjectScope(JsonObject object) { + this.object = object; this.it = object.entrySet().iterator(); } @@ -296,14 +328,9 @@ public Map.Entry next() { return next; } - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - @Override JsonValue getJsonValue() { - return value; + return value == null ? object : value; } } diff --git a/impl/src/main/java/org/eclipse/parsson/StreamCreator.java b/impl/src/main/java/org/eclipse/parsson/StreamCreator.java new file mode 100644 index 0000000..cf391d1 --- /dev/null +++ b/impl/src/main/java/org/eclipse/parsson/StreamCreator.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.eclipse.parsson; + +import java.util.Objects; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * Copy of public static Stream Stream#iterate(T seed, Predicate hasNext, UnaryOperator next) + * because the last is since Java 9 + */ +class StreamCreator { + /** + * Returns a sequential ordered {@code Stream} produced by iterative + * application of the given {@code next} function to an initial element, + * conditioned on satisfying the given {@code hasNext} predicate. The + * stream terminates as soon as the {@code hasNext} predicate returns false. + * + *

{@code Stream.iterate} should produce the same sequence of elements as + * produced by the corresponding for-loop: + *

{@code
+     *     for (T index=seed; hasNext.test(index); index = next.apply(index)) {
+     *         ...
+     *     }
+     * }
+ * + *

The resulting sequence may be empty if the {@code hasNext} predicate + * does not hold on the seed value. Otherwise the first element will be the + * supplied {@code seed} value, the next element (if present) will be the + * result of applying the {@code next} function to the {@code seed} value, + * and so on iteratively until the {@code hasNext} predicate indicates that + * the stream should terminate. + * + *

The action of applying the {@code hasNext} predicate to an element + * happens-before + * the action of applying the {@code next} function to that element. The + * action of applying the {@code next} function for one element + * happens-before the action of applying the {@code hasNext} + * predicate for subsequent elements. For any given element an action may + * be performed in whatever thread the library chooses. + * + * @param the type of stream elements + * @param seed the initial element + * @param hasNext a predicate to apply to elements to determine when the + * stream must terminate. + * @param next a function to be applied to the previous element to produce + * a new element + * @return a new sequential {@code Stream} + * @since 9 + */ + public static Stream iterate(T seed, Predicate hasNext, UnaryOperator next) { + Objects.requireNonNull(next); + Objects.requireNonNull(hasNext); + Spliterator spliterator = new Spliterators.AbstractSpliterator(Long.MAX_VALUE, + Spliterator.ORDERED | Spliterator.IMMUTABLE) { + T prev; + boolean started, finished; + + @Override + public boolean tryAdvance(Consumer action) { + Objects.requireNonNull(action); + if (finished) + return false; + T t; + if (started) + t = next.apply(prev); + else { + t = seed; + started = true; + } + if (!hasNext.test(t)) { + prev = null; + finished = true; + return false; + } + action.accept(prev = t); + return true; + } + + @Override + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action); + if (finished) + return; + finished = true; + T t = started ? next.apply(prev) : seed; + prev = null; + while (hasNext.test(t)) { + action.accept(t); + t = next.apply(t); + } + } + }; + return StreamSupport.stream(spliterator, false); + } +} diff --git a/impl/src/test/java/org/eclipse/parsson/tests/JsonParserTest.java b/impl/src/test/java/org/eclipse/parsson/tests/JsonParserTest.java index 3a36de4..4179893 100644 --- a/impl/src/test/java/org/eclipse/parsson/tests/JsonParserTest.java +++ b/impl/src/test/java/org/eclipse/parsson/tests/JsonParserTest.java @@ -16,10 +16,17 @@ package org.eclipse.parsson.tests; +import static org.eclipse.parsson.JsonParserFixture.testWithCreateParserFromObject; +import static org.eclipse.parsson.JsonParserFixture.testWithCreateParserFromString; +import static org.hamcrest.Matchers.contains; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.hamcrest.MatcherAssert.assertThat; + import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.InputStreamReader; @@ -31,19 +38,32 @@ import jakarta.json.stream.JsonParser; import jakarta.json.stream.JsonParser.Event; import jakarta.json.stream.JsonParserFactory; + import java.math.BigDecimal; +import java.math.BigInteger; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.EnumSet; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.Random; import java.util.Scanner; +import java.util.stream.Collector; +import java.util.stream.Collectors; + import jakarta.json.stream.JsonParsingException; import org.eclipse.parsson.api.BufferPool; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; /** * JsonParser Tests @@ -54,6 +74,22 @@ public class JsonParserTest { static final Charset UTF_32LE = Charset.forName("UTF-32LE"); static final Charset UTF_32BE = Charset.forName("UTF-32BE"); + private static final EnumSet GET_STRING_EVENT_ENUM_SET = + EnumSet.of(JsonParser.Event.KEY_NAME, JsonParser.Event.VALUE_STRING, JsonParser.Event.VALUE_NUMBER); + + private static final EnumSet NOT_GET_VALUE_EVENT_ENUM_SET = EnumSet.of(JsonParser.Event.END_OBJECT, JsonParser.Event.END_ARRAY); + + private static final Collector, ?, ArrayList> MAP_TO_LIST_COLLECTOR = Collector.of(ArrayList::new, + (list, entry) -> { + list.add(entry.getKey()); + list.add(entry.getValue().toString()); + }, + (left, right) -> { + left.addAll(right); + return left; + }, + Collector.Characteristics.IDENTITY_FINISH); + @Test void testReader() { JsonParser reader = Json.createParser( @@ -870,4 +906,631 @@ private void checkExceptionFromNext(String input) { } fail(); } + + @Nested + public class DirectParserTests { + @Test + void testNumbersStructure() { + testWithCreateParserFromObject(Json.createObjectBuilder() + .add("int", 1) + .add("long", 1L) + .add("double", 1d) + .add("BigInteger", BigInteger.TEN) + .add("BigDecimal", BigDecimal.TEN) + .build(), this::testNumbers); + } + + @Test + void testNumbersString() { + testWithCreateParserFromString("{\"int\":1,\"long\":1,\"double\":1.0,\"BigInteger\":10,\"BigDecimal\":10}", this::testNumbers); + } + + private void testNumbers(JsonParser parser) { + parser.next(); + parser.next(); + parser.getString(); + parser.next(); + assertTrue(parser.isIntegralNumber()); + assertEquals(1, parser.getInt()); + + parser.next(); + parser.getString(); + parser.next(); + assertTrue(parser.isIntegralNumber()); + assertEquals(1L, parser.getLong()); + + parser.next(); + parser.getString(); + parser.next(); + assertFalse(parser.isIntegralNumber()); + assertEquals(BigDecimal.valueOf(1d), parser.getBigDecimal()); + + parser.next(); + parser.getString(); + parser.next(); + assertTrue(parser.isIntegralNumber()); + assertEquals(BigDecimal.TEN, parser.getBigDecimal()); + + parser.next(); + parser.getString(); + parser.next(); + assertTrue(parser.isIntegralNumber()); + assertEquals(BigDecimal.TEN, parser.getBigDecimal()); + } + + @Test + void testParser_getStringStructure(){ + testWithCreateParserFromObject(TestData.createFamilyPerson(), this::testParser_getString); + } + + @Test + void testParser_getStringString(){ + testWithCreateParserFromString(TestData.JSON_FAMILY_STRING, this::testParser_getString); + } + + private void testParser_getString(JsonParser parser) { + List values = new ArrayList<>(); + parser.next(); + while (parser.hasNext()) { + Event event = parser.next(); + if (GET_STRING_EVENT_ENUM_SET.contains(event)) { + String strValue = Objects.toString(parser.getString(), "null"); + values.add(strValue); + } + } + + assertThat(values,TestData.FAMILY_MATCHER_WITH_NO_QUOTATION); + } + + @Test + void testParser_getValueStructure(){ + testWithCreateParserFromObject(TestData.createFamilyPerson(), this::testParser_getValue); + } + + @Test + void testParser_getValueString(){ + testWithCreateParserFromString(TestData.JSON_FAMILY_STRING, this::testParser_getValue); + } + + private void testParser_getValue(JsonParser parser) { + List values = new ArrayList<>(); + parser.next(); + while (parser.hasNext()) { + Event event = parser.next(); + if (!NOT_GET_VALUE_EVENT_ENUM_SET.contains(event)) { + String strValue = Objects.toString(parser.getValue(), "null"); + values.add(strValue); + } + } + + assertThat(values, TestData.FAMILY_MATCHER_KEYS_WITH_QUOTATION); + } + + @Test + void testSkipArrayStructure() { + testWithCreateParserFromObject(TestData.createObjectWithArrays(), this::testSkipArray); + } + + @Test + void testSkipArrayString() { + testWithCreateParserFromString(TestData.JSON_OBJECT_WITH_ARRAYS, this::testSkipArray); + } + + private void testSkipArray(JsonParser parser) { + parser.next(); + parser.next(); + parser.getString(); + parser.next(); + parser.skipArray(); + parser.next(); + String key = parser.getString(); + + assertEquals("secondElement", key); + } + + @Test + void testSkipObjectStructure() { + testWithCreateParserFromObject(TestData.createJsonObject(), this::testSkipObject); + } + + @Test + void testSkipObjectString() { + testWithCreateParserFromString(TestData.JSON_OBJECT_WITH_OBJECTS, this::testSkipObject); + } + + private void testSkipObject(JsonParser parser) { + parser.next(); + parser.next(); + parser.getString(); + parser.next(); + parser.skipObject(); + parser.next(); + String key = parser.getString(); + + assertEquals("secondPerson", key); + } + + private void assertThrowsIllegalStateException(Executable executable) { + assertThrows(IllegalStateException.class, executable); + } + + @Test + void testErrorGetObjectStructure() { + assertThrowsIllegalStateException(() -> testWithCreateParserFromObject(TestData.createJsonObject(), JsonParser::getObject)); + } + + @Test + void testErrorGetObjectString() { + assertThrowsIllegalStateException(() -> testWithCreateParserFromString(TestData.JSON_OBJECT_WITH_OBJECTS, JsonParser::getObject)); + } + + @Test + void testErrorGetArrayStructure() { + assertThrowsIllegalStateException(() -> testWithCreateParserFromObject(TestData.createJsonObject(), this::testErrorGetArray)); + } + + @Test + void testErrorGetArrayString() { + assertThrowsIllegalStateException(() -> testWithCreateParserFromString(TestData.JSON_OBJECT_WITH_OBJECTS, this::testErrorGetArray)); + } + + private void testErrorGetArray(JsonParser parser) { + parser.next(); + parser.getArray(); + } + + @Test + void testErrorGetValueEndOfObjectStructure() { + assertThrowsIllegalStateException(() -> testWithCreateParserFromObject(TestData.createJsonObject(), this::testErrorGetValueEndOfObject)); + } + + @Test + void testErrorGetValueEndOfObjectString() { + assertThrowsIllegalStateException(() -> testWithCreateParserFromString(TestData.JSON_OBJECT_WITH_OBJECTS, this::testErrorGetValueEndOfObject)); + } + + private void testErrorGetValueEndOfObject(JsonParser parser) { + parser.next(); + parser.skipObject(); + parser.getValue(); + } + + @Test + void testErrorGetValueEndOfArrayStructure() { + assertThrowsIllegalStateException(() -> testWithCreateParserFromObject(TestData.createObjectWithArrays(), this::testErrorGetValueEndOfArray)); + } + + @Test + void testErrorGetValueEndOfArrayString() { + assertThrowsIllegalStateException(() -> testWithCreateParserFromString(TestData.JSON_OBJECT_WITH_ARRAYS, this::testErrorGetValueEndOfArray)); + } + + private void testErrorGetValueEndOfArray(JsonParser parser) { + parser.next(); + parser.next(); + parser.getString(); + parser.next(); + parser.skipArray(); + parser.getValue(); + } + + @Test + void testBooleanNullandCurrentEventStructure() { + testWithCreateParserFromObject(Json.createObjectBuilder() + .add("true", true) + .add("false", false) + .addNull("null") + .build(), this::testBooleanNullandCurrentEvent); + } + + @Test + void testBooleanNullandCurrentEventString() { + testWithCreateParserFromString("{\"true\":true,\"false\":false,\"null\":null}", this::testBooleanNullandCurrentEvent); + } + + private void testBooleanNullandCurrentEvent(JsonParser parser) { + parser.next(); + parser.next(); + parser.getValue(); + parser.next(); + assertEquals(JsonValue.ValueType.TRUE, parser.getValue().getValueType()); + parser.next(); + parser.getValue(); + parser.next(); + assertEquals(JsonValue.ValueType.FALSE, parser.getValue().getValueType()); + parser.next(); + parser.getValue(); + parser.next(); + assertEquals(JsonValue.ValueType.NULL, parser.getValue().getValueType()); + assertEquals(Event.VALUE_NULL, parser.currentEvent()); + } + + @Test + void testBigLongAndDecimalsStructure() { + testWithCreateParserFromObject(Json.createObjectBuilder() + .add("long", 12345678901234567L) + .add("longer", 1234567890123456789L) + .build(), this::testBigLongAndDecimals); + } + + @Test + void testBigLongAndDecimalsString() { + testWithCreateParserFromString("{\"long\":12345678901234567,\"longer\":1234567890123456789}", this::testBigLongAndDecimals); + } + + private void testBigLongAndDecimals(JsonParser parser) { + parser.next(); + parser.next(); + parser.getString(); + parser.next(); + assertEquals("12345678901234567", parser.getValue().toString()); + parser.next(); + parser.getString(); + parser.next(); + assertEquals("1234567890123456789", parser.getValue().toString()); + } + + private void assertThrowsJsonParsingException(Executable executable) { + assertThrows(JsonParsingException.class, executable); + } + + @Test + void testWrongValueAndEndOfObjectInArray() {//509 ArrayContext.getNextEvent, no coma + assertThrowsJsonParsingException(() -> testWithCreateParserFromString("{\"a\":[5 }]}", parser -> { + parser.next(); + parser.next(); + parser.getString(); + parser.next(); + parser.getValue(); + })); + } + + @Test + void testWrongEndOfObjectInArray() {//518 ArrayContext.getNextEvent, at the end + assertThrowsJsonParsingException(() -> testWithCreateParserFromString("{\"a\":[}, 3]}", parser -> { + parser.next(); + parser.next(); + parser.getString(); + parser.next(); + parser.getValue(); + })); + } + + @Test + void testWrongKey() {//477 ObjectContext.getNextEvent, at the end + assertThrowsJsonParsingException(() -> testWithCreateParserFromString("{\"a\":1, 5}", parser -> { + parser.next(); + parser.next(); + parser.getString(); + parser.next(); + parser.getValue(); + parser.next(); + })); + } + + @Test + void testErrorInTheValue() {//470 ObjectContext.getNextEvent, no coma + assertThrowsJsonParsingException(() -> testWithCreateParserFromString("{\"a\":1:}", parser -> { + parser.next(); + parser.next(); + parser.getString(); + parser.next(); + parser.getValue(); + parser.next(); + })); + } + + @Test + void testNoValueAfterKey() {//452 ObjectContext.getNextEvent, no colon + assertThrowsJsonParsingException(() -> testWithCreateParserFromString("{\"a\"}", parser -> { + parser.next(); + parser.next(); + parser.getString(); + parser.next(); + })); + } + + @Test + void testNoJSONAtAll() {//382 NoneContext.getNextEvent, at the end + assertThrowsJsonParsingException(() -> testWithCreateParserFromString("", JsonParser::next)); + } + + @Test + void testWrongArrayEndWithComa() {//518 ArrayContext.getNextEvent, at the end + assertThrowsJsonParsingException(() -> testWithCreateParserFromString("[,", parser -> { + parser.next(); + parser.getArray(); + })); + } + } + + @Nested + class StreamTests { + @Test + void testGetValueStream_GetOneElement_Structure() { + testWithCreateParserFromObject(TestData.createFamilyPerson(), this::testGetValueStream_GetOneElement); + } + + @Test + void testGetValueStream_GetOneElement_String() { + testWithCreateParserFromString(TestData.JSON_FAMILY_STRING, this::testGetValueStream_GetOneElement); + } + + private void testGetValueStream_GetOneElement(JsonParser parser) { + JsonString name = (JsonString) parser.getValueStream() + .map(JsonValue::asJsonObject) + .map(JsonObject::values) + .findFirst() + .orElseThrow(() -> new NoSuchElementException("No value present")) + .stream() + .filter(e -> e.getValueType() == JsonValue.ValueType.STRING) + .findFirst() + .orElseThrow(() -> new RuntimeException("Name not found")); + + assertEquals("John", name.getString()); + } + + @Test + void testGetValueStream_GetListStructure() { + testWithCreateParserFromObject(TestData.createFamilyPerson(), this::testGetValueStream_GetList); + } + + @Test + void testGetValueStream_GetListString() { + testWithCreateParserFromString(TestData.JSON_FAMILY_STRING, this::testGetValueStream_GetList); + } + + private void testGetValueStream_GetList(JsonParser parser) { + List values = parser.getValueStream().map(value -> Objects.toString(value, "null")).collect(Collectors.toList()); + + assertThat(values, contains(TestData.JSON_FAMILY_STRING)); + } + + @Test + void testGetArrayStream_GetOneElementStructure() { + testWithCreateParserFromObject(TestData.createObjectWithArrays(), this::testGetArrayStream_GetOneElement); + } + + @Test + void testGetArrayStream_GetOneElementString() { + testWithCreateParserFromString(TestData.JSON_OBJECT_WITH_ARRAYS, this::testGetArrayStream_GetOneElement); + } + + private void testGetArrayStream_GetOneElement(JsonParser parser) { + parser.next(); + parser.next(); + String key = parser.getString(); + parser.next(); + JsonString element = (JsonString) parser.getArrayStream().filter(e -> e.getValueType() == JsonValue.ValueType.STRING) + .findFirst() + .orElseThrow(() -> new RuntimeException("Element not found")); + + assertEquals("first", element.getString()); + assertEquals("firstElement", key); + } + + @Test + void testGetArrayStream_GetListStructure() { + testWithCreateParserFromObject(TestData.createObjectWithArrays(), this::testGetArrayStream_GetList); + } + + @Test + void testGetArrayStream_GetListString() { + testWithCreateParserFromString(TestData.JSON_OBJECT_WITH_ARRAYS, this::testGetArrayStream_GetList); + } + + private void testGetArrayStream_GetList(JsonParser parser) { + parser.next(); + parser.next(); + String key = parser.getString(); + parser.next(); + List values = parser.getArrayStream().map(value -> Objects.toString(value, "null")).collect(Collectors.toList()); + + assertThat(values, TestData.ARRAY_STREAM_MATCHER); + assertEquals("firstElement", key); + } + + @Test + void testGetObjectStream_GetOneElementStructure() { + testWithCreateParserFromObject(TestData.createJsonObject(), this::testGetObjectStream_GetOneElement); + } + + @Test + void testGetObjectStream_GetOneElementString() { + testWithCreateParserFromString(TestData.JSON_OBJECT_WITH_OBJECTS, this::testGetObjectStream_GetOneElement); + } + + private void testGetObjectStream_GetOneElement(JsonParser parser) { + parser.next(); + String surname = parser.getObjectStream().filter(e -> e.getKey().equals("firstPerson")) + .map(Map.Entry::getValue) + .map(JsonValue::asJsonObject) + .map(obj -> obj.getString("surname")) + .findFirst() + .orElseThrow(() -> new RuntimeException("Surname not found")); + + assertEquals("Smith", surname); + } + + @Test + void testGetObjectStream_GetListStructure() { + testWithCreateParserFromObject(TestData.createFamilyPerson(), this::testGetObjectStream_GetList); + } + + @Test + void testGetObjectStream_GetListString() { + testWithCreateParserFromString(TestData.JSON_FAMILY_STRING, this::testGetObjectStream_GetList); + } + + private void testGetObjectStream_GetList(JsonParser parser) { + parser.next(); + List values = parser.getObjectStream().collect(MAP_TO_LIST_COLLECTOR); + + assertThat(values, TestData.FAMILY_MATCHER_KEYS_WITHOUT_QUOTATION); + } + } + + @Nested + public class JSONPStandardParserTests { + @Test + void testStandardStructureParser_getValueStream() { + testWithCreateParserFromObject(TestData.createFamilyPerson(), this::test_getValueStream); + } + + @Test + void testStandardStringParser_getValueStream() { + testWithCreateParserFromString(TestData.JSON_FAMILY_STRING, this::test_getValueStream); + } + + private void test_getValueStream(JsonParser parser) { + List values = parser.getValueStream().map(value -> Objects.toString(value, "null")).collect(Collectors.toList()); + + assertThat(values, contains(TestData.JSON_FAMILY_STRING)); + } + + @Test + void testStandardStructureParser_getArrayStream() { + testWithCreateParserFromObject(TestData.createObjectWithArrays(), this::test_getArrayStream); + } + + @Test + void testStandardStringParser_getArrayStream() { + testWithCreateParserFromString(TestData.JSON_OBJECT_WITH_ARRAYS, this::test_getArrayStream); + } + + private void test_getArrayStream(JsonParser parser) { + parser.next(); + parser.next(); + String key = parser.getString(); + parser.next(); + List values = parser.getArrayStream().map(value -> Objects.toString(value, "null")).collect(Collectors.toList()); + + assertThat(values, TestData.ARRAY_STREAM_MATCHER); + assertEquals("firstElement", key); + } + + @Test + void testStandardStructureParser_getObjectStream() { + testWithCreateParserFromObject(TestData.createFamilyPerson(), this::test_getObjectStream); + } + + @Test + void testStandardStringParser_getObjectStream() { + testWithCreateParserFromString(TestData.JSON_FAMILY_STRING, this::test_getObjectStream); + } + + private void test_getObjectStream(JsonParser parser) { + parser.next(); + List values = parser.getObjectStream().collect(MAP_TO_LIST_COLLECTOR); + + assertThat(values, TestData.FAMILY_MATCHER_KEYS_WITHOUT_QUOTATION); + } + + @Test + void testStandardStructureParser_getValue() { + testWithCreateParserFromObject(TestData.createFamilyPerson(), this::test_getValue); + } + + @Test + void testStandardStringParser_getValue() { + testWithCreateParserFromString(TestData.JSON_FAMILY_STRING, this::test_getValue); + } + + private void test_getValue(JsonParser parser) { + List values = new ArrayList<>(); + parser.next(); + while (parser.hasNext()) { + Event event = parser.next(); + if (!NOT_GET_VALUE_EVENT_ENUM_SET.contains(event)) { + String strValue = Objects.toString(parser.getValue(), "null"); + values.add(strValue); + } + } + + assertThat(values, TestData.FAMILY_MATCHER_KEYS_WITH_QUOTATION); + } + + @Test + void testStandardStructureParser_getString() { + testWithCreateParserFromObject(TestData.createFamilyPerson(), this::test_getString); + } + + @Test + void testStandardStringParser_getString() { + testWithCreateParserFromString(TestData.JSON_FAMILY_STRING, this::test_getString); + } + + private void test_getString(JsonParser parser) { + List values = new ArrayList<>(); + parser.next(); + while (parser.hasNext()) { + Event event = parser.next(); + if (GET_STRING_EVENT_ENUM_SET.contains(event)) { + String strValue = Objects.toString(parser.getString(), "null"); + values.add(strValue); + } + } + + assertThat(values, TestData.FAMILY_MATCHER_WITH_NO_QUOTATION); + } + } + + private static class TestData { + private static final String JSON_OBJECT_WITH_OBJECTS = "{\"firstPerson\":{\"name\":\"John\", \"surname\":\"Smith\"}," + + "\"secondPerson\":{\"name\":\"Deborah\", \"surname\":\"Harris\"}}"; + + private static final String JSON_OBJECT_WITH_ARRAYS = "{\"firstElement\":[\"first\", \"second\"],\"secondElement\":[\"third\", \"fourth\"]}"; + + private static final String JSON_FAMILY_STRING = "{\"name\":\"John\",\"surname\":\"Smith\",\"age\":30,\"married\":true," + + "\"wife\":{\"name\":\"Deborah\",\"surname\":\"Harris\"},\"children\":[\"Jack\",\"Mike\"]}"; + + private static final Matcher> FAMILY_MATCHER_KEYS_WITHOUT_QUOTATION = + Matchers.contains("name", "\"John\"", "surname", "\"Smith\"", "age", "30", "married", "true", "wife", + "{\"name\":\"Deborah\",\"surname\":\"Harris\"}", "children", "[\"Jack\",\"Mike\"]"); + + private static final Matcher> FAMILY_MATCHER_KEYS_WITH_QUOTATION = + Matchers.contains("\"name\"", "\"John\"", "\"surname\"", "\"Smith\"", "\"age\"", "30", "\"married\"", "true", + "\"wife\"", "{\"name\":\"Deborah\",\"surname\":\"Harris\"}", "\"children\"", "[\"Jack\",\"Mike\"]"); + + private static final Matcher> FAMILY_MATCHER_WITH_NO_QUOTATION = + Matchers.contains("name", "John", "surname", "Smith", "age", "30", "married", + "wife", "name", "Deborah", "surname", "Harris", "children", "Jack", "Mike"); + + private static final Matcher> ARRAY_STREAM_MATCHER = Matchers.contains("\"first\"", "\"second\""); + + private static JsonObject createFamilyPerson() { + return Json.createObjectBuilder() + .add("name", "John") + .add("surname", "Smith") + .add("age", 30) + .add("married", true) + .add("wife", createPerson("Deborah", "Harris")) + .add("children", createArray("Jack", "Mike")) + .build(); + } + + private static JsonObject createObjectWithArrays() { + return Json.createObjectBuilder() + .add("firstElement", createArray("first", "second")) + .add("secondElement", createArray("third", "fourth")) + .build(); + } + + private static JsonArrayBuilder createArray(String firstElement, String secondElement) { + return Json.createArrayBuilder().add(firstElement).add(secondElement); + } + + private static JsonObject createJsonObject() { + return Json.createObjectBuilder() + .add("firstPerson", createPerson("John", "Smith")) + .add("secondPerson", createPerson("Deborah", "Harris")) + .build(); + } + + private static JsonObjectBuilder createPerson(String name, String surname) { + return Json.createObjectBuilder() + .add("name", name) + .add("surname", surname); + } + } }