You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by cc...@apache.org on 2015/09/13 11:32:16 UTC

[2/2] incubator-groovy git commit: Improve StreamingJsonBuilder support for @CompileStatic

Improve StreamingJsonBuilder support for @CompileStatic


Project: http://git-wip-us.apache.org/repos/asf/incubator-groovy/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-groovy/commit/71dc7622
Tree: http://git-wip-us.apache.org/repos/asf/incubator-groovy/tree/71dc7622
Diff: http://git-wip-us.apache.org/repos/asf/incubator-groovy/diff/71dc7622

Branch: refs/heads/master
Commit: 71dc7622a8c7c3a066a118ac6147e151dba82071
Parents: 1eda6ed
Author: Graeme Rocher <gr...@gmail.com>
Authored: Fri Aug 21 12:45:49 2015 +0200
Committer: Cedric Champeau <cc...@apache.org>
Committed: Sun Sep 13 11:29:57 2015 +0200

----------------------------------------------------------------------
 .../src/main/java/groovy/json/JsonOutput.java   |  80 ++--
 .../java/groovy/json/StreamingJsonBuilder.java  | 413 ++++++++++++++-----
 .../groovy/json/StreamingJsonBuilderTest.groovy |  41 ++
 3 files changed, 404 insertions(+), 130 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/71dc7622/subprojects/groovy-json/src/main/java/groovy/json/JsonOutput.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/groovy/json/JsonOutput.java b/subprojects/groovy-json/src/main/java/groovy/json/JsonOutput.java
index d186c75..503f79c 100644
--- a/subprojects/groovy-json/src/main/java/groovy/json/JsonOutput.java
+++ b/subprojects/groovy-json/src/main/java/groovy/json/JsonOutput.java
@@ -40,11 +40,28 @@ import java.util.*;
  * @author Roshan Dawrani
  * @author Andrey Bloschetsov
  * @author Rick Hightower
+ * @author Graeme Rocher
  *
  * @since 1.8.0
  */
 public class JsonOutput {
 
+    static final char OPEN_BRACKET = '[';
+    static final char CLOSE_BRACKET = ']';
+    static final char OPEN_BRACE = '{';
+    static final char CLOSE_BRACE = '}';
+    static final char COLON = ':';
+    static final char COMMA = ',';
+    static final char SPACE = ' ';
+    static final char NEW_LINE = '\n';
+    static final char QUOTE = '"';
+
+    private static final char[] EMPTY_STRING_CHARS = Chr.array(QUOTE, QUOTE);
+
+    private static final String NULL_VALUE = "null";
+    private static final String JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
+    private static final String DEFAULT_TIMEZONE = "GMT";
+
     /**
      * @return "true" or "false" for a boolean value
      */
@@ -55,8 +72,6 @@ public class JsonOutput {
         return buffer.toString();
     }
 
-    private static final String NULL_VALUE = "null";
-
     /**
      * @return a string representation for a number
      * @throws JsonException if the number is infinite or not a number.
@@ -312,7 +327,6 @@ public class JsonOutput {
         }
     }
 
-    private static final char[] EMPTY_STRING_CHARS = Chr.array('"', '"');
 
     /**
      * Serializes any char sequence and writes it into specified buffer.
@@ -329,8 +343,8 @@ public class JsonOutput {
      * Serializes date and writes it into specified buffer.
      */
     private static void writeDate(Date date, CharBuf buffer) {
-        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
-        formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
+        SimpleDateFormat formatter = new SimpleDateFormat(JSON_DATE_FORMAT, Locale.US);
+        formatter.setTimeZone(TimeZone.getTimeZone(DEFAULT_TIMEZONE));
         buffer.addQuoted(formatter.format(date));
     }
 
@@ -338,13 +352,13 @@ public class JsonOutput {
      * Serializes array and writes it into specified buffer.
      */
     private static void writeArray(Class<?> arrayClass, Object array, CharBuf buffer) {
-        buffer.addChar('[');
+        buffer.addChar(OPEN_BRACKET);
         if (Object[].class.isAssignableFrom(arrayClass)) {
             Object[] objArray = (Object[]) array;
             if (objArray.length > 0) {
                 writeObject(objArray[0], buffer);
                 for (int i = 1; i < objArray.length; i++) {
-                    buffer.addChar(',');
+                    buffer.addChar(COMMA);
                     writeObject(objArray[i], buffer);
                 }
             }
@@ -353,7 +367,7 @@ public class JsonOutput {
             if (intArray.length > 0) {
                 buffer.addInt(intArray[0]);
                 for (int i = 1; i < intArray.length; i++) {
-                    buffer.addChar(',').addInt(intArray[i]);
+                    buffer.addChar(COMMA).addInt(intArray[i]);
                 }
             }
         } else if (long[].class.isAssignableFrom(arrayClass)) {
@@ -361,7 +375,7 @@ public class JsonOutput {
             if (longArray.length > 0) {
                 buffer.addLong(longArray[0]);
                 for (int i = 1; i < longArray.length; i++) {
-                    buffer.addChar(',').addLong(longArray[i]);
+                    buffer.addChar(COMMA).addLong(longArray[i]);
                 }
             }
         } else if (boolean[].class.isAssignableFrom(arrayClass)) {
@@ -369,7 +383,7 @@ public class JsonOutput {
             if (booleanArray.length > 0) {
                 buffer.addBoolean(booleanArray[0]);
                 for (int i = 1; i < booleanArray.length; i++) {
-                    buffer.addChar(',').addBoolean(booleanArray[i]);
+                    buffer.addChar(COMMA).addBoolean(booleanArray[i]);
                 }
             }
         } else if (char[].class.isAssignableFrom(arrayClass)) {
@@ -377,7 +391,7 @@ public class JsonOutput {
             if (charArray.length > 0) {
                 buffer.addJsonEscapedString(Chr.array(charArray[0]));
                 for (int i = 1; i < charArray.length; i++) {
-                    buffer.addChar(',').addJsonEscapedString(Chr.array(charArray[i]));
+                    buffer.addChar(COMMA).addJsonEscapedString(Chr.array(charArray[i]));
                 }
             }
         } else if (double[].class.isAssignableFrom(arrayClass)) {
@@ -385,7 +399,7 @@ public class JsonOutput {
             if (doubleArray.length > 0) {
                 buffer.addDouble(doubleArray[0]);
                 for (int i = 1; i < doubleArray.length; i++) {
-                    buffer.addChar(',').addDouble(doubleArray[i]);
+                    buffer.addChar(COMMA).addDouble(doubleArray[i]);
                 }
             }
         } else if (float[].class.isAssignableFrom(arrayClass)) {
@@ -393,7 +407,7 @@ public class JsonOutput {
             if (floatArray.length > 0) {
                 buffer.addFloat(floatArray[0]);
                 for (int i = 1; i < floatArray.length; i++) {
-                    buffer.addChar(',').addFloat(floatArray[i]);
+                    buffer.addChar(COMMA).addFloat(floatArray[i]);
                 }
             }
         } else if (byte[].class.isAssignableFrom(arrayClass)) {
@@ -401,7 +415,7 @@ public class JsonOutput {
             if (byteArray.length > 0) {
                 buffer.addByte(byteArray[0]);
                 for (int i = 1; i < byteArray.length; i++) {
-                    buffer.addChar(',').addByte(byteArray[i]);
+                    buffer.addChar(COMMA).addByte(byteArray[i]);
                 }
             }
         } else if (short[].class.isAssignableFrom(arrayClass)) {
@@ -409,21 +423,21 @@ public class JsonOutput {
             if (shortArray.length > 0) {
                 buffer.addShort(shortArray[0]);
                 for (int i = 1; i < shortArray.length; i++) {
-                    buffer.addChar(',').addShort(shortArray[i]);
+                    buffer.addChar(COMMA).addShort(shortArray[i]);
                 }
             }
         }
-        buffer.addChar(']');
+        buffer.addChar(CLOSE_BRACKET);
     }
 
-    private static final char[] EMPTY_MAP_CHARS = {'{', '}'};
+    private static final char[] EMPTY_MAP_CHARS = {OPEN_BRACE, CLOSE_BRACE};
 
     /**
      * Serializes map and writes it into specified buffer.
      */
     private static void writeMap(Map<?, ?> map, CharBuf buffer) {
         if (!map.isEmpty()) {
-            buffer.addChar('{');
+            buffer.addChar(OPEN_BRACE);
             boolean firstItem = true;
             for (Map.Entry<?, ?> entry : map.entrySet()) {
                 if (entry.getKey() == null) {
@@ -431,7 +445,7 @@ public class JsonOutput {
                 }
 
                 if (!firstItem) {
-                    buffer.addChar(',');
+                    buffer.addChar(COMMA);
                 } else {
                     firstItem = false;
                 }
@@ -439,28 +453,28 @@ public class JsonOutput {
                 buffer.addJsonFieldName(entry.getKey().toString());
                 writeObject(entry.getValue(), buffer);
             }
-            buffer.addChar('}');
+            buffer.addChar(CLOSE_BRACE);
         } else {
             buffer.addChars(EMPTY_MAP_CHARS);
         }
     }
 
-    private static final char[] EMPTY_LIST_CHARS = {'[', ']'};
+    private static final char[] EMPTY_LIST_CHARS = {OPEN_BRACKET, CLOSE_BRACKET};
 
     /**
      * Serializes iterator and writes it into specified buffer.
      */
     private static void writeIterator(Iterator<?> iterator, CharBuf buffer) {
         if (iterator.hasNext()) {
-            buffer.addChar('[');
+            buffer.addChar(OPEN_BRACKET);
             Object it = iterator.next();
             writeObject(it, buffer);
             while (iterator.hasNext()) {
                 it = iterator.next();
-                buffer.addChar(',');
+                buffer.addChar(COMMA);
                 writeObject(it, buffer);
             }
-            buffer.addChar(']');
+            buffer.addChar(CLOSE_BRACKET);
         } else {
             buffer.addChars(EMPTY_LIST_CHARS);
         }
@@ -485,38 +499,38 @@ public class JsonOutput {
             switch (token.getType()) {
                 case OPEN_CURLY:
                     indentSize += 4;
-                    output.addChars(Chr.array('{', '\n')).addChars(getIndent(indentSize, indentCache));
+                    output.addChars(Chr.array(OPEN_BRACE, NEW_LINE)).addChars(getIndent(indentSize, indentCache));
 
                     break;
                 case CLOSE_CURLY:
                     indentSize -= 4;
-                    output.addChar('\n');
+                    output.addChar(NEW_LINE);
                     if (indentSize > 0) {
                         output.addChars(getIndent(indentSize, indentCache));
                     }
-                    output.addChar('}');
+                    output.addChar(CLOSE_BRACE);
 
                     break;
                 case OPEN_BRACKET:
                     indentSize += 4;
-                    output.addChars(Chr.array('[', '\n')).addChars(getIndent(indentSize, indentCache));
+                    output.addChars(Chr.array(OPEN_BRACKET, NEW_LINE)).addChars(getIndent(indentSize, indentCache));
 
                     break;
                 case CLOSE_BRACKET:
                     indentSize -= 4;
-                    output.addChar('\n');
+                    output.addChar(NEW_LINE);
                     if (indentSize > 0) {
                         output.addChars(getIndent(indentSize, indentCache));
                     }
-                    output.addChar(']');
+                    output.addChar(CLOSE_BRACKET);
 
                     break;
                 case COMMA:
-                    output.addChars(Chr.array(',', '\n')).addChars(getIndent(indentSize, indentCache));
+                    output.addChars(Chr.array(COMMA, NEW_LINE)).addChars(getIndent(indentSize, indentCache));
 
                     break;
                 case COLON:
-                    output.addChars(Chr.array(':', ' '));
+                    output.addChars(Chr.array(COLON, SPACE));
 
                     break;
                 case STRING:
@@ -546,7 +560,7 @@ public class JsonOutput {
         char[] indent = indentCache.get(indentSize);
         if (indent == null) {
             indent = new char[indentSize];
-            Arrays.fill(indent, ' ');
+            Arrays.fill(indent, SPACE);
             indentCache.put(indentSize, indent);
         }
 

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/71dc7622/subprojects/groovy-json/src/main/java/groovy/json/StreamingJsonBuilder.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/groovy/json/StreamingJsonBuilder.java b/subprojects/groovy-json/src/main/java/groovy/json/StreamingJsonBuilder.java
index e3a4c50..ac9cf68 100644
--- a/subprojects/groovy-json/src/main/java/groovy/json/StreamingJsonBuilder.java
+++ b/subprojects/groovy-json/src/main/java/groovy/json/StreamingJsonBuilder.java
@@ -19,6 +19,7 @@
 package groovy.json;
 
 import groovy.lang.Closure;
+import groovy.lang.DelegatesTo;
 import groovy.lang.GroovyObjectSupport;
 
 import java.io.IOException;
@@ -64,10 +65,15 @@ import java.util.*;
  *
  * @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;
 
     /**
@@ -115,6 +121,24 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
     }
 
     /**
+     * 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(JsonOutput.toJson(Collections.singletonMap(name, Collections.emptyMap())));
+    }
+
+    /**
      * A list of elements as arguments to the JSON builder creates a root JSON array
      * <p>
      * Example:
@@ -181,7 +205,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
      * @param coll a collection
      * @param c a closure used to convert the objects of coll
      */
-    public Object call(Collection coll, Closure c) throws IOException {
+    public Object call(Collection coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
         StreamingJsonDelegate.writeCollectionWithClosure(writer, coll, c);
 
         return null;
@@ -205,15 +229,116 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
      *
      * @param c a closure whose method call statements represent key / values of a JSON object
      */
-    public Object call(Closure c) throws IOException {
-        writer.write("{");
+    public Object call(@DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
+        writer.write(JsonOutput.OPEN_BRACE);
         StreamingJsonDelegate.cloneDelegateAndGetContent(writer, c);
-        writer.write("}");
+        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(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
+     * <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, 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 &mdash;
+     * 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(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:
@@ -281,45 +406,25 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
             Object[] arr = (Object[]) args;
             try {
                 if (arr.length == 0) {
-                    writer.write(JsonOutput.toJson(Collections.singletonMap(name, Collections.emptyMap())));
+                    call(name);
                 } 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("}");
+                        final Closure callable = (Closure) arr[0];
+                        call(name, callable);
                     } else if (arr[0] instanceof Map) {
-                        writer.write(JsonOutput.toJson(Collections.singletonMap(name, (Map) arr[0])));
+                        final Map<String, 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) {
-                    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("}}");
+                    final Closure callable = (Closure) arr[1];
+                    call(name, map, callable);
                 } else if (StreamingJsonDelegate.isCollectionWithClosure(arr)) {
-                    writer.write("{");
-                    writer.write(JsonOutput.toJson(name));
-                    writer.write(":");
-                    call((Collection) arr[0], (Closure) arr[1]);
-                    writer.write("}");
+                    final Collection coll = (Collection) arr[0];
+                    final Closure callable = (Closure) arr[1];
+                    call(name, coll, callable);
                 } else {
                     notExpectedArgs = true;
                 }
@@ -336,89 +441,203 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
             throw new JsonException("Expected no arguments, a single map, a single closure, or a map and closure as arguments.");
         }
     }
-}
 
-class StreamingJsonDelegate extends GroovyObjectSupport {
+    /**
+     * The delegate used when invoking closures
+     */
+    public static class StreamingJsonDelegate extends GroovyObjectSupport {
 
-    private Writer writer;
-    private boolean first;
+        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 {
-                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)));
+        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<Object> list = Arrays.asList(arr);
+                        call(name, list);
+                    }
+                } catch (IOException ioe) {
+                    throw new JsonException(ioe);
                 }
-            } catch (IOException ioe) {
-                throw new JsonException(ioe);
             }
+
+            return this;
         }
 
-        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 {
+            writeName(name);
+            writeArray(list);
+        }
 
-    public static boolean isCollectionWithClosure(Object[] args) {
-        return args.length == 2 && args[0] instanceof Collection && args[1] instanceof Closure;
-    }
+        /**
+         * 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 {
+            writeName(name);
+            writeArray(Arrays.asList(array));
+        }
 
-    public static Object writeCollectionWithClosure(Writer writer, Collection coll, Closure closure) throws IOException {
-        writer.write("[");
-        boolean first = true;
-        for (Object it : coll) {
+        /**
+         * 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, Collection coll, Closure c) throws IOException {
+            writeName(name);
+            writeObjects(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 {
+            writeName(name);
+            writeValue(value);
+        }
+
+
+        private void writeObjects(Collection coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
+            verifyValue();
+            writeCollectionWithClosure(writer, coll, c);
+        }
+
+        private 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;
+            }
+        }
+
+
+        private void writeName(String name) throws IOException {
+            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(",");
+                writer.write(JsonOutput.COMMA);
             } else {
                 first = false;
             }
+            writer.write(JsonOutput.toJson(name));
+            writer.write(JsonOutput.COLON);
+        }
 
-            writer.write("{");
-            curryDelegateAndGetContent(writer, closure, it);
-            writer.write("}");
+        private void writeValue(Object value) throws IOException {
+            verifyValue();
+            writer.write(JsonOutput.toJson(value));
         }
-        writer.write("]");
 
-        return writer;
-    }
+        private void writeArray(List<Object> list) throws IOException {
+            verifyValue();
+            writer.write(JsonOutput.toJson(list));
+        }
 
-    public static void cloneDelegateAndGetContent(Writer w, Closure c) {
-        cloneDelegateAndGetContent(w, c, true);
-    }
+        public static boolean isCollectionWithClosure(Object[] args) {
+            return args.length == 2 && args[0] instanceof Collection && args[1] instanceof Closure;
+        }
 
-    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 Object writeCollectionWithClosure(Writer writer, Collection coll, Closure closure) throws IOException {
+            writer.write(JsonOutput.OPEN_BRACKET);
+            boolean first = true;
+            for (Object it : coll) {
+                if (!first) {
+                    writer.write(JsonOutput.COMMA);
+                } else {
+                    first = false;
+                }
 
-    public static void curryDelegateAndGetContent(Writer w, Closure c, Object o) {
-        curryDelegateAndGetContent(w, c, o, true);
-    }
+                writer.write(JsonOutput.OPEN_BRACE);
+                curryDelegateAndGetContent(writer, closure, it);
+                writer.write(JsonOutput.CLOSE_BRACE);
+            }
+            writer.write(JsonOutput.CLOSE_BRACKET);
+
+            return writer;
+        }
+
+        public static void cloneDelegateAndGetContent(Writer w, Closure c) {
+            cloneDelegateAndGetContent(w, c, 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();
+        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();
+        }
+
+        private enum State {
+            NAME, VALUE
+        }
     }
 }
+
+

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/71dc7622/subprojects/groovy-json/src/test/groovy/groovy/json/StreamingJsonBuilderTest.groovy
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/test/groovy/groovy/json/StreamingJsonBuilderTest.groovy b/subprojects/groovy-json/src/test/groovy/groovy/json/StreamingJsonBuilderTest.groovy
index 2aa2dc7..10afbbf 100644
--- a/subprojects/groovy-json/src/test/groovy/groovy/json/StreamingJsonBuilderTest.groovy
+++ b/subprojects/groovy-json/src/test/groovy/groovy/json/StreamingJsonBuilderTest.groovy
@@ -18,12 +18,53 @@
  */
 package groovy.json
 
+import groovy.transform.CompileStatic
+
 /**
  * @author Tim Yates
  * @author Guillaume Laforge
  */
 class StreamingJsonBuilderTest extends GroovyTestCase {
 
+    @CompileStatic
+    void testJsonBuilderUsageWithCompileStatic() {
+        new StringWriter().with { w ->
+            def json = new StreamingJsonBuilder(w)
+            json.call('response') {
+                call "status", "ok"
+                call "userTier", "free"
+                call "total", 2413
+                call "startIndex", 1
+                call "pageSize", 10
+                call "currentPage", 1
+                call "pages", 242
+                call "orderBy", "newest"
+                call("results",
+                [
+                        id: "world/video/2011/jan/19/tunisia-demonstrators-democracy-video",
+                        sectionId: "world",
+                        sectionName: "World news",
+                        webPublicationDate: "2011-01-19T15:12:46Z",
+                        webTitle: "Tunisian demonstrators demand new democracy - video",
+                        webUrl: "http://www.guardian.co.uk/world/video/2011/jan/19/tunisia-demonstrators-democracy-video",
+                        apiUrl: "http://content.guardianapis.com/world/video/2011/jan/19/tunisia-demonstrators-democracy-video"
+                ],
+                        [
+                                id: "world/gallery/2011/jan/19/tunisia-protests-pictures",
+                                sectionId: "world",
+                                sectionName: "World news",
+                                webPublicationDate: "2011-01-19T15:01:09Z",
+                                webTitle: "Tunisia protests continue in pictures ",
+                                webUrl: "http://www.guardian.co.uk/world/gallery/2011/jan/19/tunisia-protests-pictures",
+                                apiUrl: "http://content.guardianapis.com/world/gallery/2011/jan/19/tunisia-protests-pictures"
+                        ])
+            }
+
+            assert w.toString() ==
+                    '''{"response":{"status":"ok","userTier":"free","total":2413,"startIndex":1,"pageSize":10,"currentPage":1,"pages":242,"orderBy":"newest","results":[{"id":"world/video/2011/jan/19/tunisia-demonstrators-democracy-video","sectionId":"world","sectionName":"World news","webPublicationDate":"2011-01-19T15:12:46Z","webTitle":"Tunisian demonstrators demand new democracy - video","webUrl":"http://www.guardian.co.uk/world/video/2011/jan/19/tunisia-demonstrators-democracy-video","apiUrl":"http://content.guardianapis.com/world/video/2011/jan/19/tunisia-demonstrators-democracy-video"},{"id":"world/gallery/2011/jan/19/tunisia-protests-pictures","sectionId":"world","sectionName":"World news","webPublicationDate":"2011-01-19T15:01:09Z","webTitle":"Tunisia protests continue in pictures ","webUrl":"http://www.guardian.co.uk/world/gallery/2011/jan/19/tunisia-protests-pictures","apiUrl":"http://content.guardianapis.com/world/gallery/2011/jan/19/tunisia-protests-pictures"}]}}'''
+        }
+    }
+
     void testJsonBuilderConstructor() {
         new StringWriter().with { w ->
             new StreamingJsonBuilder(w, [a: 1, b: true])