/*
* 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 groovy.json;
import groovy.lang.*;
import java.io.IOException;
import java.io.Writer;
import java.util.*;
/**
* A builder for creating JSON payloads.
* <p>
* This builder supports the usual builder syntax made of nested method calls and closures,
* but also some specific aspects of JSON data structures, such as list of values, etc.
* Please make sure to have a look at the various methods provided by this builder
* to be able to learn about the various possibilities of usage.
* <p>
* Unlike the JsonBuilder class which creates a data structure in memory,
* which is handy in those situations where you want to alter the structure programatically before output,
* the StreamingJsonBuilder streams to a writer directly without any memory data structure.
* So if you don't need to modify the structure, and want a more memory-efficient approach,
* please use the StreamingJsonBuilder.
* <p>
* Example:
* <pre class="groovyTestCase">
* new StringWriter().with { w ->
* def builder = new groovy.json.StreamingJsonBuilder(w)
* builder.people {
* person {
* firstName 'Tim'
* lastName 'Yates'
* // Named arguments are valid values for objects too
* address(
* city: 'Manchester',
* country: 'UK',
* zip: 'M1 2AB',
* )
* living true
* eyes 'left', 'right'
* }
* }
*
* assert w.toString() == '{"people":{"person":{"firstName":"Tim","lastName":"Yates","address":{"city":"Manchester","country":"UK","zip":"M1 2AB"},"living":true,"eyes":["left","right"]}}}'
* }
* </pre>
*
* @author Tim Yates
* @author Andrey Bloschetsov
* @author Graeme Rocher
*
* @since 1.8.1
*/
public class StreamingJsonBuilder extends GroovyObjectSupport {
private static final String DOUBLE_CLOSE_BRACKET = "}}";
private static final String COLON_WITH_OPEN_BRACE = ":{";
private final Writer writer;
private final JsonGenerator generator;
/**
* Instantiates a JSON builder.
*
* @param writer A writer to which Json will be written
*/
public StreamingJsonBuilder(Writer writer) {
this.writer = writer;
generator = JsonOutput.DEFAULT_GENERATOR;
}
/**
* Instantiates a JSON builder with the given generator.
*
* @param writer A writer to which Json will be written
* @param generator used to generate the output
* @since 2.5
*/
public StreamingJsonBuilder(Writer writer, JsonGenerator generator) {
this.writer = writer;
this.generator = generator;
}
/**
* Instantiates a JSON builder, possibly with some existing data structure.
*
* @param writer A writer to which Json will be written
* @param content a pre-existing data structure, default to null
* @throws IOException
*/
public StreamingJsonBuilder(Writer writer, Object content) throws IOException {
this(writer, content, JsonOutput.DEFAULT_GENERATOR);
}
/**
* Instantiates a JSON builder, possibly with some existing data structure and
* the given generator.
*
* @param writer A writer to which Json will be written
* @param content a pre-existing data structure, default to null
* @param generator used to generate the output
* @throws IOException
* @since 2.5
*/
public StreamingJsonBuilder(Writer writer, Object content, JsonGenerator generator) throws IOException {
this.writer = writer;
this.generator = generator;
if (content != null) {
writer.write(generator.toJson(content));
}
}
/**
* Named arguments can be passed to the JSON builder instance to create a root JSON object
* <p>
* Example:
* <pre class="groovyTestCase">
* new StringWriter().with { w ->
* def json = new groovy.json.StreamingJsonBuilder(w)
* json name: "Tim", age: 31
*
* assert w.toString() == '{"name":"Tim","age":31}'
* }
* </pre>
*
* @param m a map of key / value pairs
* @return a map of key / value pairs
*/
public Object call(Map m) throws IOException {
writer.write(generator.toJson(m));
return m;
}
/**
* The empty args call will create a key whose value will be an empty JSON object:
* <pre class="groovyTestCase">
* new StringWriter().with { w ->
* def json = new groovy.json.StreamingJsonBuilder(w)
* json.person()
*
* assert w.toString() == '{"person":{}}'
* }
* </pre>
*
* @param name The name of the empty object to create
* @throws IOException
*/
public void call(String name) throws IOException {
writer.write(generator.toJson(Collections.singletonMap(name, Collections.emptyMap())));
}
/**
* A list of elements as arguments to the JSON builder creates a root JSON array
* <p>
* Example:
* <pre class="groovyTestCase">
* new StringWriter().with { w ->
* def json = new groovy.json.StreamingJsonBuilder(w)
* def result = json([1, 2, 3])
*
* assert result == [ 1, 2, 3 ]
* assert w.toString() == "[1,2,3]"
* }
* </pre>
*
* @param l a list of values
* @return a list of values
*/
public Object call(List l) throws IOException {
writer.write(generator.toJson(l));
return l;
}
/**
* Varargs elements as arguments to the JSON builder create a root JSON array
* <p>
* Example:
* <pre class="groovyTestCase">
* new StringWriter().with { w ->
* def json = new groovy.json.StreamingJsonBuilder(w)
* def result = json 1, 2, 3
*
* assert result instanceof List
* assert w.toString() == "[1,2,3]"
* }
* </pre>
* @param args an array of values
* @return a list of values
*/
public Object call(Object... args) throws IOException {
return call(Arrays.asList(args));
}
/**
* A collection and closure passed to a JSON builder will create a root JSON array applying
* the closure to each object in the collection
* <p>
* Example:
* <pre class="groovyTestCase">
* class Author {
* String name
* }
* def authors = [new Author (name: "Guillaume"), new Author (name: "Jochen"), new Author (name: "Paul")]
*
* new StringWriter().with { w ->
* def json = new groovy.json.StreamingJsonBuilder(w)
* json authors, { Author author ->
* name author.name
* }
*
* assert w.toString() == '[{"name":"Guillaume"},{"name":"Jochen"},{"name":"Paul"}]'
* }
* </pre>
* @param coll a collection
* @param c a closure used to convert the objects of coll
*/
public Object call(Iterable coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
return StreamingJsonDelegate.writeCollectionWithClosure(writer, coll, c, generator);
}
/**
* Delegates to {@link #call(Iterable, Closure)}
*/
public Object call(Collection coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
return call((Iterable)coll, c);
}
/**
* A closure passed to a JSON builder will create a root JSON object
* <p>
* Example:
* <pre class="groovyTestCase">
* new StringWriter().with { w ->
* def json = new groovy.json.StreamingJsonBuilder(w)
* json {
* name "Tim"
* age 39
* }
*
* assert w.toString() == '{"name":"Tim","age":39}'
* }
* </pre>
*
* @param c a closure whose method call statements represent key / values of a JSON object
*/
public Object call(@DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
writer.write(JsonOutput.OPEN_BRACE);
StreamingJsonDelegate.cloneDelegateAndGetContent(writer, c, true, generator);
writer.write(JsonOutput.CLOSE_BRACE);
return null;
}
/**
* A name and a closure passed to a JSON builder will create a key with a JSON object
* <p>
* Example:
* <pre class="groovyTestCase">
* new StringWriter().with { w ->
* def json = new groovy.json.StreamingJsonBuilder(w)
* json.person {
* name "Tim"
* age 39
* }
*
* assert w.toString() == '{"person":{"name":"Tim","age":39}}'
* }
* </pre>
*
* @param name The key for the JSON object
* @param c a closure whose method call statements represent key / values of a JSON object
*/
public void call(String name, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
writer.write(JsonOutput.OPEN_BRACE);
writer.write(generator.toJson(name));
writer.write(JsonOutput.COLON);
call(c);
writer.write(JsonOutput.CLOSE_BRACE);
}
/**
* A name, a collection and closure passed to a JSON builder will create a root JSON array applying
* the closure to each object in the collection
* <p>
* Example:
* <pre class="groovyTestCase">
* class Author {
* String name
* }
* def authors = [new Author (name: "Guillaume"), new Author (name: "Jochen"), new Author (name: "Paul")]
*
* new StringWriter().with { w ->
* def json = new groovy.json.StreamingJsonBuilder(w)
* json.people authors, { Author author ->
* name author.name
* }
*
* assert w.toString() == '{"people":[{"name":"Guillaume"},{"name":"Jochen"},{"name":"Paul"}]}'
* }
* </pre>
* @param coll a collection
* @param c a closure used to convert the objects of coll
*/
public void call(String name, Iterable coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
writer.write(JsonOutput.OPEN_BRACE);
writer.write(generator.toJson(name));
writer.write(JsonOutput.COLON);
call(coll, c);
writer.write(JsonOutput.CLOSE_BRACE);
}
/**
* Delegates to {@link #call(String, Iterable, Closure)}
*/
public void call(String name, Collection coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
call(name, (Iterable)coll, c);
}
/**
* If you use named arguments and a closure as last argument,
* the key/value pairs of the map (as named arguments)
* and the key/value pairs represented in the closure
* will be merged together —
* the closure properties overriding the map key/values
* in case the same key is used.
*
* <pre class="groovyTestCase">
* new StringWriter().with { w ->
* def json = new groovy.json.StreamingJsonBuilder(w)
* json.person(name: "Tim", age: 35) { town "Manchester" }
*
* assert w.toString() == '{"person":{"name":"Tim","age":35,"town":"Manchester"}}'
* }
* </pre>
*
* @param name The name of the JSON object
* @param map The attributes of the JSON object
* @param callable Additional attributes of the JSON object represented by the closure
* @throws IOException
*/
public void call(String name, Map map, @DelegatesTo(StreamingJsonDelegate.class) Closure callable) throws IOException {
writer.write(JsonOutput.OPEN_BRACE);
writer.write(generator.toJson(name));
writer.write(COLON_WITH_OPEN_BRACE);
boolean first = true;
for (Object it : map.entrySet()) {
if (!first) {
writer.write(JsonOutput.COMMA);
} else {
first = false;
}
Map.Entry entry = (Map.Entry) it;
String key = entry.getKey().toString();
if (generator.isExcludingFieldsNamed(key)) {
continue;
}
Object value = entry.getValue();
if (generator.isExcludingValues(value)) {
return;
}
writer.write(generator.toJson(key));
writer.write(JsonOutput.COLON);
writer.write(generator.toJson(value));
}
StreamingJsonDelegate.cloneDelegateAndGetContent(writer, callable, map.size() == 0, generator);
writer.write(DOUBLE_CLOSE_BRACKET);
}
/**
* A method call on the JSON builder instance will create a root object with only one key
* whose name is the name of the method being called.
* This method takes as arguments:
* <ul>
* <li>a closure</li>
* <li>a map (ie. named arguments)</li>
* <li>a map and a closure</li>
* <li>or no argument at all</li>
* </ul>
* <p>
* Example with a classical builder-style:
* <pre class="groovyTestCase">
* new StringWriter().with { w ->
* def json = new groovy.json.StreamingJsonBuilder(w)
* json.person {
* name "Tim"
* age 28
* }
*
* assert w.toString() == '{"person":{"name":"Tim","age":28}}'
* }
* </pre>
*
* Or alternatively with a method call taking named arguments:
* <pre class="groovyTestCase">
* new StringWriter().with { w ->
* def json = new groovy.json.StreamingJsonBuilder(w)
* json.person name: "Tim", age: 32
*
* assert w.toString() == '{"person":{"name":"Tim","age":32}}'
* }
* </pre>
*
* If you use named arguments and a closure as last argument,
* the key/value pairs of the map (as named arguments)
* and the key/value pairs represented in the closure
* will be merged together —
* the closure properties overriding the map key/values
* in case the same key is used.
* <pre class="groovyTestCase">
* new StringWriter().with { w ->
* def json = new groovy.json.StreamingJsonBuilder(w)
* json.person(name: "Tim", age: 35) { town "Manchester" }
*
* assert w.toString() == '{"person":{"name":"Tim","age":35,"town":"Manchester"}}'
* }
* </pre>
*
* The empty args call will create a key whose value will be an empty JSON object:
* <pre class="groovyTestCase">
* new StringWriter().with { w ->
* def json = new groovy.json.StreamingJsonBuilder(w)
* json.person()
*
* assert w.toString() == '{"person":{}}'
* }
* </pre>
*
* @param name the single key
* @param args the value associated with the key
*/
public Object invokeMethod(String name, Object args) {
boolean notExpectedArgs = false;
if (args != null && Object[].class.isAssignableFrom(args.getClass())) {
Object[] arr = (Object[]) args;
try {
switch(arr.length) {
case 0:
call(name);
break;
case 1:
if (arr[0] instanceof Closure) {
final Closure callable = (Closure) arr[0];
call(name, callable);
} else if (arr[0] instanceof Map) {
final Map<String, Map> map = Collections.singletonMap(name, (Map) arr[0]);
call(map);
} else {
notExpectedArgs = true;
}
break;
case 2:
final Object first = arr[0];
final Object second = arr[1];
final boolean isClosure = second instanceof Closure;
if(isClosure && first instanceof Map ) {
final Closure callable = (Closure) second;
call(name, (Map)first, callable);
}
else if(isClosure && first instanceof Iterable) {
final Iterable coll = (Iterable) first;
final Closure callable = (Closure) second;
call(name, coll, callable);
}
else if(isClosure && first.getClass().isArray()) {
final Iterable coll = Arrays.asList((Object[])first);
final Closure callable = (Closure) second;
call(name, coll, callable);
}
else {
notExpectedArgs = true;
}
break;
default:
notExpectedArgs = true;
}
} catch (IOException ioe) {
throw new JsonException(ioe);
}
} else {
notExpectedArgs = true;
}
if (!notExpectedArgs) {
return this;
} else {
throw new JsonException("Expected no arguments, a single map, a single closure, or a map and closure as arguments.");
}
}
/**
* The delegate used when invoking closures
*/
public static class StreamingJsonDelegate extends GroovyObjectSupport {
protected final Writer writer;
protected boolean first;
protected State state;
private final JsonGenerator generator;
public StreamingJsonDelegate(Writer w, boolean first) {
this(w, first, null);
}
StreamingJsonDelegate(Writer w, boolean first, JsonGenerator generator) {
this.writer = w;
this.first = first;
this.generator = (generator != null) ? generator : JsonOutput.DEFAULT_GENERATOR;
}
/**
* @return Obtains the current writer
*/
public Writer getWriter() {
return writer;
}
public Object invokeMethod(String name, Object args) {
if (args != null && Object[].class.isAssignableFrom(args.getClass())) {
try {
Object[] arr = (Object[]) args;
final int len = arr.length;
switch (len) {
case 1:
final Object value = arr[0];
if(value instanceof Closure) {
call(name, (Closure)value);
}
else if(value instanceof Writable) {
call(name, (Writable)value);
}
else {
call(name, value);
}
return null;
case 2:
if(arr[len -1] instanceof Closure) {
final Object obj = arr[0];
final Closure callable = (Closure) arr[1];
if(obj instanceof Iterable) {
call(name, (Iterable)obj, callable);
return null;
}
else if(obj.getClass().isArray()) {
call(name, Arrays.asList( (Object[])obj), callable);
return null;
}
else {
call(name, obj, callable);
return null;
}
}
default:
final List<Object> list = Arrays.asList(arr);
call(name, list);
}
} catch (IOException ioe) {
throw new JsonException(ioe);
}
}
return this;
}
/**
* Writes the name and a JSON array
* @param name The name of the JSON attribute
* @param list The list representing the array
* @throws IOException
*/
public void call(String name, List<Object> list) throws IOException {
if (generator.isExcludingFieldsNamed(name)) {
return;
}
writeName(name);
writeArray(list);
}
/**
* Writes the name and a JSON array
* @param name The name of the JSON attribute
* @param array The list representing the array
* @throws IOException
*/
public void call(String name, Object...array) throws IOException {
if (generator.isExcludingFieldsNamed(name)) {
return;
}
writeName(name);
writeArray(Arrays.asList(array));
}
/**
* A collection and closure passed to a JSON builder will create a root JSON array applying
* the closure to each object in the collection
* <p>
* Example:
* <pre class="groovyTestCase">
* class Author {
* String name
* }
* def authorList = [new Author (name: "Guillaume"), new Author (name: "Jochen"), new Author (name: "Paul")]
*
* new StringWriter().with { w ->
* def json = new groovy.json.StreamingJsonBuilder(w)
* json.book {
* authors authorList, { Author author ->
* name author.name
* }
* }
*
* assert w.toString() == '{"book":{"authors":[{"name":"Guillaume"},{"name":"Jochen"},{"name":"Paul"}]}}'
* }
* </pre>
* @param coll a collection
* @param c a closure used to convert the objects of coll
*/
public void call(String name, Iterable coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
if (generator.isExcludingFieldsNamed(name)) {
return;
}
writeName(name);
writeObjects(coll, c);
}
/**
* Delegates to {@link #call(String, Iterable, Closure)}
*/
public void call(String name, Collection coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
call(name, (Iterable)coll, c);
}
/**
* Writes the name and value of a JSON attribute
*
* @param name The attribute name
* @param value The value
* @throws IOException
*/
public void call(String name, Object value) throws IOException {
if (generator.isExcludingFieldsNamed(name) || generator.isExcludingValues(value)) {
return;
}
writeName(name);
writeValue(value);
}
/**
* Writes the name and value of a JSON attribute
*
* @param name The attribute name
* @param value The value
* @throws IOException
*/
public void call(String name, Object value, @DelegatesTo(StreamingJsonDelegate.class) Closure callable) throws IOException {
if (generator.isExcludingFieldsNamed(name)) {
return;
}
writeName(name);
verifyValue();
writeObject(writer, value, callable, generator);
}
/**
* Writes the name and another JSON object
*
* @param name The attribute name
* @param value The value
* @throws IOException
*/
public void call(String name,@DelegatesTo(StreamingJsonDelegate.class) Closure value) throws IOException {
if (generator.isExcludingFieldsNamed(name)) {
return;
}
writeName(name);
verifyValue();
writer.write(JsonOutput.OPEN_BRACE);
StreamingJsonDelegate.cloneDelegateAndGetContent(writer, value, true, generator);
writer.write(JsonOutput.CLOSE_BRACE);
}
/**
* Writes an unescaped value. Note: can cause invalid JSON if passed JSON is invalid
*
* @param name The attribute name
* @param json The value
* @throws IOException
*/
public void call(String name, JsonOutput.JsonUnescaped json) throws IOException {
if (generator.isExcludingFieldsNamed(name)) {
return;
}
writeName(name);
verifyValue();
writer.write(json.toString());
}
/**
* Writes the given Writable as the value of the given attribute name
*
* @param name The attribute name
* @param json The writable value
* @throws IOException
*/
public void call(String name, Writable json) throws IOException {
writeName(name);
verifyValue();
if(json instanceof GString) {
writer.write(generator.toJson(json.toString()));
}
else {
json.writeTo(writer);
}
}
private void writeObjects(Iterable coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
verifyValue();
writeCollectionWithClosure(writer, coll, c, generator);
}
protected void verifyValue() {
if(state == State.VALUE) {
throw new IllegalStateException("Cannot write value when value has just been written. Write a name first!");
}
else {
state = State.VALUE;
}
}
protected void writeName(String name) throws IOException {
if (generator.isExcludingFieldsNamed(name)) {
return;
}
if(state == State.NAME) {
throw new IllegalStateException("Cannot write a name when a name has just been written. Write a value first!");
}
else {
this.state = State.NAME;
}
if (!first) {
writer.write(JsonOutput.COMMA);
} else {
first = false;
}
writer.write(generator.toJson(name));
writer.write(JsonOutput.COLON);
}
protected void writeValue(Object value) throws IOException {
if (generator.isExcludingValues(value)) {
return;
}
verifyValue();
writer.write(generator.toJson(value));
}
protected void writeArray(List<Object> list) throws IOException {
verifyValue();
writer.write(generator.toJson(list));
}
public static boolean isCollectionWithClosure(Object[] args) {
return args.length == 2 && args[0] instanceof Iterable && args[1] instanceof Closure;
}
public static Object writeCollectionWithClosure(Writer writer, Collection coll, @DelegatesTo(StreamingJsonDelegate.class) Closure closure) throws IOException {
return writeCollectionWithClosure(writer, (Iterable)coll, closure, JsonOutput.DEFAULT_GENERATOR);
}
private static Object writeCollectionWithClosure(Writer writer, Iterable coll, @DelegatesTo(StreamingJsonDelegate.class) Closure closure, JsonGenerator generator)
throws IOException {
writer.write(JsonOutput.OPEN_BRACKET);
boolean first = true;
for (Object it : coll) {
if (!first) {
writer.write(JsonOutput.COMMA);
} else {
first = false;
}
writeObject(writer, it, closure, generator);
}
writer.write(JsonOutput.CLOSE_BRACKET);
return writer;
}
private static void writeObject(Writer writer, Object object, Closure closure, JsonGenerator generator) throws IOException {
writer.write(JsonOutput.OPEN_BRACE);
curryDelegateAndGetContent(writer, closure, object, true, generator);
writer.write(JsonOutput.CLOSE_BRACE);
}
public static void cloneDelegateAndGetContent(Writer w, @DelegatesTo(StreamingJsonDelegate.class) Closure c)
{
cloneDelegateAndGetContent(w, c, true);
}
public static void cloneDelegateAndGetContent(Writer w, @DelegatesTo(StreamingJsonDelegate.class) Closure c, boolean first) {
cloneDelegateAndGetContent(w, c, first, JsonOutput.DEFAULT_GENERATOR);
}
private static void cloneDelegateAndGetContent(Writer w, @DelegatesTo(StreamingJsonDelegate.class) Closure c, boolean first, JsonGenerator generator) {
StreamingJsonDelegate delegate = new StreamingJsonDelegate(w, first, generator);
Closure cloned = (Closure) c.clone();
cloned.setDelegate(delegate);
cloned.setResolveStrategy(Closure.DELEGATE_FIRST);
cloned.call();
}
public static void curryDelegateAndGetContent(Writer w, @DelegatesTo(StreamingJsonDelegate.class) Closure c, Object o) {
curryDelegateAndGetContent(w, c, o, true);
}
public static void curryDelegateAndGetContent(Writer w, @DelegatesTo(StreamingJsonDelegate.class) Closure c, Object o, boolean first) {
curryDelegateAndGetContent(w, c, o, first, JsonOutput.DEFAULT_GENERATOR);
}
private static void curryDelegateAndGetContent(Writer w, @DelegatesTo(StreamingJsonDelegate.class) Closure c, Object o, boolean first, JsonGenerator generator) {
StreamingJsonDelegate delegate = new StreamingJsonDelegate(w, first, generator);
Closure curried = c.curry(o);
curried.setDelegate(delegate);
curried.setResolveStrategy(Closure.DELEGATE_FIRST);
curried.call();
}
private enum State {
NAME, VALUE
}
}
}