From aa0a4c0a1842afa826daed2a60aa348658ce2a9e Mon Sep 17 00:00:00 2001 From: Markus Jung Date: Tue, 25 Aug 2026 20:50:26 +0200 Subject: [PATCH] JOHNZON-441 - implement currentEvent for RewindableJsonParser and JsonValueParserAdapter --- .../johnzon/jsonb/JsonValueParserAdapter.java | 304 +++++++++--------- .../mapper/jsonp/RewindableJsonParser.java | 72 ++++- .../jsonp/RewindableJsonParserTest.java | 151 +++++++++ 3 files changed, 373 insertions(+), 154 deletions(-) create mode 100644 johnzon-mapper/src/test/java/org/apache/johnzon/mapper/jsonp/RewindableJsonParserTest.java diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonValueParserAdapter.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonValueParserAdapter.java index 4c09496a2..6743d76ca 100644 --- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonValueParserAdapter.java +++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonValueParserAdapter.java @@ -1,145 +1,159 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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.apache.johnzon.jsonb; - -import java.math.BigDecimal; -import java.util.function.Supplier; - -import jakarta.json.JsonNumber; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; -import jakarta.json.stream.JsonLocation; -import jakarta.json.stream.JsonParser; -import jakarta.json.stream.JsonParserFactory; - -import org.apache.johnzon.mapper.jsonp.RewindableJsonParser; - -class JsonValueParserAdapter implements JsonParser { - - private static class JsonStringParserAdapter extends JsonValueParserAdapter { - - public JsonStringParserAdapter(JsonString jsonValue) { - super(jsonValue); - } - - @Override - public String getString() { - return getValue().getString(); - } - } - - private static class JsonNumberParserAdapter extends JsonValueParserAdapter { - - public JsonNumberParserAdapter(JsonNumber jsonValue) { - super(jsonValue); - } - - @Override - public boolean isIntegralNumber() { - return getValue().isIntegral(); - } - - @Override - public int getInt() { - return getValue().intValueExact(); - } - - @Override - public long getLong() { - return getValue().longValueExact(); - } - - @Override - public BigDecimal getBigDecimal() { - return getValue().bigDecimalValue(); - } - } - - public static JsonParser createFor(final JsonValue jsonValue, - final Supplier parserFactoryProvider) { - return new RewindableJsonParser(doCreate(jsonValue, parserFactoryProvider)); - } - - private static JsonParser doCreate(final JsonValue jsonValue, - final Supplier parserFactoryProvider) { - switch (jsonValue.getValueType()) { - case OBJECT: return parserFactoryProvider.get().createParser(jsonValue.asJsonObject()); - case ARRAY: return parserFactoryProvider.get().createParser(jsonValue.asJsonArray()); - case STRING: return new JsonStringParserAdapter((JsonString) jsonValue); - case NUMBER: return new JsonNumberParserAdapter((JsonNumber) jsonValue); - default: return new JsonValueParserAdapter<>(jsonValue); - } - } - - private final T jsonValue; - - JsonValueParserAdapter(T jsonValue) { - this.jsonValue = jsonValue; - } - - @Override - public boolean hasNext() { - return false; - } - - @Override - public Event next() { - throw new UnsupportedOperationException("next() no supported for " + jsonValue.getValueType()); - } - - @Override - public String getString() { - throw new UnsupportedOperationException("next() no supported for " + jsonValue.getValueType()); - } - - @Override - public boolean isIntegralNumber() { - throw new UnsupportedOperationException("isIntegralNumber() not supported for " + jsonValue.getValueType()); - } - - @Override - public int getInt() { - throw new UnsupportedOperationException("getInt() not supported for " + jsonValue.getValueType()); - } - - @Override - public long getLong() { - throw new UnsupportedOperationException("getLong() not supported for " + jsonValue.getValueType()); - } - - @Override - public BigDecimal getBigDecimal() { - throw new UnsupportedOperationException("getBigDecimal() not supported for " + jsonValue.getValueType()); - } - - @Override - public JsonLocation getLocation() { - throw new UnsupportedOperationException("getLocation() not supported for " + jsonValue.getValueType()); - } - - @Override - public void close() { - // no-op - } - - @Override - public T getValue() { - return jsonValue; - } -} +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.johnzon.jsonb; + +import java.math.BigDecimal; +import java.util.function.Supplier; + +import jakarta.json.JsonNumber; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; +import jakarta.json.stream.JsonLocation; +import jakarta.json.stream.JsonParser; +import jakarta.json.stream.JsonParserFactory; + +import org.apache.johnzon.mapper.jsonp.RewindableJsonParser; + +class JsonValueParserAdapter implements JsonParser { + + private static class JsonStringParserAdapter extends JsonValueParserAdapter { + + public JsonStringParserAdapter(JsonString jsonValue) { + super(jsonValue); + } + + @Override + public String getString() { + return getValue().getString(); + } + } + + private static class JsonNumberParserAdapter extends JsonValueParserAdapter { + + public JsonNumberParserAdapter(JsonNumber jsonValue) { + super(jsonValue); + } + + @Override + public boolean isIntegralNumber() { + return getValue().isIntegral(); + } + + @Override + public int getInt() { + return getValue().intValueExact(); + } + + @Override + public long getLong() { + return getValue().longValueExact(); + } + + @Override + public BigDecimal getBigDecimal() { + return getValue().bigDecimalValue(); + } + } + + public static JsonParser createFor(final JsonValue jsonValue, + final Supplier parserFactoryProvider) { + return new RewindableJsonParser(doCreate(jsonValue, parserFactoryProvider)); + } + + private static JsonParser doCreate(final JsonValue jsonValue, + final Supplier parserFactoryProvider) { + switch (jsonValue.getValueType()) { + case OBJECT: return parserFactoryProvider.get().createParser(jsonValue.asJsonObject()); + case ARRAY: return parserFactoryProvider.get().createParser(jsonValue.asJsonArray()); + case STRING: return new JsonStringParserAdapter((JsonString) jsonValue); + case NUMBER: return new JsonNumberParserAdapter((JsonNumber) jsonValue); + default: return new JsonValueParserAdapter<>(jsonValue); + } + } + + private final T jsonValue; + + JsonValueParserAdapter(T jsonValue) { + this.jsonValue = jsonValue; + } + + @Override + public boolean hasNext() { + return false; + } + + @Override + public Event next() { + throw new UnsupportedOperationException("next() no supported for " + jsonValue.getValueType()); + } + + @Override + public Event currentEvent() { + switch (jsonValue.getValueType()) { + case STRING: return Event.VALUE_STRING; + case NUMBER: return Event.VALUE_NUMBER; + case TRUE: return Event.VALUE_TRUE; + case FALSE: return Event.VALUE_FALSE; + case NULL: return Event.VALUE_NULL; + case OBJECT: return Event.START_OBJECT; + case ARRAY: return Event.START_ARRAY; + default: throw new UnsupportedOperationException("currentEvent() not supported for " + jsonValue.getValueType()); + } + } + + @Override + public String getString() { + throw new UnsupportedOperationException("next() no supported for " + jsonValue.getValueType()); + } + + @Override + public boolean isIntegralNumber() { + throw new UnsupportedOperationException("isIntegralNumber() not supported for " + jsonValue.getValueType()); + } + + @Override + public int getInt() { + throw new UnsupportedOperationException("getInt() not supported for " + jsonValue.getValueType()); + } + + @Override + public long getLong() { + throw new UnsupportedOperationException("getLong() not supported for " + jsonValue.getValueType()); + } + + @Override + public BigDecimal getBigDecimal() { + throw new UnsupportedOperationException("getBigDecimal() not supported for " + jsonValue.getValueType()); + } + + @Override + public JsonLocation getLocation() { + throw new UnsupportedOperationException("getLocation() not supported for " + jsonValue.getValueType()); + } + + @Override + public void close() { + // no-op + } + + @Override + public T getValue() { + return jsonValue; + } +} diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/jsonp/RewindableJsonParser.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/jsonp/RewindableJsonParser.java index f0c246091..005f34553 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/jsonp/RewindableJsonParser.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/jsonp/RewindableJsonParser.java @@ -20,7 +20,12 @@ import java.math.BigDecimal; import java.util.Map; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.stream.Stream; +import java.util.stream.StreamSupport; import jakarta.json.JsonArray; import jakarta.json.JsonObject; @@ -31,6 +36,7 @@ public class RewindableJsonParser implements JsonParser { private final JsonParser delegate; private Event last; + private boolean pendingRewind; public RewindableJsonParser(final JsonParser delegate) { this.delegate = delegate; @@ -42,14 +48,30 @@ public Event getLast() { @Override public boolean hasNext() { - return delegate.hasNext(); + return pendingRewind || delegate.hasNext(); } @Override public Event next() { + if (pendingRewind) { + pendingRewind = false; + return last; + } return last = delegate.next(); } + @Override + public Event currentEvent() { + if (last == null) { + if (!delegate.hasNext()) { // value adapters without event stream + return delegate.currentEvent(); + } + last = delegate.next(); + pendingRewind = true; + } + return last; + } + @Override public String getString() { return delegate.getString(); @@ -87,41 +109,73 @@ public void close() { @Override public JsonObject getObject() { - return delegate.getObject(); + return trackResult(delegate::getObject); } @Override public JsonValue getValue() { - return delegate.getValue(); + return trackResult(delegate::getValue); } @Override public JsonArray getArray() { - return delegate.getArray(); + return trackResult(delegate::getArray); } @Override public Stream getArrayStream() { - return delegate.getArrayStream(); + return trackStream(delegate.getArrayStream()); } @Override public Stream> getObjectStream() { - return delegate.getObjectStream(); + return trackStream(delegate.getObjectStream()); } @Override public Stream getValueStream() { - return delegate.getValueStream(); + return trackStream(delegate.getValueStream()); } @Override public void skipArray() { - delegate.skipArray(); + trackAdvance(delegate::skipArray); } @Override public void skipObject() { - delegate.skipObject(); + trackAdvance(delegate::skipObject); + } + + private T trackResult(final Supplier operation) { + pendingRewind = false; + final T result = operation.get(); + last = delegate.currentEvent(); + return result; + } + + private void trackAdvance(final Runnable operation) { + pendingRewind = false; + operation.run(); + last = delegate.currentEvent(); + } + + private Stream trackStream(final Stream stream) { + pendingRewind = false; + last = delegate.currentEvent(); + final boolean parallel = stream.isParallel(); + final Spliterator spliterator = stream.spliterator(); + final Spliterator tracking = new Spliterators.AbstractSpliterator( + spliterator.estimateSize(), spliterator.characteristics()) { + @Override + public boolean tryAdvance(final Consumer action) { + try { + return spliterator.tryAdvance(action); + } finally { + last = delegate.currentEvent(); + } + } + }; + return StreamSupport.stream(tracking, parallel).onClose(stream::close); } } diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/jsonp/RewindableJsonParserTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/jsonp/RewindableJsonParserTest.java new file mode 100644 index 000000000..72bcfe709 --- /dev/null +++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/jsonp/RewindableJsonParserTest.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.johnzon.mapper.jsonp; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.StringReader; + +import jakarta.json.Json; +import jakarta.json.stream.JsonParser; +import jakarta.json.stream.JsonParser.Event; + +import org.junit.Test; + +public class RewindableJsonParserTest { + private RewindableJsonParser parser(final String json) { + return new RewindableJsonParser(Json.createParser(new StringReader(json))); + } + + @Test + public void currentEventBeforeNextIsReplayedByNext() { + final RewindableJsonParser parser = parser("{\"a\":1}"); + assertNull(parser.getLast()); + assertEquals(Event.START_OBJECT, parser.currentEvent()); + // JSON-P 2.1 JsonParser#currentEvent: "current parsing state". + assertEquals(Event.START_OBJECT, parser.currentEvent()); + assertTrue(parser.hasNext()); + assertEquals(Event.START_OBJECT, parser.next()); + assertEquals(Event.START_OBJECT, parser.currentEvent()); + assertEquals(Event.KEY_NAME, parser.next()); + assertEquals("a", parser.getString()); + assertEquals(Event.KEY_NAME, parser.currentEvent()); + assertEquals(Event.VALUE_NUMBER, parser.next()); + assertEquals(1, parser.getInt()); + assertEquals(Event.END_OBJECT, parser.next()); + assertFalse(parser.hasNext()); + } + + @Test + public void nextFirstKeepsWorking() { + final RewindableJsonParser parser = parser("[true]"); + assertEquals(Event.START_ARRAY, parser.next()); + assertEquals(Event.START_ARRAY, parser.currentEvent()); + assertEquals(Event.VALUE_TRUE, parser.next()); + assertEquals(Event.END_ARRAY, parser.next()); + } + + @Test + public void getObjectAfterPeek() { + final RewindableJsonParser parser = parser("{\"a\":\"b\"}"); + assertEquals(Event.START_OBJECT, parser.currentEvent()); + assertEquals("b", parser.getObject().getString("a")); + // JSON-P 2.1 JsonParser#getObject: advance to the "corresponding END_OBJECT". + assertEquals(Event.END_OBJECT, parser.currentEvent()); + assertFalse(parser.hasNext()); + } + + @Test + public void getValueAfterPeek() { + final RewindableJsonParser parser = parser("[1,2]"); + assertEquals(Event.START_ARRAY, parser.currentEvent()); + assertEquals(2, parser.getValue().asJsonArray().size()); + // JSON-P 2.1 JsonParser#getValue: "behavior is the same as getArray()". + assertEquals(Event.END_ARRAY, parser.currentEvent()); + assertFalse(parser.hasNext()); + } + + @Test + public void getArrayAfterPeek() { + final RewindableJsonParser parser = parser("[1,2]"); + assertEquals(Event.START_ARRAY, parser.currentEvent()); + assertEquals(2, parser.getArray().size()); + // JSON-P 2.1 JsonParser#getArray: advance to the "corresponding END_ARRAY". + assertEquals(Event.END_ARRAY, parser.currentEvent()); + assertFalse(parser.hasNext()); + } + + @Test + public void getArrayStreamAfterPeek() { + final RewindableJsonParser parser = parser("[1,2]"); + assertEquals(Event.START_ARRAY, parser.currentEvent()); + assertEquals(2, parser.getArrayStream().count()); + // JSON-P 2.1 JsonParser#getArrayStream: elements are "read lazily"; full consumption reaches END_ARRAY. + assertEquals(Event.END_ARRAY, parser.currentEvent()); + assertFalse(parser.hasNext()); + } + + @Test + public void getObjectStreamAfterPeek() { + final RewindableJsonParser parser = parser("{\"a\":1,\"b\":2}"); + assertEquals(Event.START_OBJECT, parser.currentEvent()); + assertEquals(2, parser.getObjectStream().count()); + // JSON-P 2.1 JsonParser#getObjectStream: pairs are "read lazily"; full consumption reaches END_OBJECT. + assertEquals(Event.END_OBJECT, parser.currentEvent()); + assertFalse(parser.hasNext()); + } + + @Test + public void getValueStreamTracksCurrentEvent() { + final RewindableJsonParser parser = parser("[1,2]"); + assertNull(parser.getLast()); + assertEquals(1, parser.getValueStream().count()); + // JSON-P 2.1 JsonParser#getValueStream: values are "read lazily"; track the final state. + assertEquals(Event.END_ARRAY, parser.getLast()); + assertEquals(Event.END_ARRAY, parser.currentEvent()); + assertFalse(parser.hasNext()); + } + + @Test + public void skipAfterPeek() { + final JsonParser parser = parser("{\"a\":{\"b\":1},\"c\":2}"); + assertEquals(Event.START_OBJECT, parser.currentEvent()); + assertEquals(Event.START_OBJECT, parser.next()); + assertEquals(Event.KEY_NAME, parser.next()); + assertEquals(Event.START_OBJECT, parser.next()); + parser.skipObject(); + // JSON-P 2.1 JsonParser#skipObject: advance the parser to "END_OBJECT". + assertEquals(Event.END_OBJECT, parser.currentEvent()); + assertEquals(Event.KEY_NAME, parser.next()); + assertEquals("c", parser.getString()); + } + + @Test + public void skipArrayAfterPeek() { + final RewindableJsonParser parser = parser("[1,2]"); + assertEquals(Event.START_ARRAY, parser.currentEvent()); + parser.skipArray(); + // JSON-P 2.1 JsonParser#skipArray: advance the parser to "END_ARRAY". + assertEquals(Event.END_ARRAY, parser.currentEvent()); + assertFalse(parser.hasNext()); + } +}