BATCHEE-80 adding jsonp reader/writer
diff --git a/extensions/extension-doc-helper/pom.xml b/extensions/extension-doc-helper/pom.xml
index 50cc1b8..a7df850 100644
--- a/extensions/extension-doc-helper/pom.xml
+++ b/extensions/extension-doc-helper/pom.xml
@@ -87,6 +87,11 @@
       <artifactId>batchee-shiro</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.batchee</groupId>
+      <artifactId>batchee-jsonp</artifactId>
+      <version>${project.version}</version>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/extensions/jsonp/pom.xml b/extensions/jsonp/pom.xml
new file mode 100644
index 0000000..4c166f4
--- /dev/null
+++ b/extensions/jsonp/pom.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <artifactId>batchee-extensions</artifactId>
+    <groupId>org.apache.batchee</groupId>
+    <version>0.3-incubating-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>batchee-jsonp</artifactId>
+  <name>BatchEE :: Extensions :: Jackson</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.geronimo.specs</groupId>
+      <artifactId>geronimo-json_1.0_spec</artifactId>
+      <version>1.0-alpha-1</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.batchee</groupId>
+      <artifactId>batchee-extras</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.johnzon</groupId>
+      <artifactId>johnzon-core</artifactId>
+      <version>0.9.2-incubating</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <properties>
+    <jackson.version>2.2.3</jackson.version>
+  </properties>
+</project>
diff --git a/extensions/jsonp/src/main/java/org/apache/batchee/jsonp/JsonPartialReader.java b/extensions/jsonp/src/main/java/org/apache/batchee/jsonp/JsonPartialReader.java
new file mode 100644
index 0000000..d5e9fa6
--- /dev/null
+++ b/extensions/jsonp/src/main/java/org/apache/batchee/jsonp/JsonPartialReader.java
@@ -0,0 +1,170 @@
+/*
+ * 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.batchee.jsonp;
+
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonStructure;
+import javax.json.spi.JsonProvider;
+import javax.json.stream.JsonParser;
+import javax.json.stream.JsonParsingException;
+
+public class JsonPartialReader {
+    private final JsonParser parser;
+    private final JsonProvider provider;
+    private boolean closed = false;
+
+    public JsonPartialReader(final JsonProvider provider, final JsonParser parser) {
+        this.parser = parser;
+        this.provider = provider == null ? JsonProvider.provider() : provider;
+    }
+
+    public JsonStructure read(final JsonParser.Event event) {
+        switch (event) {
+            case START_OBJECT:
+                final JsonObjectBuilder objectBuilder = provider.createObjectBuilder();
+                parseObject(objectBuilder);
+                return objectBuilder.build();
+            case START_ARRAY:
+                final JsonArrayBuilder arrayBuilder = provider.createArrayBuilder();
+                parseArray(arrayBuilder);
+                return arrayBuilder.build();
+            default:
+                throw new JsonParsingException("Unknown structure: " + parser.next(), parser.getLocation());
+        }
+
+    }
+
+    public void close() {
+        if (!closed) {
+            closed = true;
+            parser.close();
+        }
+    }
+
+    private void parseObject(final JsonObjectBuilder builder) {
+        String key = null;
+        while (parser.hasNext()) {
+            final JsonParser.Event next = parser.next();
+            switch (next) {
+                case KEY_NAME:
+                    key = parser.getString();
+                    break;
+
+                case VALUE_STRING:
+                    builder.add(key, parser.getString());
+                    break;
+
+                case START_OBJECT:
+                    final JsonObjectBuilder subObject = provider.createObjectBuilder();
+                    parseObject(subObject);
+                    builder.add(key, subObject);
+                    break;
+
+                case START_ARRAY:
+                    final JsonArrayBuilder subArray = provider.createArrayBuilder();
+                    parseArray(subArray);
+                    builder.add(key, subArray);
+                    break;
+
+                case VALUE_NUMBER:
+                    if (parser.isIntegralNumber()) {
+                        builder.add(key, parser.getLong());
+                    } else {
+                        builder.add(key, parser.getBigDecimal());
+                    }
+                    break;
+
+                case VALUE_NULL:
+                    builder.addNull(key);
+                    break;
+
+                case VALUE_TRUE:
+                    builder.add(key, true);
+                    break;
+
+                case VALUE_FALSE:
+                    builder.add(key, false);
+                    break;
+
+                case END_OBJECT:
+                    return;
+
+                case END_ARRAY:
+                    throw new JsonParsingException("']', shouldn't occur", parser.getLocation());
+
+                default:
+                    throw new JsonParsingException(next.name() + ", shouldn't occur", parser.getLocation());
+            }
+        }
+    }
+
+    private void parseArray(final JsonArrayBuilder builder) {
+        while (parser.hasNext()) {
+            final JsonParser.Event next = parser.next();
+            switch (next) {
+                case VALUE_STRING:
+                    builder.add(parser.getString());
+                    break;
+
+                case VALUE_NUMBER:
+                    if (parser.isIntegralNumber()) {
+                        builder.add(parser.getLong());
+                    } else {
+                        builder.add(parser.getBigDecimal());
+                    }
+                    break;
+
+                case START_OBJECT:
+                    JsonObjectBuilder subObject = provider.createObjectBuilder();
+                    parseObject(subObject);
+                    builder.add(subObject);
+                    break;
+
+                case START_ARRAY:
+                    JsonArrayBuilder subArray = provider.createArrayBuilder();
+                    parseArray(subArray);
+                    builder.add(subArray);
+                    break;
+
+                case END_ARRAY:
+                    return;
+
+                case VALUE_NULL:
+                    builder.addNull();
+                    break;
+
+                case VALUE_TRUE:
+                    builder.add(true);
+                    break;
+
+                case VALUE_FALSE:
+                    builder.add(false);
+                    break;
+
+                case KEY_NAME:
+                    throw new JsonParsingException("array doesn't have keys", parser.getLocation());
+
+                case END_OBJECT:
+                    throw new JsonParsingException("'}', shouldn't occur", parser.getLocation());
+
+                default:
+                    throw new JsonParsingException(next.name() + ", shouldn't occur", parser.getLocation());
+            }
+        }
+    }
+}
diff --git a/extensions/jsonp/src/main/java/org/apache/batchee/jsonp/JsonpReader.java b/extensions/jsonp/src/main/java/org/apache/batchee/jsonp/JsonpReader.java
new file mode 100644
index 0000000..57402fc
--- /dev/null
+++ b/extensions/jsonp/src/main/java/org/apache/batchee/jsonp/JsonpReader.java
@@ -0,0 +1,90 @@
+/*
+ * 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.batchee.jsonp;
+
+import org.apache.batchee.doc.api.Documentation;
+import org.apache.batchee.extras.transaction.CountedReader;
+
+import javax.batch.api.BatchProperty;
+import javax.inject.Inject;
+import javax.json.spi.JsonProvider;
+import javax.json.stream.JsonParser;
+import java.io.FileInputStream;
+import java.io.Serializable;
+
+@Documentation("Reads a JSON file using JSON-P providing JsonStructure as item.")
+public class JsonpReader extends CountedReader {
+    @Inject
+    @BatchProperty
+    @Documentation("Incoming file")
+    private String file;
+
+    @Inject
+    @BatchProperty
+    @Documentation("Should root be skipped (default: true)")
+    private String skipRoot;
+
+    @Inject
+    @BatchProperty
+    @Documentation("JSON-P provider if not using the default")
+    private String provider;
+
+    private JsonParser parser;
+    private JsonPartialReader reader;
+    private JsonParser.Event end = null;
+
+    @Override
+    public void open(final Serializable checkpoint) throws Exception {
+        final ClassLoader loader = Thread.currentThread().getContextClassLoader();
+        final JsonProvider provider = this.provider == null ? JsonProvider.provider() : JsonProvider.class.cast(loader.loadClass(this.provider));
+        parser = provider.createParser(new FileInputStream(file));
+        reader = new JsonPartialReader(provider, parser);
+
+        if (skipRoot == null || "true".equalsIgnoreCase(skipRoot)) {
+            final JsonParser.Event event = parser.next();
+            if (event == JsonParser.Event.START_ARRAY) {
+                end = JsonParser.Event.END_ARRAY;
+            } else {
+                end = JsonParser.Event.END_OBJECT;
+            }
+        }
+        super.open(checkpoint);
+    }
+
+    @Override
+    protected Object doRead() throws Exception {
+        JsonParser.Event event;
+        do {
+            event = parser.next();
+        } while (
+            (event != JsonParser.Event.START_OBJECT && event != end) ||
+            (end == null && (event == JsonParser.Event.END_ARRAY ||
+            event == JsonParser.Event.END_OBJECT)));
+        if (!parser.hasNext()) {
+            return null;
+        }
+
+        return reader.read(event);
+    }
+
+    @Override
+    public void close() throws Exception {
+        if (reader != null) {
+            reader.close();
+        }
+    }
+}
diff --git a/extensions/jsonp/src/main/java/org/apache/batchee/jsonp/JsonpWriter.java b/extensions/jsonp/src/main/java/org/apache/batchee/jsonp/JsonpWriter.java
new file mode 100644
index 0000000..a4af1a6
--- /dev/null
+++ b/extensions/jsonp/src/main/java/org/apache/batchee/jsonp/JsonpWriter.java
@@ -0,0 +1,156 @@
+/*
+ * 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.batchee.jsonp;
+
+import org.apache.batchee.doc.api.Documentation;
+import org.apache.batchee.extras.transaction.TransactionalWriter;
+
+import javax.batch.api.BatchProperty;
+import javax.batch.api.chunk.ItemWriter;
+import javax.batch.operations.BatchRuntimeException;
+import javax.inject.Inject;
+import javax.json.JsonStructure;
+import javax.json.spi.JsonProvider;
+import javax.json.stream.JsonGenerator;
+import java.io.File;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Documentation("Write a JSON file using JSON-P and taking JsonStructure as items.")
+public class JsonpWriter implements ItemWriter {
+    @Inject
+    @BatchProperty
+    @Documentation("output file")
+    private String file;
+
+    @Inject
+    @BatchProperty
+    @Documentation("output encoding")
+    private String encoding;
+
+    @Inject
+    @BatchProperty
+    @Documentation("comma separated key value pairs for the generator factory (converted to a Map<?, ?>)")
+    private String configuration;
+
+    @Inject
+    @BatchProperty
+    @Documentation("is the array wrapped in an object or not")
+    private String skipRoot;
+
+    @Inject
+    @BatchProperty
+    @Documentation("how to generate field names for each item, default uses item1, item2, ...")
+    private String fieldNameGeneratorClass;
+
+    @Inject
+    @BatchProperty
+    @Documentation("JSON-P provider if not using the default")
+    private String provider;
+
+    private JsonGenerator generator;
+    private TransactionalWriter writer;
+    private FieldNameGenerator fieldNameGenerator = null;
+
+    @Override
+    public void open(final Serializable checkpoint) throws Exception {
+        final ClassLoader loader = Thread.currentThread().getContextClassLoader();
+        final JsonProvider provider = this.provider == null ? JsonProvider.provider() : JsonProvider.class.cast(loader.loadClass(this.provider));
+
+        final File outputFile = new File(file);
+        if (!outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) {
+            throw new BatchRuntimeException("Can't create " + outputFile.getAbsolutePath());
+        }
+
+        writer = new TransactionalWriter(outputFile, encoding, checkpoint);
+        generator = provider.createGeneratorFactory(buildConfig()).createGenerator(writer);
+        if (fieldNameGeneratorClass != null) {
+            if ("default".equals(fieldNameGeneratorClass)) {
+                fieldNameGenerator = new FieldNameGenerator() {
+                    private int count = 0;
+
+                    @Override
+                    public String nextName() {
+                        return "item" + ++count;
+                    }
+                };
+            } else {
+                fieldNameGenerator = FieldNameGenerator.class.cast(Thread.currentThread().getContextClassLoader().loadClass(fieldNameGeneratorClass).newInstance());
+            }
+        }
+
+        if (useGlobalWrapper()) {
+            if (fieldNameGenerator != null) {
+                generator.writeStartObject();
+            } else {
+                generator.writeStartArray();
+            }
+        }
+    }
+
+    private Map<String, ?> buildConfig() {
+        final Map<String, Object> map = new HashMap<String, Object>();
+        if (configuration != null) {
+            for (final String entry : configuration.trim().split(" *, *")) {
+                final String[] parts = entry.split(" *= *");
+                if (parts.length != 2) {
+                    throw new IllegalArgumentException(entry + " not matching a=b pattern");
+                }
+                map.put(parts[0], parts[1]);
+            }
+        }
+        return map;
+    }
+
+    @Override
+    public void close() throws Exception {
+        if (generator != null) {
+            if (useGlobalWrapper()) {
+                generator.writeEnd();
+            }
+            generator.close();
+        }
+    }
+
+    @Override
+    public void writeItems(final List<Object> items) throws Exception {
+        final List<JsonStructure> structures = List.class.cast(items);
+        for (final JsonStructure structure : structures) {
+            if (fieldNameGenerator != null) {
+                generator.write(fieldNameGenerator.nextName(), structure);
+            } else {
+                generator.write(structure);
+            }
+        }
+        writer.flush();
+    }
+
+    @Override
+    public Serializable checkpointInfo() throws Exception {
+        return writer.position();
+    }
+
+    private boolean useGlobalWrapper() {
+        return skipRoot == null || !"true".equalsIgnoreCase(skipRoot);
+    }
+
+    public interface FieldNameGenerator {
+        String nextName();
+    }
+}
diff --git a/extensions/jsonp/src/main/resources/META-INF/batchee.xml b/extensions/jsonp/src/main/resources/META-INF/batchee.xml
new file mode 100644
index 0000000..9208df6
--- /dev/null
+++ b/extensions/jsonp/src/main/resources/META-INF/batchee.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<batch-artifacts xmlns="http://xmlns.jcp.org/xml/ns/javaee">
+  <ref id="jsonpReader" class="org.apache.batchee.jsonp.JsonpReader" />
+  <ref id="jsonpWriter" class="org.apache.batchee.jsonp.JsonpWriter" />
+</batch-artifacts>
diff --git a/extensions/jsonp/src/test/java/org/apache/batchee/jsonp/JsonpReaderTest.java b/extensions/jsonp/src/test/java/org/apache/batchee/jsonp/JsonpReaderTest.java
new file mode 100644
index 0000000..afe69ef
--- /dev/null
+++ b/extensions/jsonp/src/test/java/org/apache/batchee/jsonp/JsonpReaderTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.batchee.jsonp;
+
+import org.apache.batchee.jsonp.util.IOs;
+import org.apache.batchee.util.Batches;
+import org.testng.annotations.Test;
+
+import javax.batch.api.chunk.ItemWriter;
+import javax.batch.operations.JobOperator;
+import javax.batch.runtime.BatchRuntime;
+import javax.json.JsonObject;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+import static org.testng.Assert.assertEquals;
+
+public class JsonpReaderTest {
+    @Test
+    public void read() {
+        IOs.write("target/work/jsonp-input.json", "[" +
+            "  {" +
+            "    \"v1\":\"record 1 # field 1\"," +
+            "    \"v2\":\"record 1 # field 2\"" +
+            "  }," +
+            "  {" +
+            "    \"v1\":\"record 2 # field 1\"," +
+            "    \"v2\":\"record 2 # field 2\"" +
+            "  }" +
+            "]");
+
+        final JobOperator operator = BatchRuntime.getJobOperator();
+        Batches.waitForEnd(operator, operator.start("jsonp-reader", new Properties()));
+        assertEquals(Writer.ITEMS.size(), 2);
+        for (int i = 1; i < Writer.ITEMS.size() + 1; i++) {
+            final JsonObject record = Writer.ITEMS.get(i - 1);
+            assertEquals("record " + i + " # field 1", record.getString("v1"));
+            assertEquals("record " + i + " # field 2", record.getString("v2"));
+        }
+    }
+
+    public static class Writer implements ItemWriter {
+        public static List<JsonObject> ITEMS = new ArrayList<JsonObject>(2);
+
+        @Override
+        public void open(final Serializable checkpoint) throws Exception {
+            // no-op
+        }
+
+        @Override
+        public void close() throws Exception {
+            // no-op
+        }
+
+        @Override
+        public void writeItems(final List<Object> items) throws Exception {
+            ITEMS.addAll(List.class.cast(items));
+        }
+
+        @Override
+        public Serializable checkpointInfo() throws Exception {
+            return null;
+        }
+    }
+}
diff --git a/extensions/jsonp/src/test/java/org/apache/batchee/jsonp/JsonpWriterTest.java b/extensions/jsonp/src/test/java/org/apache/batchee/jsonp/JsonpWriterTest.java
new file mode 100644
index 0000000..b039fc9
--- /dev/null
+++ b/extensions/jsonp/src/test/java/org/apache/batchee/jsonp/JsonpWriterTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.batchee.jsonp;
+
+import org.apache.batchee.jsonp.util.IOs;
+import org.apache.batchee.util.Batches;
+import org.testng.annotations.Test;
+
+import javax.batch.api.chunk.ItemReader;
+import javax.batch.operations.JobOperator;
+import javax.batch.runtime.BatchRuntime;
+import javax.json.Json;
+import java.io.Serializable;
+import java.util.Properties;
+
+import static org.testng.Assert.assertEquals;
+
+public class JsonpWriterTest {
+    @Test
+    public void write() {
+        final JobOperator operator = BatchRuntime.getJobOperator();
+        Batches.waitForEnd(operator, operator.start("jsonp-writer", new Properties()));
+        final String output = IOs.slurp("target/work/jsonp-output.json");
+        assertEquals(output.replace("\n", "").replace("\r", "").replace(" ", "").replace("\t", ""),
+                    "[{\"v1\":\"v11\",\"v2\":\"v21\"},{\"v1\":\"v12\",\"v2\":\"v22\"}]");
+    }
+
+    public static class Reader implements ItemReader {
+        private int count = 0;
+
+        @Override
+        public void open(final Serializable checkpoint) throws Exception {
+            // no-op
+        }
+
+        @Override
+        public void close() throws Exception {
+            // no-op
+        }
+
+        @Override
+        public Object readItem() throws Exception {
+            if (count++ < 2) {
+                return Json.createObjectBuilder().add("v1", "v1" + count).add("v2", "v2" + count).build();
+            }
+            return null;
+        }
+
+        @Override
+        public Serializable checkpointInfo() throws Exception {
+            return null;
+        }
+    }
+}
diff --git a/extensions/jsonp/src/test/java/org/apache/batchee/jsonp/util/IOs.java b/extensions/jsonp/src/test/java/org/apache/batchee/jsonp/util/IOs.java
new file mode 100644
index 0000000..0b807e2
--- /dev/null
+++ b/extensions/jsonp/src/test/java/org/apache/batchee/jsonp/util/IOs.java
@@ -0,0 +1,62 @@
+/*
+ * 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.batchee.jsonp.util;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+
+public class IOs {
+    public static void write(final String path, final String content) {
+        final File file = new File(path);
+        if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
+            throw new RuntimeException("Can't create " + path);
+        }
+
+        try {
+            final FileWriter writer = new FileWriter(file);
+            writer.write(content);
+            writer.close();
+        } catch (final IOException e) {
+            // no-op
+        }
+    }
+
+    public static String slurp(final String path) {
+        final StringBuilder builder = new StringBuilder();
+        try {
+            final BufferedReader reader = new BufferedReader(new FileReader(path));
+            String line;
+            do {
+                line = reader.readLine();
+                if (line != null) {
+                    builder.append(line);
+                }
+            } while (line != null);
+            reader.close();
+        } catch (final Exception e) {
+            // no-op
+        }
+        return builder.toString();
+    }
+
+    private IOs() {
+        // no-op
+    }
+}
diff --git a/extensions/jsonp/src/test/resources/META-INF/batch-jobs/jsonp-reader.xml b/extensions/jsonp/src/test/resources/META-INF/batch-jobs/jsonp-reader.xml
new file mode 100644
index 0000000..f7df4f5
--- /dev/null
+++ b/extensions/jsonp/src/test/resources/META-INF/batch-jobs/jsonp-reader.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  See the NOTICE file distributed with this work for additional information
+  regarding copyright ownership. 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.
+-->
+<job id="jsonp-reader" version="1.0"
+     xmlns="http://xmlns.jcp.org/xml/ns/javaee"
+     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+     xsi:schemaLocation="
+      http://xmlns.jcp.org/xml/ns/javaee
+      http://xmlns.jcp.org/xml/ns/javaee/jobXML_1_0.xsd">
+  <step id="step1">
+    <chunk>
+      <reader ref="jsonpReader">
+        <properties>
+          <property name="file" value="target/work/jsonp-input.json"/>
+        </properties>
+      </reader>
+      <writer ref="org.apache.batchee.jsonp.JsonpReaderTest$Writer" />
+    </chunk>
+  </step>
+</job>
diff --git a/extensions/jsonp/src/test/resources/META-INF/batch-jobs/jsonp-writer.xml b/extensions/jsonp/src/test/resources/META-INF/batch-jobs/jsonp-writer.xml
new file mode 100644
index 0000000..072493f
--- /dev/null
+++ b/extensions/jsonp/src/test/resources/META-INF/batch-jobs/jsonp-writer.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  See the NOTICE file distributed with this work for additional information
+  regarding copyright ownership. 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.
+-->
+<job id="jsonp-writer" version="1.0"
+     xmlns="http://xmlns.jcp.org/xml/ns/javaee"
+     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+     xsi:schemaLocation="
+      http://xmlns.jcp.org/xml/ns/javaee
+      http://xmlns.jcp.org/xml/ns/javaee/jobXML_1_0.xsd">
+  <step id="step1">
+    <chunk>
+      <reader ref="org.apache.batchee.jsonp.JsonpWriterTest$Reader" />
+      <writer ref="jsonpWriter">
+        <properties>
+          <property name="file" value="target/work/jsonp-output.json"/>
+        </properties>
+      </writer>
+    </chunk>
+  </step>
+</job>
diff --git a/extensions/pom.xml b/extensions/pom.xml
index fbb38a6..c990745 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -40,6 +40,7 @@
         <module>hazelcast</module>
         <module>modelmapper</module>
         <module>commons-csv</module>
+        <module>jsonp</module>
         <module>extension-doc-helper</module>
     </modules>