/**
* 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.GroovyObjectSupport;
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
* @since 1.8.1
*/
public class StreamingJsonBuilder extends GroovyObjectSupport {
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
* <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(JsonOutput.toJson(m));
return m;
}
/**
* 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(JsonOutput.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(Collection coll, Closure c) throws IOException {
StreamingJsonDelegate.writeCollectionWithClosure(writer, coll, c);
return null;
}
/**
* 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(Closure c) throws IOException {
writer.write("{");
StreamingJsonDelegate.cloneDelegateAndGetContent(writer, c);
writer.write("}");
return null;
}
/**
* 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 classicala 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 {
if (arr.length == 0) {
writer.write(JsonOutput.toJson(Collections.singletonMap(name, Collections.emptyMap())));
} else if (arr.length == 1) {
if (arr[0] instanceof Closure) {
writer.write("{");
writer.write(JsonOutput.toJson(name));
writer.write(":");
call((Closure) arr[0]);
writer.write("}");
} else if (arr[0] instanceof Map) {
writer.write(JsonOutput.toJson(Collections.singletonMap(name, (Map) arr[0])));
} else {
notExpectedArgs = true;
}
} else if (arr.length == 2 && arr[0] instanceof Map && arr[1] instanceof Closure) {
writer.write("{");
writer.write(JsonOutput.toJson(name));
writer.write(":{");
boolean first = true;
Map map = (Map) arr[0];
for (Object it : map.entrySet()) {
if (!first) {
writer.write(",");
} else {
first = false;
}
Map.Entry entry = (Map.Entry) it;
writer.write(JsonOutput.toJson(entry.getKey()));
writer.write(":");
writer.write(JsonOutput.toJson(entry.getValue()));
}
StreamingJsonDelegate.cloneDelegateAndGetContent(writer, (Closure) arr[1], map.size() == 0);
writer.write("}}");
} else if (StreamingJsonDelegate.isCollectionWithClosure(arr)) {
writer.write("{");
writer.write(JsonOutput.toJson(name));
writer.write(":");
call((Collection) arr[0], (Closure) arr[1]);
writer.write("}");
} 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.");
}
}
}
class StreamingJsonDelegate extends GroovyObjectSupport {
private Writer writer;
private boolean first;
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 {
if (!first) {
writer.write(",");
} else {
first = false;
}
writer.write(JsonOutput.toJson(name));
writer.write(":");
Object[] arr = (Object[]) args;
if (arr.length == 1) {
writer.write(JsonOutput.toJson(arr[0]));
} else if (isCollectionWithClosure(arr)) {
writeCollectionWithClosure(writer, (Collection) arr[0], (Closure) arr[1]);
} else {
writer.write(JsonOutput.toJson(Arrays.asList(arr)));
}
} catch (IOException ioe) {
throw new JsonException(ioe);
}
}
return this;
}
public static boolean isCollectionWithClosure(Object[] args) {
return args.length == 2 && args[0] instanceof Collection && args[1] instanceof Closure;
}
public static Object writeCollectionWithClosure(Writer writer, Collection coll, Closure closure) throws IOException {
writer.write("[");
boolean first = true;
for (Object it : coll) {
if (!first) {
writer.write(",");
} else {
first = false;
}
writer.write("{");
curryDelegateAndGetContent(writer, closure, it);
writer.write("}");
}
writer.write("]");
return writer;
}
public static void cloneDelegateAndGetContent(Writer w, Closure c) {
cloneDelegateAndGetContent(w, c, true);
}
public static void cloneDelegateAndGetContent(Writer w, Closure c, boolean first) {
StreamingJsonDelegate delegate = new StreamingJsonDelegate(w, first);
Closure cloned = (Closure) c.clone();
cloned.setDelegate(delegate);
cloned.setResolveStrategy(Closure.DELEGATE_FIRST);
cloned.call();
}
public static void curryDelegateAndGetContent(Writer w, Closure c, Object o) {
curryDelegateAndGetContent(w, c, o, true);
}
public static void curryDelegateAndGetContent(Writer w, Closure c, Object o, boolean first) {
StreamingJsonDelegate delegate = new StreamingJsonDelegate(w, first);
Closure curried = c.curry(o);
curried.setDelegate(delegate);
curried.setResolveStrategy(Closure.DELEGATE_FIRST);
curried.call();
}
}