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/parrot Commit: 132025997e110e284fe1ce5e4d19a75ae5418ae6 Parents: 8213bd0 Author: John Wagenleitner <[email protected]> Authored: Sun Oct 23 16:40:11 2016 -0700 Committer: John Wagenleitner <[email protected]> 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[] + """ + } }
