groovy.json.StreamingJsonBuilder Maven / Gradle / Ivy
/*
* 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.Closure;
import groovy.lang.DelegatesTo;
import groovy.lang.GString;
import groovy.lang.GroovyObjectSupport;
import groovy.lang.Writable;
import java.io.IOException;
import java.io.Writer;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* A builder for creating JSON payloads.
*
* 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.
*
* Unlike the JsonBuilder class which creates a data structure in memory,
* which is handy in those situations where you want to alter the structure programmatically 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.
*
* Example:
*
* new StringWriter().with { w {@code ->}
* 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"]}}}'
* }
*
*
* @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.0
*/
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.0
*/
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
*
* Example:
*
* new StringWriter().with { w {@code ->}
* def json = new groovy.json.StreamingJsonBuilder(w)
* json name: "Tim", age: 31
*
* assert w.toString() == '{"name":"Tim","age":31}'
* }
*
*
* @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:
*
* new StringWriter().with { w {@code ->}
* def json = new groovy.json.StreamingJsonBuilder(w)
* json.person()
*
* assert w.toString() == '{"person":{}}'
* }
*
*
* @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
*
* Example:
*
* new StringWriter().with { w {@code ->}
* def json = new groovy.json.StreamingJsonBuilder(w)
* def result = json([1, 2, 3])
*
* assert result == [ 1, 2, 3 ]
* assert w.toString() == "[1,2,3]"
* }
*
*
* @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
*
* Example:
*
* new StringWriter().with { w {@code ->}
* def json = new groovy.json.StreamingJsonBuilder(w)
* def result = json 1, 2, 3
*
* assert result instanceof List
* assert w.toString() == "[1,2,3]"
* }
*
* @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
*
* Example:
*
* class Author {
* String name
* }
* def authors = [new Author (name: "Guillaume"), new Author (name: "Jochen"), new Author (name: "Paul")]
*
* new StringWriter().with { w {@code ->}
* def json = new groovy.json.StreamingJsonBuilder(w)
* json authors, { Author author {@code ->}
* name author.name
* }
*
* assert w.toString() == '[{"name":"Guillaume"},{"name":"Jochen"},{"name":"Paul"}]'
* }
*
* @param coll a collection
* @param c a closure used to convert the objects of coll
*/
public Object call(Iterable coll, @DelegatesTo(value = StreamingJsonDelegate.class, strategy = Closure.DELEGATE_FIRST) Closure c) throws IOException {
return StreamingJsonDelegate.writeCollectionWithClosure(writer, coll, c, generator);
}
/**
* Delegates to {@link #call(Iterable, Closure)}
*/
public Object call(Collection coll, @DelegatesTo(value = StreamingJsonDelegate.class, strategy = Closure.DELEGATE_FIRST) Closure c) throws IOException {
return call((Iterable) coll, c);
}
/**
* A closure passed to a JSON builder will create a root JSON object
*
* Example:
*
* new StringWriter().with { w {@code ->}
* def json = new groovy.json.StreamingJsonBuilder(w)
* json {
* name "Tim"
* age 39
* }
*
* assert w.toString() == '{"name":"Tim","age":39}'
* }
*
*
* @param c a closure whose method call statements represent key / values of a JSON object
*/
public Object call(@DelegatesTo(value = StreamingJsonDelegate.class, strategy = Closure.DELEGATE_FIRST) 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
*
* Example:
*
* new StringWriter().with { w {@code ->}
* def json = new groovy.json.StreamingJsonBuilder(w)
* json.person {
* name "Tim"
* age 39
* }
*
* assert w.toString() == '{"person":{"name":"Tim","age":39}}'
* }
*
*
* @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(value = StreamingJsonDelegate.class, strategy = Closure.DELEGATE_FIRST) 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
*
* Example:
*
* class Author {
* String name
* }
* def authors = [new Author (name: "Guillaume"), new Author (name: "Jochen"), new Author (name: "Paul")]
*
* new StringWriter().with { w {@code ->}
* def json = new groovy.json.StreamingJsonBuilder(w)
* json.people authors, { Author author {@code ->}
* name author.name
* }
*
* assert w.toString() == '{"people":[{"name":"Guillaume"},{"name":"Jochen"},{"name":"Paul"}]}'
* }
*
* @param coll a collection
* @param c a closure used to convert the objects of coll
*/
public void call(String name, Iterable coll, @DelegatesTo(value = StreamingJsonDelegate.class, strategy = Closure.DELEGATE_FIRST) 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(value = StreamingJsonDelegate.class, strategy = Closure.DELEGATE_FIRST) 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.
*
*
* new StringWriter().with { w {@code ->}
* 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"}}'
* }
*
*
* @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(value = StreamingJsonDelegate.class, strategy = Closure.DELEGATE_FIRST) 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:
*
* - a closure
* - a map (ie. named arguments)
* - a map and a closure
* - or no argument at all
*
*
* Example with a classical builder-style:
*
* new StringWriter().with { w {@code ->}
* def json = new groovy.json.StreamingJsonBuilder(w)
* json.person {
* name "Tim"
* age 28
* }
*
* assert w.toString() == '{"person":{"name":"Tim","age":28}}'
* }
*
*
* Or alternatively with a method call taking named arguments:
*
* new StringWriter().with { w {@code ->}
* def json = new groovy.json.StreamingJsonBuilder(w)
* json.person name: "Tim", age: 32
*
* assert w.toString() == '{"person":{"name":"Tim","age":32}}'
* }
*
*
* 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.
*
* new StringWriter().with { w {@code ->}
* 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"}}'
* }
*
*
* The empty args call will create a key whose value will be an empty JSON object:
*
* new StringWriter().with { w {@code ->}
* def json = new groovy.json.StreamingJsonBuilder(w)
* json.person()
*
* assert w.toString() == '{"person":{}}'
* }
*
*
* @param name the single key
* @param args the value associated with the key
*/
@Override
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 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);
}
public 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;
}
@Override
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;
}
}
// fall through
default:
final List