groovy.json.StreamingJsonBuilder Maven / Gradle / Ivy
Show all versions of groovy-all Show documentation
/*
* 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.GroovyObjectSupport;
import java.io.IOException;
import java.io.Writer;
import java.util.*;
/**
* 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 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.
*
* Example:
*
* 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"]}}}'
* }
*
*
* @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 Writer writer;
/**
* Instantiates a JSON builder.
*
* @param writer A writer to which Json will be written
*/
public StreamingJsonBuilder(Writer writer) {
this.writer = writer;
}
/**
* 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
*/
public StreamingJsonBuilder(Writer writer, Object content) throws IOException {
this(writer);
if (content != null) {
writer.write(JsonOutput.toJson(content));
}
}
/**
* Named arguments can be passed to the JSON builder instance to create a root JSON object
*
* Example:
*
* new StringWriter().with { w ->
* 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(JsonOutput.toJson(m));
return m;
}
/**
* The empty args call will create a key whose value will be an empty JSON object:
*
* new StringWriter().with { w ->
* 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(JsonOutput.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 ->
* 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(JsonOutput.toJson(l));
return l;
}
/**
* Varargs elements as arguments to the JSON builder create a root JSON array
*
* Example:
*
* 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]"
* }
*
* @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 ->
* def json = new groovy.json.StreamingJsonBuilder(w)
* json authors, { Author author ->
* 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(Collection coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
StreamingJsonDelegate.writeCollectionWithClosure(writer, coll, c);
return null;
}
/**
* A closure passed to a JSON builder will create a root JSON object
*
* Example:
*
* new StringWriter().with { w ->
* 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(StreamingJsonDelegate.class) Closure c) throws IOException {
writer.write(JsonOutput.OPEN_BRACE);
StreamingJsonDelegate.cloneDelegateAndGetContent(writer, c);
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 ->
* 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(StreamingJsonDelegate.class) Closure c) throws IOException {
writer.write(JsonOutput.OPEN_BRACE);
writer.write(JsonOutput.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 ->
* 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"}]}'
* }
*
* @param coll a collection
* @param c a closure used to convert the objects of coll
*/
public void call(String name, Collection coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
writer.write(JsonOutput.OPEN_BRACE);
writer.write(JsonOutput.toJson(name));
writer.write(JsonOutput.COLON);
call(coll, c);
writer.write(JsonOutput.CLOSE_BRACE);
}
/**
* 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 ->
* 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(StreamingJsonDelegate.class) Closure callable) throws IOException {
writer.write(JsonOutput.OPEN_BRACE);
writer.write(JsonOutput.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;
writer.write(JsonOutput.toJson(entry.getKey()));
writer.write(JsonOutput.COLON);
writer.write(JsonOutput.toJson(entry.getValue()));
}
StreamingJsonDelegate.cloneDelegateAndGetContent(writer, callable, map.size() == 0);
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 classicala builder-style:
*
* 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}}'
* }
*
*
* Or alternatively with a method call taking named arguments:
*
* 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}}'
* }
*
*
* 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 ->
* 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 ->
* 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
*/
public Object invokeMethod(String name, Object args) {
boolean notExpectedArgs = false;
if (args != null && Object[].class.isAssignableFrom(args.getClass())) {
Object[] arr = (Object[]) args;
try {
if (arr.length == 0) {
call(name);
} else if (arr.length == 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;
}
} else if (arr.length == 2 && arr[0] instanceof Map && arr[1] instanceof Closure) {
Map map = (Map) arr[0];
final Closure callable = (Closure) arr[1];
call(name, map, callable);
} else if (StreamingJsonDelegate.isCollectionWithClosure(arr)) {
final Collection coll = (Collection) arr[0];
final Closure callable = (Closure) arr[1];
call(name, coll, callable);
} else {
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 {
private Writer writer;
private boolean first;
private State state;
public StreamingJsonDelegate(Writer w, boolean first) {
this.writer = w;
this.first = first;
}
public Object invokeMethod(String name, Object args) {
if (args != null && Object[].class.isAssignableFrom(args.getClass())) {
try {
Object[] arr = (Object[]) args;
if (arr.length == 1) {
final Object value = arr[0];
call(name, value);
} else if (isCollectionWithClosure(arr)) {
final Collection coll = (Collection) arr[0];
final Closure callable = (Closure) arr[1];
call(name, coll, callable);
} else {
final List