You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by jw...@apache.org on 2016/10/23 23:48:08 UTC
[2/2] groovy git commit: JsonGenerator - JSON serialization options
(closes #371, closes #433)
JsonGenerator - JSON serialization options (closes #371, closes #433)
Fixes or partially addresses the following:
GROOVY-6699: ignore properties/fields during serialization
GROOVY-6854: serialize ISO-8601 dates
GROOVY-6975: deactivate unicode escaping
GROOVY-7682: JodaTime/JSR310 (using custom converter)
GROOVY-7780: exclude null values
Project: http://git-wip-us.apache.org/repos/asf/groovy/repo
Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/13202599
Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/13202599
Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/13202599
Branch: refs/heads/master
Commit: 132025997e110e284fe1ce5e4d19a75ae5418ae6
Parents: 8213bd0
Author: John Wagenleitner <jw...@apache.org>
Authored: Sun Oct 23 16:40:11 2016 -0700
Committer: John Wagenleitner <jw...@apache.org>
Committed: Sun Oct 23 16:40:11 2016 -0700
----------------------------------------------------------------------
.../java/groovy/json/DefaultJsonGenerator.java | 549 +++++++++++++++++++
.../src/main/java/groovy/json/JsonBuilder.java | 28 +-
.../main/java/groovy/json/JsonGenerator.java | 326 +++++++++++
.../src/main/java/groovy/json/JsonOutput.java | 372 +------------
.../java/groovy/json/StreamingJsonBuilder.java | 134 ++++-
.../main/java/groovy/json/internal/CharBuf.java | 57 +-
.../groovy-json/src/spec/doc/json-builder.adoc | 9 +-
.../src/spec/doc/json-userguide.adoc | 28 +-
.../src/spec/doc/streaming-jason-builder.adoc | 9 +-
.../src/spec/test/json/JsonBuilderTest.groovy | 32 ++
.../src/spec/test/json/JsonTest.groovy | 68 +++
.../test/json/StreamingJsonBuilderTest.groovy | 35 +-
.../test/groovy/groovy/json/CharBufTest.groovy | 35 ++
.../groovy/json/CustomJsonGeneratorTest.groovy | 89 +++
.../groovy/json/DefaultJsonGeneratorTest.groovy | 283 ++++++++++
.../groovy/groovy/json/JsonBuilderTest.groovy | 27 +
.../groovy/json/StreamingJsonBuilderTest.groovy | 50 ++
17 files changed, 1729 insertions(+), 402 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/main/java/groovy/json/DefaultJsonGenerator.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/groovy/json/DefaultJsonGenerator.java b/subprojects/groovy-json/src/main/java/groovy/json/DefaultJsonGenerator.java
new file mode 100644
index 0000000..1dbd0ad
--- /dev/null
+++ b/subprojects/groovy-json/src/main/java/groovy/json/DefaultJsonGenerator.java
@@ -0,0 +1,549 @@
+/*
+ * 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.json.internal.CharBuf;
+import groovy.json.internal.Chr;
+import groovy.lang.Closure;
+import groovy.util.Expando;
+import org.codehaus.groovy.runtime.DefaultGroovyMethods;
+
+import java.io.File;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.URL;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.UUID;
+
+import static groovy.json.JsonOutput.CLOSE_BRACE;
+import static groovy.json.JsonOutput.CLOSE_BRACKET;
+import static groovy.json.JsonOutput.COMMA;
+import static groovy.json.JsonOutput.EMPTY_LIST_CHARS;
+import static groovy.json.JsonOutput.EMPTY_MAP_CHARS;
+import static groovy.json.JsonOutput.EMPTY_STRING_CHARS;
+import static groovy.json.JsonOutput.OPEN_BRACE;
+import static groovy.json.JsonOutput.OPEN_BRACKET;
+
+/**
+ * A JsonGenerator that can be configured with various {@link JsonGenerator.Options}.
+ * If the default options are sufficient consider using the static {@code JsonOutput.toJson}
+ * methods.
+ *
+ * @see JsonGenerator.Options#build()
+ * @since 2.5
+ */
+public class DefaultJsonGenerator implements JsonGenerator {
+
+ protected final boolean excludeNulls;
+ protected final boolean disableUnicodeEscaping;
+ protected final String dateFormat;
+ protected final Locale dateLocale;
+ protected final TimeZone timezone;
+ protected final Set<Converter> converters = new LinkedHashSet<Converter>();
+ protected final Set<String> excludedFieldNames = new HashSet<String>();
+ protected final Set<Class<?>> excludedFieldTypes = new HashSet<Class<?>>();
+
+ protected DefaultJsonGenerator(Options options) {
+ excludeNulls = options.excludeNulls;
+ disableUnicodeEscaping = options.disableUnicodeEscaping;
+ dateFormat = options.dateFormat;
+ dateLocale = options.dateLocale;
+ timezone = options.timezone;
+ if (!options.converters.isEmpty()) {
+ converters.addAll(options.converters);
+ }
+ if (!options.excludedFieldNames.isEmpty()) {
+ excludedFieldNames.addAll(options.excludedFieldNames);
+ }
+ if (!options.excludedFieldTypes.isEmpty()) {
+ excludedFieldTypes.addAll(options.excludedFieldTypes);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toJson(Object object) {
+ CharBuf buffer = CharBuf.create(255);
+ writeObject(object, buffer);
+ return buffer.toString();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isExcludingFieldsNamed(String name) {
+ return excludedFieldNames.contains(name);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isExcludingValues(Object value) {
+ if (value == null) {
+ return excludeNulls;
+ } else {
+ return shouldExcludeType(value.getClass());
+ }
+ }
+
+ /**
+ * Serializes Number value and writes it into specified buffer.
+ */
+ protected void writeNumber(Class<?> numberClass, Number value, CharBuf buffer) {
+ if (numberClass == Integer.class) {
+ buffer.addInt((Integer) value);
+ } else if (numberClass == Long.class) {
+ buffer.addLong((Long) value);
+ } else if (numberClass == BigInteger.class) {
+ buffer.addBigInteger((BigInteger) value);
+ } else if (numberClass == BigDecimal.class) {
+ buffer.addBigDecimal((BigDecimal) value);
+ } else if (numberClass == Double.class) {
+ Double doubleValue = (Double) value;
+ if (doubleValue.isInfinite()) {
+ throw new JsonException("Number " + value + " can't be serialized as JSON: infinite are not allowed in JSON.");
+ }
+ if (doubleValue.isNaN()) {
+ throw new JsonException("Number " + value + " can't be serialized as JSON: NaN are not allowed in JSON.");
+ }
+
+ buffer.addDouble(doubleValue);
+ } else if (numberClass == Float.class) {
+ Float floatValue = (Float) value;
+ if (floatValue.isInfinite()) {
+ throw new JsonException("Number " + value + " can't be serialized as JSON: infinite are not allowed in JSON.");
+ }
+ if (floatValue.isNaN()) {
+ throw new JsonException("Number " + value + " can't be serialized as JSON: NaN are not allowed in JSON.");
+ }
+
+ buffer.addFloat(floatValue);
+ } else if (numberClass == Byte.class) {
+ buffer.addByte((Byte) value);
+ } else if (numberClass == Short.class) {
+ buffer.addShort((Short) value);
+ } else { // Handle other Number implementations
+ buffer.addString(value.toString());
+ }
+ }
+
+ protected void writeObject(Object object, CharBuf buffer) {
+ writeObject(null, object, buffer);
+ }
+
+ /**
+ * Serializes object and writes it into specified buffer.
+ */
+ protected void writeObject(String key, Object object, CharBuf buffer) {
+
+ if (isExcludingValues(object)) {
+ return;
+ }
+
+ if (object == null) {
+ buffer.addNull();
+ return;
+ }
+
+ Class<?> objectClass = object.getClass();
+
+ Converter converter = findConverter(objectClass);
+ if (converter != null) {
+ writeRaw(converter.convert(object, key), buffer);
+ return;
+ }
+
+ if (CharSequence.class.isAssignableFrom(objectClass)) { // Handle String, StringBuilder, GString and other CharSequence implementations
+ writeCharSequence((CharSequence) object, buffer);
+ } else if (objectClass == Boolean.class) {
+ buffer.addBoolean((Boolean) object);
+ } else if (Number.class.isAssignableFrom(objectClass)) {
+ writeNumber(objectClass, (Number) object, buffer);
+ } else if (Date.class.isAssignableFrom(objectClass)) {
+ writeDate((Date) object, buffer);
+ } else if (Calendar.class.isAssignableFrom(objectClass)) {
+ writeDate(((Calendar) object).getTime(), buffer);
+ } else if (Map.class.isAssignableFrom(objectClass)) {
+ writeMap((Map) object, buffer);
+ } else if (Iterable.class.isAssignableFrom(objectClass)) {
+ writeIterator(((Iterable<?>) object).iterator(), buffer);
+ } else if (Iterator.class.isAssignableFrom(objectClass)) {
+ writeIterator((Iterator) object, buffer);
+ } else if (objectClass == Character.class) {
+ buffer.addJsonEscapedString(Chr.array((Character) object), disableUnicodeEscaping);
+ } else if (objectClass == URL.class) {
+ buffer.addJsonEscapedString(object.toString(), disableUnicodeEscaping);
+ } else if (objectClass == UUID.class) {
+ buffer.addQuoted(object.toString());
+ } else if (objectClass == JsonOutput.JsonUnescaped.class) {
+ buffer.add(object.toString());
+ } else if (Closure.class.isAssignableFrom(objectClass)) {
+ writeMap(JsonDelegate.cloneDelegateAndGetContent((Closure<?>) object), buffer);
+ } else if (Expando.class.isAssignableFrom(objectClass)) {
+ writeMap(((Expando) object).getProperties(), buffer);
+ } else if (Enumeration.class.isAssignableFrom(objectClass)) {
+ List<?> list = Collections.list((Enumeration<?>) object);
+ writeIterator(list.iterator(), buffer);
+ } else if (objectClass.isArray()) {
+ writeArray(objectClass, object, buffer);
+ } else if (Enum.class.isAssignableFrom(objectClass)) {
+ buffer.addQuoted(((Enum<?>) object).name());
+ } else if (File.class.isAssignableFrom(objectClass)) {
+ Map<?, ?> properties = getObjectProperties(object);
+ //Clean up all recursive references to File objects
+ Iterator<? extends Map.Entry<?, ?>> iterator = properties.entrySet().iterator();
+ while(iterator.hasNext()) {
+ Map.Entry<?,?> entry = iterator.next();
+ if(entry.getValue() instanceof File) {
+ iterator.remove();
+ }
+ }
+ writeMap(properties, buffer);
+ } else {
+ Map<?, ?> properties = getObjectProperties(object);
+ writeMap(properties, buffer);
+ }
+ }
+
+ protected Map<?, ?> getObjectProperties(Object object) {
+ Map<?, ?> properties = DefaultGroovyMethods.getProperties(object);
+ properties.remove("class");
+ properties.remove("declaringClass");
+ properties.remove("metaClass");
+ return properties;
+ }
+
+ /**
+ * Serializes any char sequence and writes it into specified buffer.
+ */
+ protected void writeCharSequence(CharSequence seq, CharBuf buffer) {
+ if (seq.length() > 0) {
+ buffer.addJsonEscapedString(seq.toString(), disableUnicodeEscaping);
+ } else {
+ buffer.addChars(EMPTY_STRING_CHARS);
+ }
+ }
+
+ /**
+ * Serializes any char sequence and writes it into specified buffer
+ * without performing any manipulation of the given text.
+ */
+ protected void writeRaw(CharSequence seq, CharBuf buffer) {
+ if (seq != null) {
+ buffer.add(seq.toString());
+ }
+ }
+
+ /**
+ * Serializes date and writes it into specified buffer.
+ */
+ protected void writeDate(Date date, CharBuf buffer) {
+ SimpleDateFormat formatter = new SimpleDateFormat(dateFormat, dateLocale);
+ formatter.setTimeZone(timezone);
+ buffer.addQuoted(formatter.format(date));
+ }
+
+ /**
+ * Serializes array and writes it into specified buffer.
+ */
+ protected void writeArray(Class<?> arrayClass, Object array, CharBuf buffer) {
+ if (Object[].class.isAssignableFrom(arrayClass)) {
+ Object[] objArray = (Object[]) array;
+ writeIterator(Arrays.asList(objArray).iterator(), buffer);
+ return;
+ }
+ buffer.addChar(OPEN_BRACKET);
+ if (int[].class.isAssignableFrom(arrayClass)) {
+ int[] intArray = (int[]) array;
+ if (intArray.length > 0) {
+ buffer.addInt(intArray[0]);
+ for (int i = 1; i < intArray.length; i++) {
+ buffer.addChar(COMMA).addInt(intArray[i]);
+ }
+ }
+ } else if (long[].class.isAssignableFrom(arrayClass)) {
+ long[] longArray = (long[]) array;
+ if (longArray.length > 0) {
+ buffer.addLong(longArray[0]);
+ for (int i = 1; i < longArray.length; i++) {
+ buffer.addChar(COMMA).addLong(longArray[i]);
+ }
+ }
+ } else if (boolean[].class.isAssignableFrom(arrayClass)) {
+ boolean[] booleanArray = (boolean[]) array;
+ if (booleanArray.length > 0) {
+ buffer.addBoolean(booleanArray[0]);
+ for (int i = 1; i < booleanArray.length; i++) {
+ buffer.addChar(COMMA).addBoolean(booleanArray[i]);
+ }
+ }
+ } else if (char[].class.isAssignableFrom(arrayClass)) {
+ char[] charArray = (char[]) array;
+ if (charArray.length > 0) {
+ buffer.addJsonEscapedString(Chr.array(charArray[0]), disableUnicodeEscaping);
+ for (int i = 1; i < charArray.length; i++) {
+ buffer.addChar(COMMA).addJsonEscapedString(Chr.array(charArray[i]), disableUnicodeEscaping);
+ }
+ }
+ } else if (double[].class.isAssignableFrom(arrayClass)) {
+ double[] doubleArray = (double[]) array;
+ if (doubleArray.length > 0) {
+ buffer.addDouble(doubleArray[0]);
+ for (int i = 1; i < doubleArray.length; i++) {
+ buffer.addChar(COMMA).addDouble(doubleArray[i]);
+ }
+ }
+ } else if (float[].class.isAssignableFrom(arrayClass)) {
+ float[] floatArray = (float[]) array;
+ if (floatArray.length > 0) {
+ buffer.addFloat(floatArray[0]);
+ for (int i = 1; i < floatArray.length; i++) {
+ buffer.addChar(COMMA).addFloat(floatArray[i]);
+ }
+ }
+ } else if (byte[].class.isAssignableFrom(arrayClass)) {
+ byte[] byteArray = (byte[]) array;
+ if (byteArray.length > 0) {
+ buffer.addByte(byteArray[0]);
+ for (int i = 1; i < byteArray.length; i++) {
+ buffer.addChar(COMMA).addByte(byteArray[i]);
+ }
+ }
+ } else if (short[].class.isAssignableFrom(arrayClass)) {
+ short[] shortArray = (short[]) array;
+ if (shortArray.length > 0) {
+ buffer.addShort(shortArray[0]);
+ for (int i = 1; i < shortArray.length; i++) {
+ buffer.addChar(COMMA).addShort(shortArray[i]);
+ }
+ }
+ }
+ buffer.addChar(CLOSE_BRACKET);
+ }
+
+ /**
+ * Serializes map and writes it into specified buffer.
+ */
+ protected void writeMap(Map<?, ?> map, CharBuf buffer) {
+ if (map.isEmpty()) {
+ buffer.addChars(EMPTY_MAP_CHARS);
+ return;
+ }
+ buffer.addChar(OPEN_BRACE);
+ for (Map.Entry<?, ?> entry : map.entrySet()) {
+ if (entry.getKey() == null) {
+ throw new IllegalArgumentException("Maps with null keys can\'t be converted to JSON");
+ }
+ String key = entry.getKey().toString();
+ Object value = entry.getValue();
+ if (isExcludingValues(value) || isExcludingFieldsNamed(key)) {
+ continue;
+ }
+ writeMapEntry(key, value, buffer);
+ buffer.addChar(COMMA);
+ }
+ buffer.removeLastChar(COMMA); // dangling comma
+ buffer.addChar(CLOSE_BRACE);
+ }
+
+ /**
+ * Serializes a map entry and writes it into specified buffer.
+ */
+ protected void writeMapEntry(String key, Object value, CharBuf buffer) {
+ buffer.addJsonFieldName(key, disableUnicodeEscaping);
+ writeObject(key, value, buffer);
+ }
+
+ /**
+ * Serializes iterator and writes it into specified buffer.
+ */
+ protected void writeIterator(Iterator<?> iterator, CharBuf buffer) {
+ if (!iterator.hasNext()) {
+ buffer.addChars(EMPTY_LIST_CHARS);
+ return;
+ }
+ buffer.addChar(OPEN_BRACKET);
+ while (iterator.hasNext()) {
+ Object it = iterator.next();
+ if (!isExcludingValues(it)) {
+ writeObject(it, buffer);
+ buffer.addChar(COMMA);
+ }
+ }
+ buffer.removeLastChar(COMMA); // dangling comma
+ buffer.addChar(CLOSE_BRACKET);
+ }
+
+ /**
+ * Finds a converter that can handle the given type. The first converter
+ * that reports it can handle the type is returned, based on the order in
+ * which the converters were specified. A {@code null} value will be returned
+ * if no suitable converter can be found for the given type.
+ *
+ * @param type that this converter can handle
+ * @return first converter that can handle the given type; else {@code null}
+ * if no compatible converters are found for the given type.
+ */
+ protected Converter findConverter(Class<?> type) {
+ for (Converter c : converters) {
+ if (c.handles(type)) {
+ return c;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Indicates whether the given type should be excluded from the generated output.
+ *
+ * @param type the type to check
+ * @return {@code true} if the given type should not be output, else {@code false}
+ */
+ protected boolean shouldExcludeType(Class<?> type) {
+ for (Class<?> t : excludedFieldTypes) {
+ if (t.isAssignableFrom(type)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * A converter that handles converting a given type to a JSON value
+ * using a closure.
+ *
+ * @since 2.5
+ */
+ protected static class ClosureConverter implements Converter {
+
+ protected final Class<?> type;
+ protected final Closure<? extends CharSequence> closure;
+ protected final int paramCount;
+
+ protected ClosureConverter(Class<?> type, Closure<? extends CharSequence> closure) {
+ if (type == null) {
+ throw new NullPointerException("Type parameter must not be null");
+ }
+ if (closure == null) {
+ throw new NullPointerException("Closure parameter must not be null");
+ }
+
+ int paramCount = closure.getMaximumNumberOfParameters();
+ if (paramCount < 1) {
+ throw new IllegalArgumentException("Closure must accept at least one parameter");
+ }
+ Class<?> param1 = closure.getParameterTypes()[0];
+ if (!param1.isAssignableFrom(type)) {
+ throw new IllegalArgumentException("Expected first parameter to be of type: " + type.toString());
+ }
+ if (paramCount > 1) {
+ Class<?> param2 = closure.getParameterTypes()[1];
+ if (!param2.isAssignableFrom(String.class)) {
+ throw new IllegalArgumentException("Expected second parameter to be of type: " + String.class.toString());
+ }
+ }
+ this.type = type;
+ this.closure = closure;
+ this.paramCount = paramCount;
+ }
+
+ /**
+ * Returns {@code true} if this converter can handle conversions
+ * of the given type.
+ *
+ * @param type the type of the object to convert
+ * @return true if this converter can successfully convert values of
+ * the given type to a JSON value
+ */
+ public boolean handles(Class<?> type) {
+ return this.type.isAssignableFrom(type);
+ }
+
+ /**
+ * Converts a given value to a JSON value.
+ *
+ * @param value the object to convert
+ * @return a JSON value representing the value
+ */
+ public CharSequence convert(Object value) {
+ return convert(value, null);
+ }
+
+ /**
+ * Converts a given value to a JSON value.
+ *
+ * @param value the object to convert
+ * @param key the key name for the value, may be {@code null}
+ * @return a JSON value representing the value
+ */
+ public CharSequence convert(Object value, String key) {
+ return (paramCount == 1) ?
+ closure.call(value) :
+ closure.call(value, key);
+ }
+
+ /**
+ * Any two Converter instances registered for the same type are considered
+ * to be equal. This comparison makes managing instances in a Set easier;
+ * since there is no chaining of Converters it makes sense to only allow
+ * one per type.
+ *
+ * @param o the object with which to compare.
+ * @return {@code true} if this object contains the same class; {@code false} otherwise.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof ClosureConverter)) {
+ return false;
+ }
+ return this.type == ((ClosureConverter)o).type;
+ }
+
+ @Override
+ public int hashCode() {
+ return this.type.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + "<" + this.type.toString() + ">";
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/main/java/groovy/json/JsonBuilder.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/groovy/json/JsonBuilder.java b/subprojects/groovy-json/src/main/java/groovy/json/JsonBuilder.java
index 0a30b7d..abaac0b 100644
--- a/subprojects/groovy-json/src/main/java/groovy/json/JsonBuilder.java
+++ b/subprojects/groovy-json/src/main/java/groovy/json/JsonBuilder.java
@@ -65,12 +65,24 @@ import java.util.*;
*/
public class JsonBuilder extends GroovyObjectSupport implements Writable {
+ private final JsonGenerator generator;
private Object content;
/**
* Instantiates a JSON builder.
*/
public JsonBuilder() {
+ this.generator = JsonOutput.DEFAULT_GENERATOR;
+ }
+
+ /**
+ * Instantiates a JSON builder with a configured generator.
+ *
+ * @param generator used to generate the output
+ * @since 2.5
+ */
+ public JsonBuilder(JsonGenerator generator) {
+ this.generator = generator;
}
/**
@@ -80,6 +92,20 @@ public class JsonBuilder extends GroovyObjectSupport implements Writable {
*/
public JsonBuilder(Object content) {
this.content = content;
+ this.generator = JsonOutput.DEFAULT_GENERATOR;
+ }
+
+ /**
+ * Instantiates a JSON builder with some existing data structure
+ * and a configured generator.
+ *
+ * @param content a pre-existing data structure
+ * @param generator used to generate the output
+ * @since 2.5
+ */
+ public JsonBuilder(Object content, JsonGenerator generator) {
+ this.content = content;
+ this.generator = generator;
}
public Object getContent() {
@@ -344,7 +370,7 @@ public class JsonBuilder extends GroovyObjectSupport implements Writable {
* @return a JSON output
*/
public String toString() {
- return JsonOutput.toJson(content);
+ return generator.toJson(content);
}
/**
http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/main/java/groovy/json/JsonGenerator.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/groovy/json/JsonGenerator.java b/subprojects/groovy-json/src/main/java/groovy/json/JsonGenerator.java
new file mode 100644
index 0000000..91b0e06
--- /dev/null
+++ b/subprojects/groovy-json/src/main/java/groovy/json/JsonGenerator.java
@@ -0,0 +1,326 @@
+/*
+ * 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.transform.stc.ClosureParams;
+import groovy.transform.stc.FromString;
+
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.TimeZone;
+
+/**
+ * Generates JSON from objects.
+ *
+ * The {@link Options} builder can be used to configure an instance of a JsonGenerator.
+ *
+ * @see Options#build()
+ * @since 2.5
+ */
+public interface JsonGenerator {
+
+ /**
+ * Converts an object to its JSON representation.
+ *
+ * @param object to convert to JSON
+ * @return JSON
+ */
+ String toJson(Object object);
+
+ /**
+ * Indicates whether this JsonGenerator is configured to exclude fields by
+ * the given name.
+ *
+ * @param name of the field
+ * @return true if that field is being excluded, else false
+ */
+ boolean isExcludingFieldsNamed(String name);
+
+ /**
+ * Indicates whether this JsonGenerator is configured to exclude values
+ * of the given object (may be {@code null}).
+ *
+ * @param value an instance of an object
+ * @return true if values like this are being excluded, else false
+ */
+ boolean isExcludingValues(Object value);
+
+ /**
+ * Handles converting a given type to a JSON value.
+ *
+ * @since 2.5
+ */
+ interface Converter {
+
+ /**
+ * Returns {@code true} if this converter can handle conversions
+ * of the given type.
+ *
+ * @param type the type of the object to convert
+ * @return {@code true} if this converter can successfully convert values of
+ * the given type to a JSON value, else {@code false}
+ */
+ boolean handles(Class<?> type);
+
+ /**
+ * Converts a given object to a JSON value.
+ *
+ * @param value the object to convert
+ * @return a JSON value representing the object
+ */
+ CharSequence convert(Object value);
+
+ /**
+ * Converts a given object to a JSON value.
+ *
+ * @param value the object to convert
+ * @param key the key name for the value, may be {@code null}
+ * @return a JSON value representing the object
+ */
+ CharSequence convert(Object value, String key);
+
+ }
+
+ /**
+ * A builder used to construct a {@link JsonGenerator} instance that allows
+ * control over the serialized JSON output. If you do not need to customize the
+ * output it is recommended to use the static {@code JsonOutput.toJson} methods.
+ *
+ * <p>
+ * Example:
+ * <pre><code class="groovyTestCase">
+ * def generator = new groovy.json.JsonGenerator.Options()
+ * .excludeNulls()
+ * .dateFormat('yyyy')
+ * .excludeFieldsByName('bar', 'baz')
+ * .excludeFieldsByType(java.sql.Date)
+ * .build()
+ *
+ * def input = [foo: null, lastUpdated: Date.parse('yyyy-MM-dd', '2014-10-24'),
+ * bar: 'foo', baz: 'foo', systemDate: new java.sql.Date(new Date().getTime())]
+ *
+ * assert generator.toJson(input) == '{"lastUpdated":"2014"}'
+ * </code></pre>
+ *
+ * @since 2.5
+ */
+ class Options {
+
+ protected static final String JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
+ protected static final Locale JSON_DATE_FORMAT_LOCALE = Locale.US;
+ protected static final String DEFAULT_TIMEZONE = "GMT";
+
+ protected boolean excludeNulls;
+ protected boolean disableUnicodeEscaping;
+ protected String dateFormat = JSON_DATE_FORMAT;
+ protected Locale dateLocale = JSON_DATE_FORMAT_LOCALE;
+ protected TimeZone timezone = TimeZone.getTimeZone(DEFAULT_TIMEZONE);
+ protected final Set<Converter> converters = new LinkedHashSet<Converter>();
+ protected final Set<String> excludedFieldNames = new HashSet<String>();
+ protected final Set<Class<?>> excludedFieldTypes = new HashSet<Class<?>>();
+
+ public Options() {}
+
+ /**
+ * Do not serialize {@code null} values.
+ *
+ * @return a reference to this {@code Options} instance
+ */
+ public Options excludeNulls() {
+ excludeNulls = true;
+ return this;
+ }
+
+ /**
+ * Disables the escaping of Unicode characters in JSON String values.
+ *
+ * @return a reference to this {@code Options} instance
+ */
+ public Options disableUnicodeEscaping() {
+ disableUnicodeEscaping = true;
+ return this;
+ }
+
+ /**
+ * Sets the date format that will be used to serialize {@code Date} objects.
+ * This must be a valid pattern for {@link java.text.SimpleDateFormat} and the
+ * date formatter will be constructed with the default locale of {@link Locale#US}.
+ *
+ * @param format date format pattern used to serialize dates
+ * @return a reference to this {@code Options} instance
+ * @exception NullPointerException if the given pattern is null
+ * @exception IllegalArgumentException if the given pattern is invalid
+ */
+ public Options dateFormat(String format) {
+ return dateFormat(format, JSON_DATE_FORMAT_LOCALE);
+ }
+
+ /**
+ * Sets the date format that will be used to serialize {@code Date} objects.
+ * This must be a valid pattern for {@link java.text.SimpleDateFormat}.
+ *
+ * @param format date format pattern used to serialize dates
+ * @param locale the locale whose date format symbols will be used
+ * @return a reference to this {@code Options} instance
+ * @exception IllegalArgumentException if the given pattern is invalid
+ */
+ public Options dateFormat(String format, Locale locale) {
+ // validate date format pattern
+ new SimpleDateFormat(format, locale);
+ dateFormat = format;
+ dateLocale = locale;
+ return this;
+ }
+
+ /**
+ * Sets the time zone that will be used to serialize dates.
+ *
+ * @param timezone used to serialize dates
+ * @return a reference to this {@code Options} instance
+ * @exception NullPointerException if the given timezone is null
+ */
+ public Options timezone(String timezone) {
+ this.timezone = TimeZone.getTimeZone(timezone);
+ return this;
+ }
+
+ /**
+ * Registers a closure that will be called when the specified type or subtype
+ * is serialized.
+ *
+ * <p>The closure must accept either 1 or 2 parameters. The first parameter
+ * is required and will be instance of the {@code type} for which the closure
+ * is registered. The second optional parameter should be of type {@code String}
+ * and, if available, will be passed the name of the key associated with this
+ * value if serializing a JSON Object. This parameter will be {@code null} when
+ * serializing a JSON Array or when there is no way to determine the name of the key.
+ *
+ * <p>The return value from the closure must be a valid JSON value. The result
+ * of the closure will be written to the internal buffer directly and no quoting,
+ * escaping or other manipulation will be done to the resulting output.
+ *
+ * <p>
+ * Example:
+ * <pre><code class="groovyTestCase">
+ * def generator = new groovy.json.JsonGenerator.Options()
+ * .addConverter(URL) { URL u ->
+ * "\"${u.getHost()}\""
+ * }
+ * .build()
+ *
+ * def input = [domain: new URL('http://groovy-lang.org/json.html#_parser_variants')]
+ *
+ * assert generator.toJson(input) == '{"domain":"groovy-lang.org"}'
+ * </code></pre>
+ *
+ * <p>If two or more closures are registered for the exact same type the last
+ * closure based on the order they were specified will be used. When serializing an
+ * object its type is compared to the list of registered types in the order the were
+ * given and the closure for the first suitable type will be called. Therefore, it is
+ * important to register more specific types first.
+ *
+ * @param type the type to convert
+ * @param closure called when the registered type or any type assignable to the given
+ * type is encountered
+ * @param <T> the type this converter is registered to handle
+ * @return a reference to this {@code Options} instance
+ * @exception NullPointerException if the given type or closure is null
+ * @exception IllegalArgumentException if the given closure does not accept
+ * a parameter of the given type
+ */
+ public <T> Options addConverter(Class<T> type,
+ @ClosureParams(value=FromString.class, options={"T","T,String"})
+ Closure<? extends CharSequence> closure)
+ {
+ Converter converter = new DefaultJsonGenerator.ClosureConverter(type, closure);
+ if (converters.contains(converter)) {
+ converters.remove(converter);
+ }
+ converters.add(converter);
+ return this;
+ }
+
+ /**
+ * Excludes from the output any fields that match the specified names.
+ *
+ * @param fieldNames name of the field to exclude from the output
+ * @return a reference to this {@code Options} instance
+ */
+ public Options excludeFieldsByName(CharSequence... fieldNames) {
+ return excludeFieldsByName(Arrays.asList(fieldNames));
+ }
+
+ /**
+ * Excludes from the output any fields that match the specified names.
+ *
+ * @param fieldNames collection of names to exclude from the output
+ * @return a reference to this {@code Options} instance
+ */
+ public Options excludeFieldsByName(Iterable<? extends CharSequence> fieldNames) {
+ for (CharSequence cs : fieldNames) {
+ if (cs != null) {
+ excludedFieldNames.add(cs.toString());
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Excludes from the output any fields whose type is the same or is
+ * assignable to any of the given types.
+ *
+ * @param types excluded from the output
+ * @return a reference to this {@code Options} instance
+ */
+ public Options excludeFieldsByType(Class<?>... types) {
+ return excludeFieldsByType(Arrays.asList(types));
+ }
+
+ /**
+ * Excludes from the output any fields whose type is the same or is
+ * assignable to any of the given types.
+ *
+ * @param types collection of types to exclude from the output
+ * @return a reference to this {@code Options} instance
+ */
+ public Options excludeFieldsByType(Iterable<Class<?>> types) {
+ for (Class<?> c : types) {
+ if (c != null) {
+ excludedFieldTypes.add(c);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Creates a {@link JsonGenerator} that is based on the current options.
+ *
+ * @return a fully configured {@link JsonGenerator}
+ */
+ public JsonGenerator build() {
+ return new DefaultJsonGenerator(this);
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/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 322e9f1..aa95b07 100644
--- a/subprojects/groovy-json/src/main/java/groovy/json/JsonOutput.java
+++ b/subprojects/groovy-json/src/main/java/groovy/json/JsonOutput.java
@@ -22,19 +22,17 @@ import groovy.json.internal.CharBuf;
import groovy.json.internal.Chr;
import groovy.lang.Closure;
import groovy.util.Expando;
-import org.codehaus.groovy.runtime.DefaultGroovyMethods;
-import java.io.File;
import java.io.StringReader;
-import java.math.BigDecimal;
-import java.math.BigInteger;
import java.net.URL;
-import java.text.SimpleDateFormat;
import java.util.*;
/**
* Class responsible for the actual String serialization of the possible values of a JSON structure.
* This class can also be used as a category, so as to add <code>toJson()</code> methods to various types.
+ * <p>
+ * This class does not provide the ability to customize the resulting output. A {@link JsonGenerator}
+ * can be used if the ability to alter the resulting output is required.
*
* @author Guillaume Laforge
* @author Roshan Dawrani
@@ -42,6 +40,7 @@ import java.util.*;
* @author Rick Hightower
* @author Graeme Rocher
*
+ * @see JsonGenerator
* @since 1.8.0
*/
public class JsonOutput {
@@ -56,20 +55,18 @@ public class JsonOutput {
static final char NEW_LINE = '\n';
static final char QUOTE = '"';
- private static final char[] EMPTY_STRING_CHARS = Chr.array(QUOTE, QUOTE);
+ static final char[] EMPTY_STRING_CHARS = Chr.array(QUOTE, QUOTE);
+ static final char[] EMPTY_MAP_CHARS = {OPEN_BRACE, CLOSE_BRACE};
+ static final char[] EMPTY_LIST_CHARS = {OPEN_BRACKET, CLOSE_BRACKET};
- 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";
+ /* package-private for use in builders */
+ static final JsonGenerator DEFAULT_GENERATOR = new DefaultJsonGenerator(new JsonGenerator.Options());
/**
* @return "true" or "false" for a boolean value
*/
public static String toJson(Boolean bool) {
- CharBuf buffer = CharBuf.create(4);
- writeObject(bool, buffer); // checking null inside
-
- return buffer.toString();
+ return DEFAULT_GENERATOR.toJson(bool);
}
/**
@@ -77,39 +74,21 @@ public class JsonOutput {
* @throws JsonException if the number is infinite or not a number.
*/
public static String toJson(Number n) {
- if (n == null) {
- return NULL_VALUE;
- }
-
- CharBuf buffer = CharBuf.create(3);
- Class<?> numberClass = n.getClass();
- writeNumber(numberClass, n, buffer);
-
- return buffer.toString();
+ return DEFAULT_GENERATOR.toJson(n);
}
/**
* @return a JSON string representation of the character
*/
public static String toJson(Character c) {
- CharBuf buffer = CharBuf.create(3);
- writeObject(c, buffer); // checking null inside
-
- return buffer.toString();
+ return DEFAULT_GENERATOR.toJson(c);
}
/**
* @return a properly encoded string with escape sequences
*/
public static String toJson(String s) {
- if (s == null) {
- return NULL_VALUE;
- }
-
- CharBuf buffer = CharBuf.create(s.length() + 2);
- writeCharSequence(s, buffer);
-
- return buffer.toString();
+ return DEFAULT_GENERATOR.toJson(s);
}
/**
@@ -119,14 +98,7 @@ public class JsonOutput {
* @return a formatted date in the form of a string
*/
public static String toJson(Date date) {
- if (date == null) {
- return NULL_VALUE;
- }
-
- CharBuf buffer = CharBuf.create(26);
- writeDate(date, buffer);
-
- return buffer.toString();
+ return DEFAULT_GENERATOR.toJson(date);
}
/**
@@ -136,62 +108,35 @@ public class JsonOutput {
* @return a formatted date in the form of a string
*/
public static String toJson(Calendar cal) {
- if (cal == null) {
- return NULL_VALUE;
- }
-
- CharBuf buffer = CharBuf.create(26);
- writeDate(cal.getTime(), buffer);
-
- return buffer.toString();
+ return DEFAULT_GENERATOR.toJson(cal);
}
/**
* @return the string representation of an uuid
*/
public static String toJson(UUID uuid) {
- CharBuf buffer = CharBuf.create(64);
- writeObject(uuid, buffer); // checking null inside
-
- return buffer.toString();
+ return DEFAULT_GENERATOR.toJson(uuid);
}
/**
* @return the string representation of the URL
*/
public static String toJson(URL url) {
- CharBuf buffer = CharBuf.create(64);
- writeObject(url, buffer); // checking null inside
-
- return buffer.toString();
+ return DEFAULT_GENERATOR.toJson(url);
}
/**
* @return an object representation of a closure
*/
public static String toJson(Closure closure) {
- if (closure == null) {
- return NULL_VALUE;
- }
-
- CharBuf buffer = CharBuf.create(255);
- writeMap(JsonDelegate.cloneDelegateAndGetContent(closure), buffer);
-
- return buffer.toString();
+ return DEFAULT_GENERATOR.toJson(closure);
}
/**
* @return an object representation of an Expando
*/
public static String toJson(Expando expando) {
- if (expando == null) {
- return NULL_VALUE;
- }
-
- CharBuf buffer = CharBuf.create(255);
- writeMap(expando.getProperties(), buffer);
-
- return buffer.toString();
+ return DEFAULT_GENERATOR.toJson(expando);
}
/**
@@ -199,289 +144,14 @@ public class JsonOutput {
* or representation for other object.
*/
public static String toJson(Object object) {
- CharBuf buffer = CharBuf.create(255);
- writeObject(object, buffer); // checking null inside
-
- return buffer.toString();
+ return DEFAULT_GENERATOR.toJson(object);
}
/**
* @return a JSON object representation for a map
*/
public static String toJson(Map m) {
- if (m == null) {
- return NULL_VALUE;
- }
-
- CharBuf buffer = CharBuf.create(255);
- writeMap(m, buffer);
-
- return buffer.toString();
- }
-
- /**
- * Serializes Number value and writes it into specified buffer.
- */
- private static void writeNumber(Class<?> numberClass, Number value, CharBuf buffer) {
- if (numberClass == Integer.class) {
- buffer.addInt((Integer) value);
- } else if (numberClass == Long.class) {
- buffer.addLong((Long) value);
- } else if (numberClass == BigInteger.class) {
- buffer.addBigInteger((BigInteger) value);
- } else if (numberClass == BigDecimal.class) {
- buffer.addBigDecimal((BigDecimal) value);
- } else if (numberClass == Double.class) {
- Double doubleValue = (Double) value;
- if (doubleValue.isInfinite()) {
- throw new JsonException("Number " + value + " can't be serialized as JSON: infinite are not allowed in JSON.");
- }
- if (doubleValue.isNaN()) {
- throw new JsonException("Number " + value + " can't be serialized as JSON: NaN are not allowed in JSON.");
- }
-
- buffer.addDouble(doubleValue);
- } else if (numberClass == Float.class) {
- Float floatValue = (Float) value;
- if (floatValue.isInfinite()) {
- throw new JsonException("Number " + value + " can't be serialized as JSON: infinite are not allowed in JSON.");
- }
- if (floatValue.isNaN()) {
- throw new JsonException("Number " + value + " can't be serialized as JSON: NaN are not allowed in JSON.");
- }
-
- buffer.addFloat(floatValue);
- } else if (numberClass == Byte.class) {
- buffer.addByte((Byte) value);
- } else if (numberClass == Short.class) {
- buffer.addShort((Short) value);
- } else { // Handle other Number implementations
- buffer.addString(value.toString());
- }
- }
-
- /**
- * Serializes object and writes it into specified buffer.
- */
- private static void writeObject(Object object, CharBuf buffer) {
- if (object == null) {
- buffer.addNull();
- } else {
- Class<?> objectClass = object.getClass();
-
- if (CharSequence.class.isAssignableFrom(objectClass)) { // Handle String, StringBuilder, GString and other CharSequence implementations
- writeCharSequence((CharSequence) object, buffer);
- } else if (objectClass == Boolean.class) {
- buffer.addBoolean((Boolean) object);
- } else if (Number.class.isAssignableFrom(objectClass)) {
- writeNumber(objectClass, (Number) object, buffer);
- } else if (Date.class.isAssignableFrom(objectClass)) {
- writeDate((Date) object, buffer);
- } else if (Calendar.class.isAssignableFrom(objectClass)) {
- writeDate(((Calendar) object).getTime(), buffer);
- } else if (Map.class.isAssignableFrom(objectClass)) {
- writeMap((Map) object, buffer);
- } else if (Iterable.class.isAssignableFrom(objectClass)) {
- writeIterator(((Iterable<?>) object).iterator(), buffer);
- } else if (Iterator.class.isAssignableFrom(objectClass)) {
- writeIterator((Iterator) object, buffer);
- } else if (objectClass == Character.class) {
- buffer.addJsonEscapedString(Chr.array((Character) object));
- } else if (objectClass == URL.class) {
- buffer.addJsonEscapedString(object.toString());
- } else if (objectClass == UUID.class) {
- buffer.addQuoted(object.toString());
- } else if (objectClass == JsonUnescaped.class) {
- buffer.add(object.toString());
- } else if (Closure.class.isAssignableFrom(objectClass)) {
- writeMap(JsonDelegate.cloneDelegateAndGetContent((Closure<?>) object), buffer);
- } else if (Expando.class.isAssignableFrom(objectClass)) {
- writeMap(((Expando) object).getProperties(), buffer);
- } else if (Enumeration.class.isAssignableFrom(objectClass)) {
- List<?> list = Collections.list((Enumeration<?>) object);
- writeIterator(list.iterator(), buffer);
- } else if (objectClass.isArray()) {
- writeArray(objectClass, object, buffer);
- } else if (Enum.class.isAssignableFrom(objectClass)) {
- buffer.addQuoted(((Enum<?>) object).name());
- }else if (File.class.isAssignableFrom(objectClass)){
- Map<?, ?> properties = getObjectProperties(object);
- //Clean up all recursive references to File objects
- Iterator<? extends Map.Entry<?, ?>> iterator = properties.entrySet().iterator();
- while(iterator.hasNext()){
- Map.Entry<?,?> entry = iterator.next();
- if(entry.getValue() instanceof File){
- iterator.remove();
- }
- }
-
- writeMap(properties, buffer);
- } else {
- Map<?, ?> properties = getObjectProperties(object);
- writeMap(properties, buffer);
- }
- }
- }
-
- private static Map<?, ?> getObjectProperties(Object object) {
- Map<?, ?> properties = DefaultGroovyMethods.getProperties(object);
- properties.remove("class");
- properties.remove("declaringClass");
- properties.remove("metaClass");
- return properties;
- }
-
-
- /**
- * Serializes any char sequence and writes it into specified buffer.
- */
- private static void writeCharSequence(CharSequence seq, CharBuf buffer) {
- if (seq.length() > 0) {
- buffer.addJsonEscapedString(seq.toString());
- } else {
- buffer.addChars(EMPTY_STRING_CHARS);
- }
- }
-
- /**
- * Serializes date and writes it into specified buffer.
- */
- private static void writeDate(Date date, CharBuf buffer) {
- SimpleDateFormat formatter = new SimpleDateFormat(JSON_DATE_FORMAT, Locale.US);
- formatter.setTimeZone(TimeZone.getTimeZone(DEFAULT_TIMEZONE));
- buffer.addQuoted(formatter.format(date));
- }
-
- /**
- * Serializes array and writes it into specified buffer.
- */
- private static void writeArray(Class<?> arrayClass, Object array, CharBuf buffer) {
- 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(COMMA);
- writeObject(objArray[i], buffer);
- }
- }
- } else if (int[].class.isAssignableFrom(arrayClass)) {
- int[] intArray = (int[]) array;
- if (intArray.length > 0) {
- buffer.addInt(intArray[0]);
- for (int i = 1; i < intArray.length; i++) {
- buffer.addChar(COMMA).addInt(intArray[i]);
- }
- }
- } else if (long[].class.isAssignableFrom(arrayClass)) {
- long[] longArray = (long[]) array;
- if (longArray.length > 0) {
- buffer.addLong(longArray[0]);
- for (int i = 1; i < longArray.length; i++) {
- buffer.addChar(COMMA).addLong(longArray[i]);
- }
- }
- } else if (boolean[].class.isAssignableFrom(arrayClass)) {
- boolean[] booleanArray = (boolean[]) array;
- if (booleanArray.length > 0) {
- buffer.addBoolean(booleanArray[0]);
- for (int i = 1; i < booleanArray.length; i++) {
- buffer.addChar(COMMA).addBoolean(booleanArray[i]);
- }
- }
- } else if (char[].class.isAssignableFrom(arrayClass)) {
- char[] charArray = (char[]) array;
- if (charArray.length > 0) {
- buffer.addJsonEscapedString(Chr.array(charArray[0]));
- for (int i = 1; i < charArray.length; i++) {
- buffer.addChar(COMMA).addJsonEscapedString(Chr.array(charArray[i]));
- }
- }
- } else if (double[].class.isAssignableFrom(arrayClass)) {
- double[] doubleArray = (double[]) array;
- if (doubleArray.length > 0) {
- buffer.addDouble(doubleArray[0]);
- for (int i = 1; i < doubleArray.length; i++) {
- buffer.addChar(COMMA).addDouble(doubleArray[i]);
- }
- }
- } else if (float[].class.isAssignableFrom(arrayClass)) {
- float[] floatArray = (float[]) array;
- if (floatArray.length > 0) {
- buffer.addFloat(floatArray[0]);
- for (int i = 1; i < floatArray.length; i++) {
- buffer.addChar(COMMA).addFloat(floatArray[i]);
- }
- }
- } else if (byte[].class.isAssignableFrom(arrayClass)) {
- byte[] byteArray = (byte[]) array;
- if (byteArray.length > 0) {
- buffer.addByte(byteArray[0]);
- for (int i = 1; i < byteArray.length; i++) {
- buffer.addChar(COMMA).addByte(byteArray[i]);
- }
- }
- } else if (short[].class.isAssignableFrom(arrayClass)) {
- short[] shortArray = (short[]) array;
- if (shortArray.length > 0) {
- buffer.addShort(shortArray[0]);
- for (int i = 1; i < shortArray.length; i++) {
- buffer.addChar(COMMA).addShort(shortArray[i]);
- }
- }
- }
- buffer.addChar(CLOSE_BRACKET);
- }
-
- 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(OPEN_BRACE);
- boolean firstItem = true;
- for (Map.Entry<?, ?> entry : map.entrySet()) {
- if (entry.getKey() == null) {
- throw new IllegalArgumentException("Maps with null keys can\'t be converted to JSON");
- }
-
- if (!firstItem) {
- buffer.addChar(COMMA);
- } else {
- firstItem = false;
- }
-
- buffer.addJsonFieldName(entry.getKey().toString());
- writeObject(entry.getValue(), buffer);
- }
- buffer.addChar(CLOSE_BRACE);
- } else {
- buffer.addChars(EMPTY_MAP_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(OPEN_BRACKET);
- Object it = iterator.next();
- writeObject(it, buffer);
- while (iterator.hasNext()) {
- it = iterator.next();
- buffer.addChar(COMMA);
- writeObject(it, buffer);
- }
- buffer.addChar(CLOSE_BRACKET);
- } else {
- buffer.addChars(EMPTY_LIST_CHARS);
- }
+ return DEFAULT_GENERATOR.toJson(m);
}
/**
http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/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 e52986f..69d5173 100644
--- a/subprojects/groovy-json/src/main/java/groovy/json/StreamingJsonBuilder.java
+++ b/subprojects/groovy-json/src/main/java/groovy/json/StreamingJsonBuilder.java
@@ -73,6 +73,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
private static final String COLON_WITH_OPEN_BRACE = ":{";
private final Writer writer;
+ private final JsonGenerator generator;
/**
* Instantiates a JSON builder.
@@ -81,6 +82,19 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
*/
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;
}
/**
@@ -88,11 +102,27 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
*
* @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);
+ 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(JsonOutput.toJson(content));
+ writer.write(generator.toJson(content));
}
}
@@ -113,7 +143,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
* @return a map of key / value pairs
*/
public Object call(Map m) throws IOException {
- writer.write(JsonOutput.toJson(m));
+ writer.write(generator.toJson(m));
return m;
}
@@ -133,7 +163,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
* @throws IOException
*/
public void call(String name) throws IOException {
- writer.write(JsonOutput.toJson(Collections.singletonMap(name, Collections.emptyMap())));
+ writer.write(generator.toJson(Collections.singletonMap(name, Collections.emptyMap())));
}
/**
@@ -154,7 +184,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
* @return a list of values
*/
public Object call(List l) throws IOException {
- writer.write(JsonOutput.toJson(l));
+ writer.write(generator.toJson(l));
return l;
}
@@ -204,7 +234,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
* @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);
+ return StreamingJsonDelegate.writeCollectionWithClosure(writer, coll, c, generator);
}
/**
@@ -234,7 +264,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
*/
public Object call(@DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
writer.write(JsonOutput.OPEN_BRACE);
- StreamingJsonDelegate.cloneDelegateAndGetContent(writer, c);
+ StreamingJsonDelegate.cloneDelegateAndGetContent(writer, c, true, generator);
writer.write(JsonOutput.CLOSE_BRACE);
return null;
@@ -261,7 +291,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
*/
public void call(String name, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
writer.write(JsonOutput.OPEN_BRACE);
- writer.write(JsonOutput.toJson(name));
+ writer.write(generator.toJson(name));
writer.write(JsonOutput.COLON);
call(c);
writer.write(JsonOutput.CLOSE_BRACE);
@@ -292,7 +322,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
*/
public void call(String name, Iterable coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
writer.write(JsonOutput.OPEN_BRACE);
- writer.write(JsonOutput.toJson(name));
+ writer.write(generator.toJson(name));
writer.write(JsonOutput.COLON);
call(coll, c);
writer.write(JsonOutput.CLOSE_BRACE);
@@ -329,7 +359,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
*/
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(generator.toJson(name));
writer.write(COLON_WITH_OPEN_BRACE);
boolean first = true;
for (Object it : map.entrySet()) {
@@ -340,11 +370,19 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
}
Map.Entry entry = (Map.Entry) it;
- writer.write(JsonOutput.toJson(entry.getKey()));
+ 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(JsonOutput.toJson(entry.getValue()));
+ writer.write(generator.toJson(value));
}
- StreamingJsonDelegate.cloneDelegateAndGetContent(writer, callable, map.size() == 0);
+ StreamingJsonDelegate.cloneDelegateAndGetContent(writer, callable, map.size() == 0, generator);
writer.write(DOUBLE_CLOSE_BRACKET);
}
@@ -479,10 +517,16 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
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;
}
/**
@@ -548,6 +592,9 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
* @throws IOException
*/
public void call(String name, List<Object> list) throws IOException {
+ if (generator.isExcludingFieldsNamed(name)) {
+ return;
+ }
writeName(name);
writeArray(list);
}
@@ -559,6 +606,9 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
* @throws IOException
*/
public void call(String name, Object...array) throws IOException {
+ if (generator.isExcludingFieldsNamed(name)) {
+ return;
+ }
writeName(name);
writeArray(Arrays.asList(array));
}
@@ -589,6 +639,9 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
* @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);
}
@@ -608,6 +661,9 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
* @throws IOException
*/
public void call(String name, Object value) throws IOException {
+ if (generator.isExcludingFieldsNamed(name) || generator.isExcludingValues(value)) {
+ return;
+ }
writeName(name);
writeValue(value);
}
@@ -620,9 +676,12 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
* @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);
+ writeObject(writer, value, callable, generator);
}
/**
* Writes the name and another JSON object
@@ -632,10 +691,13 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
* @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);
+ StreamingJsonDelegate.cloneDelegateAndGetContent(writer, value, true, generator);
writer.write(JsonOutput.CLOSE_BRACE);
}
@@ -647,6 +709,9 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
* @throws IOException
*/
public void call(String name, JsonOutput.JsonUnescaped json) throws IOException {
+ if (generator.isExcludingFieldsNamed(name)) {
+ return;
+ }
writeName(name);
verifyValue();
writer.write(json.toString());
@@ -672,7 +737,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
private void writeObjects(Iterable coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
verifyValue();
- writeCollectionWithClosure(writer, coll, c);
+ writeCollectionWithClosure(writer, coll, c, generator);
}
protected void verifyValue() {
@@ -686,6 +751,9 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
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!");
}
@@ -697,18 +765,21 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
} else {
first = false;
}
- writer.write(JsonOutput.toJson(name));
+ writer.write(generator.toJson(name));
writer.write(JsonOutput.COLON);
}
protected void writeValue(Object value) throws IOException {
+ if (generator.isExcludingValues(value)) {
+ return;
+ }
verifyValue();
- writer.write(JsonOutput.toJson(value));
+ writer.write(generator.toJson(value));
}
protected void writeArray(List<Object> list) throws IOException {
verifyValue();
- writer.write(JsonOutput.toJson(list));
+ writer.write(generator.toJson(list));
}
public static boolean isCollectionWithClosure(Object[] args) {
@@ -716,10 +787,11 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
}
public static Object writeCollectionWithClosure(Writer writer, Collection coll, @DelegatesTo(StreamingJsonDelegate.class) Closure closure) throws IOException {
- return writeCollectionWithClosure(writer, (Iterable)coll, closure);
+ return writeCollectionWithClosure(writer, (Iterable)coll, closure, JsonOutput.DEFAULT_GENERATOR);
}
- public static Object writeCollectionWithClosure(Writer writer, Iterable coll, @DelegatesTo(StreamingJsonDelegate.class) Closure closure) throws IOException {
+ 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) {
@@ -729,16 +801,16 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
first = false;
}
- writeObject(writer, it, closure);
+ writeObject(writer, it, closure, generator);
}
writer.write(JsonOutput.CLOSE_BRACKET);
return writer;
}
- private static void writeObject(Writer writer, Object object, Closure closure) throws IOException {
+ private static void writeObject(Writer writer, Object object, Closure closure, JsonGenerator generator) throws IOException {
writer.write(JsonOutput.OPEN_BRACE);
- curryDelegateAndGetContent(writer, closure, object);
+ curryDelegateAndGetContent(writer, closure, object, true, generator);
writer.write(JsonOutput.CLOSE_BRACE);
}
@@ -748,7 +820,11 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
}
public static void cloneDelegateAndGetContent(Writer w, @DelegatesTo(StreamingJsonDelegate.class) Closure c, boolean first) {
- StreamingJsonDelegate delegate = new StreamingJsonDelegate(w, 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);
@@ -760,7 +836,11 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
}
public static void curryDelegateAndGetContent(Writer w, @DelegatesTo(StreamingJsonDelegate.class) Closure c, Object o, boolean first) {
- StreamingJsonDelegate delegate = new StreamingJsonDelegate(w, 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);
@@ -772,5 +852,3 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
}
}
}
-
-
http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/main/java/groovy/json/internal/CharBuf.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/groovy/json/internal/CharBuf.java b/subprojects/groovy-json/src/main/java/groovy/json/internal/CharBuf.java
index feaa614..18f5d6a 100644
--- a/subprojects/groovy-json/src/main/java/groovy/json/internal/CharBuf.java
+++ b/subprojects/groovy-json/src/main/java/groovy/json/internal/CharBuf.java
@@ -341,32 +341,34 @@ public class CharBuf extends Writer implements CharSequence {
}
public final CharBuf addJsonEscapedString(String jsonString) {
+ return addJsonEscapedString(jsonString, false);
+ }
+
+ public final CharBuf addJsonEscapedString(String jsonString, boolean disableUnicodeEscaping) {
char[] charArray = FastStringUtils.toCharArray(jsonString);
- return addJsonEscapedString(charArray);
+ return addJsonEscapedString(charArray, disableUnicodeEscaping);
}
- private static boolean hasAnyJSONControlOrUnicodeChars(int c) {
- /* Anything less than space is a control character. */
- if (c < 30) {
+ private static boolean shouldEscape(int c, boolean disableUnicodeEscaping) {
+ if (c < 32) { /* less than space is a control char */
return true;
- /* 34 is double quote. */
- } else if (c == 34) {
+ } else if (c == 34) { /* double quote */
return true;
- } else if (c == 92) {
+ } else if (c == 92) { /* backslash */
return true;
- } else if (c < ' ' || c > 126) {
+ } else if (!disableUnicodeEscaping && c > 126) { /* non-ascii char range */
return true;
}
return false;
}
- private static boolean hasAnyJSONControlChars(final char[] charArray) {
+ private static boolean hasAnyJSONControlChars(final char[] charArray, boolean disableUnicodeEscaping) {
int index = 0;
char c;
while (true) {
c = charArray[index];
- if (hasAnyJSONControlOrUnicodeChars(c)) {
+ if (shouldEscape(c, disableUnicodeEscaping)) {
return true;
}
if (++index >= charArray.length) return false;
@@ -374,9 +376,13 @@ public class CharBuf extends Writer implements CharSequence {
}
public final CharBuf addJsonEscapedString(final char[] charArray) {
+ return addJsonEscapedString(charArray, false);
+ }
+
+ public final CharBuf addJsonEscapedString(final char[] charArray, boolean disableUnicodeEscaping) {
if (charArray.length == 0) return this;
- if (hasAnyJSONControlChars(charArray)) {
- return doAddJsonEscapedString(charArray);
+ if (hasAnyJSONControlChars(charArray, disableUnicodeEscaping)) {
+ return doAddJsonEscapedString(charArray, disableUnicodeEscaping);
} else {
return this.addQuoted(charArray);
}
@@ -386,7 +392,7 @@ public class CharBuf extends Writer implements CharSequence {
final byte[] charTo = new byte[2];
- private CharBuf doAddJsonEscapedString(char[] charArray) {
+ private CharBuf doAddJsonEscapedString(char[] charArray, boolean disableUnicodeEscaping) {
char[] _buffer = buffer;
int _location = this.location;
@@ -410,7 +416,7 @@ public class CharBuf extends Writer implements CharSequence {
while (true) {
char c = charArray[index];
- if (hasAnyJSONControlOrUnicodeChars(c)) {
+ if (shouldEscape(c, disableUnicodeEscaping)) {
/* We are covering our bet with a safety net.
otherwise we would have to have 5x buffer
allocated for control chars */
@@ -514,14 +520,22 @@ public class CharBuf extends Writer implements CharSequence {
}
public final CharBuf addJsonFieldName(String str) {
- return addJsonFieldName(FastStringUtils.toCharArray(str));
+ return addJsonFieldName(str, false);
+ }
+
+ public final CharBuf addJsonFieldName(String str, boolean disableUnicodeEscaping) {
+ return addJsonFieldName(FastStringUtils.toCharArray(str), disableUnicodeEscaping);
}
private static final char[] EMPTY_STRING_CHARS = Chr.array('"', '"');
public final CharBuf addJsonFieldName(char[] chars) {
+ return addJsonFieldName(chars, false);
+ }
+
+ public final CharBuf addJsonFieldName(char[] chars, boolean disableUnicodeEscaping) {
if (chars.length > 0) {
- addJsonEscapedString(chars);
+ addJsonEscapedString(chars, disableUnicodeEscaping);
} else {
addChars(EMPTY_STRING_CHARS);
}
@@ -671,7 +685,16 @@ public class CharBuf extends Writer implements CharSequence {
}
public void removeLastChar() {
- location--;
+ if (location > 0) {
+ location--;
+ }
+ }
+
+ public void removeLastChar(char expect) {
+ if (location == 0 || buffer[location-1] != expect) {
+ return;
+ }
+ removeLastChar();
}
private Cache<BigDecimal, char[]> bigDCache;
http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/spec/doc/json-builder.adoc
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/spec/doc/json-builder.adoc b/subprojects/groovy-json/src/spec/doc/json-builder.adoc
index dcf21d4..28ffd58 100644
--- a/subprojects/groovy-json/src/spec/doc/json-builder.adoc
+++ b/subprojects/groovy-json/src/spec/doc/json-builder.adoc
@@ -40,4 +40,11 @@ We use https://github.com/lukas-krecan/JsonUnit[JsonUnit] to check that the buil
[source,groovy]
----
include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/JsonBuilderTest.groovy[tags=json_assert,indent=0]
-----
\ No newline at end of file
+----
+
+If you need to customize the generated output you can pass a `JsonGenerator` instance when creating a `JsonBuilder`:
+
+[source,groovy]
+----
+include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/JsonBuilderTest.groovy[tags=json_builder_generator,indent=0]
+----
http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/spec/doc/json-userguide.adoc
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/spec/doc/json-userguide.adoc b/subprojects/groovy-json/src/spec/doc/json-userguide.adoc
index 5557568..683e403 100644
--- a/subprojects/groovy-json/src/spec/doc/json-userguide.adoc
+++ b/subprojects/groovy-json/src/spec/doc/json-userguide.adoc
@@ -159,7 +159,7 @@ include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/JsonTest.gr
<<json-userguide.adoc#json_jsonslurper,JsonSlurper>>, being a JSON parser.
`JsonOutput` comes with overloaded, static `toJson` methods. Each `toJson` implementation takes a different parameter type.
-The static method can either be used directly or by importing the methods with a static import statement.
+The static methods can either be used directly or by importing the methods with a static import statement.
The result of a `toJson` call is a `String` containing the JSON code.
@@ -176,6 +176,30 @@ has support for serialising POGOs, that is, plain-old Groovy objects.
include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/JsonTest.groovy[tags=json_output_pogo,indent=0]
----
+=== Customizing Output
+
+If you need control over the serialized output you can use a `JsonGenerator`. The `JsonGenerator.Options` builder
+can be used to create a customized generator. One or more options can be set on this builder in order to alter
+the resulting output. When you are done setting the options simply call the `build()` method in order to get a fully
+configured instance that will generate output based on the options selected.
+
+[source,groovy]
+----
+include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/JsonTest.groovy[tags=json_output_generator,indent=0]
+----
+
+A closure can be used to transform a type into a valid JSON value. These closure converters are registered
+for a given type and will be called any time that type or a subtype is encountered. The first parameter to the
+closure is an object matching the type for which the converter is registered and this parameter is required.
+The closure may take an optional second `String` parameter and this will be set to the key name if one is available.
+
+[source,groovy]
+----
+include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/JsonTest.groovy[tags=json_output_converter,indent=0]
+----
+
+==== Formatted Output
+
As we saw in previous examples, the JSON output is not pretty printed per default. However, the `prettyPrint` method in `JsonOutput` comes
to rescue for this task.
@@ -187,6 +211,8 @@ include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/JsonTest.gr
`prettyPrint` takes a `String` as single parameter; therefore, it can be applied on arbitrary JSON `String` instances, not only the result of
`JsonOutput.toJson`.
+=== Builders
+
Another way to create JSON from Groovy is to use `JsonBuilder` or `StreamingJsonBuilder`. Both builders provide a
DSL which allows to formulate an object graph which is then converted to JSON.
http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/spec/doc/streaming-jason-builder.adoc
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/spec/doc/streaming-jason-builder.adoc b/subprojects/groovy-json/src/spec/doc/streaming-jason-builder.adoc
index 296794e..98d3e59 100644
--- a/subprojects/groovy-json/src/spec/doc/streaming-jason-builder.adoc
+++ b/subprojects/groovy-json/src/spec/doc/streaming-jason-builder.adoc
@@ -44,4 +44,11 @@ We use https://github.com/lukas-krecan/JsonUnit[JsonUnit] to check the expected
[source,groovy]
----
include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/StreamingJsonBuilderTest.groovy[tags=json_assert,indent=0]
-----
\ No newline at end of file
+----
+
+If you need to customize the generated output you can pass a `JsonGenerator` instance when creating a `StreamingJsonBuilder`:
+
+[source,groovy]
+----
+include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/StreamingJsonBuilderTest.groovy[tags=streaming_json_builder_generator,indent=0]
+----
http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/spec/test/json/JsonBuilderTest.groovy
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/spec/test/json/JsonBuilderTest.groovy b/subprojects/groovy-json/src/spec/test/json/JsonBuilderTest.groovy
index 87ab5f1..d4a1576 100644
--- a/subprojects/groovy-json/src/spec/test/json/JsonBuilderTest.groovy
+++ b/subprojects/groovy-json/src/spec/test/json/JsonBuilderTest.groovy
@@ -69,4 +69,36 @@ class JsonBuilderTest extends GroovyTestCase {
// end::json_assert[]
"""
}
+
+ void testJsonBuilderWithGenerator() {
+ assertScript """
+ // tag::json_builder_generator[]
+ import groovy.json.*
+
+ def generator = new JsonGenerator.Options()
+ .excludeNulls()
+ .excludeFieldsByName('make', 'country', 'record')
+ .excludeFieldsByType(Number)
+ .addConverter(URL) { url -> '"http://groovy-lang.org"' }
+ .build()
+
+ JsonBuilder builder = new JsonBuilder(generator)
+ builder.records {
+ car {
+ name 'HSV Maloo'
+ make 'Holden'
+ year 2006
+ country 'Australia'
+ homepage new URL('http://example.org')
+ record {
+ type 'speed'
+ description 'production pickup truck with speed of 271kph'
+ }
+ }
+ }
+
+ assert builder.toString() == '{"records":{"car":{"name":"HSV Maloo","homepage":"http://groovy-lang.org"}}}'
+ // end::json_builder_generator[]
+ """
+ }
}