True object and array streaming (#74)

* Add tests for getArrayStream() and getObjectStream()

* Implement lazy parsing for getArrayStream() and getObjectStream()

Now these methods should behave in the spirit of the specification, only
keeping the latest element in memory and parsing more data only as
necessary.

* Remove redundant else's

* Extract anonymous spliterator implementations into static classes

* Verify the event type we get in getObjectStream()

* Add unit tests for parse errors in getArrayStream and getObjectStream

* Make sure to close all parsers
diff --git a/johnzon-core/src/main/java/org/apache/johnzon/core/JohnzonJsonParserImpl.java b/johnzon-core/src/main/java/org/apache/johnzon/core/JohnzonJsonParserImpl.java
index 6edde73..61cb708 100644
--- a/johnzon-core/src/main/java/org/apache/johnzon/core/JohnzonJsonParserImpl.java
+++ b/johnzon-core/src/main/java/org/apache/johnzon/core/JohnzonJsonParserImpl.java
@@ -17,9 +17,15 @@
 package org.apache.johnzon.core;
 
 
+import java.util.AbstractMap;
 import java.util.Collections;
 import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.function.Consumer;
 import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 
 import javax.json.JsonArray;
 import javax.json.JsonObject;
@@ -132,20 +138,76 @@
         }
     }
 
+    private static class ArrayStreamSpliterator extends Spliterators.AbstractSpliterator<JsonValue> {
+
+        private final JohnzonJsonParserImpl parser;
+
+        ArrayStreamSpliterator(JohnzonJsonParserImpl parser) {
+            super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL | Spliterator.ORDERED);
+            this.parser = parser;
+        }
+
+        @Override
+        public boolean tryAdvance(Consumer<? super JsonValue> action) {
+            Event next = parser.next();
+
+            if (next == Event.END_ARRAY) {
+                return false;
+            }
+
+            action.accept(parser.getValue());
+            return true;
+        }
+    }
+
     @Override
     public Stream<JsonValue> getArrayStream() {
-        //X TODO this implementation is very simplistic
-        //X I find it unintuitive what the spec intends here
-        //X we probably need to improve this
-        return getArray().stream();
+        Event current = current();
+        if (current != Event.START_ARRAY) {
+            throw new IllegalStateException(current + " doesn't support getArrayStream()");
+        }
+
+        return StreamSupport.stream(new ArrayStreamSpliterator(this), false);
+    }
+
+    private static class ObjectStreamSpliterator extends Spliterators.AbstractSpliterator<Map.Entry<String,JsonValue>> {
+        
+        private final JohnzonJsonParserImpl parser;
+
+        ObjectStreamSpliterator(JohnzonJsonParserImpl parser) {
+            super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL | Spliterator.ORDERED);
+            this.parser = parser;
+        }
+
+        @Override
+        public boolean tryAdvance(Consumer<? super Entry<String, JsonValue>> action) {
+            Event next = parser.next();
+
+            if (next == Event.END_OBJECT) {
+                return false;
+            }
+
+            if (next != Event.KEY_NAME) {
+                throw new IllegalStateException("Expected key name event but got " + next + " instead.");
+            }
+
+            String key = parser.getString();
+            parser.next();
+            JsonValue value = parser.getValue();
+            action.accept(new AbstractMap.SimpleImmutableEntry<>(key, value));
+            return true;
+        }
+
     }
 
     @Override
     public Stream<Map.Entry<String, JsonValue>> getObjectStream() {
-        //X TODO this implementation is very simplistic
-        //X I find it unintuitive what the spec intends here
-        //X we probably need to improve this
-        return getObject().entrySet().stream();
+        Event current = current();
+        if (current != Event.START_OBJECT) {
+            throw new IllegalStateException(current + " doesn't support getObjectStream()");
+        }
+
+        return StreamSupport.stream(new ObjectStreamSpliterator(this), false);
     }
 
     @Override
diff --git a/johnzon-core/src/test/java/org/apache/johnzon/core/JsonParserStreamingTest.java b/johnzon-core/src/test/java/org/apache/johnzon/core/JsonParserStreamingTest.java
index dfdff4e..42fd659 100644
--- a/johnzon-core/src/test/java/org/apache/johnzon/core/JsonParserStreamingTest.java
+++ b/johnzon-core/src/test/java/org/apache/johnzon/core/JsonParserStreamingTest.java
@@ -18,20 +18,26 @@
  */
 package org.apache.johnzon.core;
 
-import org.junit.Test;
+import static java.util.stream.Collectors.joining;
+import static org.junit.Assert.assertEquals;
 
-import javax.json.Json;
-import javax.json.stream.JsonParser;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.StringReader;
 import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 
-import static java.util.stream.Collectors.joining;
-import static org.junit.Assert.assertEquals;
+import javax.json.Json;
+import javax.json.JsonNumber;
+import javax.json.JsonString;
+import javax.json.stream.JsonParser;
+import javax.json.stream.JsonParsingException;
+
+import org.junit.Test;
 
 public class JsonParserStreamingTest {
     @Test
@@ -42,8 +48,9 @@
                 "\t\"}").getBytes(StandardCharsets.UTF_8);
 
         final BufferStrategy.BufferProvider<char[]> bs = BufferStrategyFactory.valueOf("QUEUE").newCharProvider(len);
-        try (final InputStream stream = new ByteArrayInputStream(bytes)) {
-            final JsonStreamParserImpl impl = new JsonStreamParserImpl(stream, len, bs, bs, false);
+        try (final InputStream stream = new ByteArrayInputStream(bytes);
+                final JsonStreamParserImpl impl = new JsonStreamParserImpl(stream, len, bs, bs, false)) {
+
             while (impl.hasNext()) {
                 impl.next();
             }
@@ -114,11 +121,71 @@
 
     private String parserAndConcat(String json) {
         StringReader sr = new StringReader(json);
-        JsonParser jsonParser = Json.createParser(sr);
 
-        String sum = jsonParser.getValueStream()
-                .map(v -> v.toString())
-                .collect(Collectors.joining(","));
-        return sum;
+        try (JsonParser jsonParser = Json.createParser(sr)) {
+            String sum = jsonParser.getValueStream()
+                    .map(v -> v.toString())
+                    .collect(Collectors.joining(","));
+            return sum;
+        }
     }
+
+    @Test
+    public void testGetArrayStream() {
+        StringReader sr = new StringReader("[1,2,3,4,5,6]");
+
+        try (JsonParser jsonParser = Json.createParser(sr)) {
+            JsonParser.Event firstEvent = jsonParser.next();
+            assertEquals(JsonParser.Event.START_ARRAY, firstEvent);
+
+            int sum = jsonParser.getArrayStream()
+                    .mapToInt(v -> ((JsonNumber)v).intValue())
+                    .sum();
+            assertEquals(21, sum);
+        }
+    }
+
+    @Test(expected = JsonParsingException.class)
+    public void testParseErrorInGetArrayStream() {
+        StringReader sr = new StringReader("[\"this is\":\"not an object\"]");
+
+        try (JsonParser jsonParser = Json.createParser(sr)) {
+            JsonParser.Event firstEvent = jsonParser.next();
+            assertEquals(JsonParser.Event.START_ARRAY, firstEvent);
+
+            jsonParser.getArrayStream().forEach(dummy -> {});
+        }
+    }
+
+    @Test
+    public void testGetObjectStream() {
+        StringReader sr = new StringReader("{\"foo\":\"bar\",\"baz\":\"quux\",\"something\":\"else\"}");
+
+        try (JsonParser jsonParser = Json.createParser(sr)) {
+            JsonParser.Event firstEvent = jsonParser.next();
+            assertEquals(JsonParser.Event.START_OBJECT, firstEvent);
+
+            Map<String, String> mappings = jsonParser.getObjectStream()
+                    .collect(Collectors.toMap(Map.Entry::getKey, e -> ((JsonString)e.getValue()).getString()));
+
+            Map<String, String> expectedMappings = new HashMap<>();
+            expectedMappings.put("foo", "bar");
+            expectedMappings.put("baz", "quux");
+            expectedMappings.put("something", "else");
+            assertEquals(expectedMappings, mappings);
+        }
+    }
+
+    @Test(expected = JsonParsingException.class)
+    public void testParseErrorInGetObjectStream() {
+        StringReader sr = new StringReader("{42}");
+
+        try (JsonParser jsonParser = Json.createParser(sr)) {
+            JsonParser.Event firstEvent = jsonParser.next();
+            assertEquals(JsonParser.Event.START_OBJECT, firstEvent);
+
+            jsonParser.getObjectStream().forEach(dummy -> {});
+        }
+    }
+
 }