grails.plugin.json.builder.DefaultJsonGenerator Maven / Gradle / Ivy
Show all versions of views-json Show documentation
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package grails.plugin.json.builder;
import groovy.json.JsonDelegate;
import groovy.json.JsonException;
import groovy.lang.Closure;
import groovy.util.Expando;
import org.apache.groovy.json.internal.CharBuf;
import org.apache.groovy.json.internal.Chr;
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 grails.plugin.json.builder.JsonOutput.CLOSE_BRACE;
import static grails.plugin.json.builder.JsonOutput.CLOSE_BRACKET;
import static grails.plugin.json.builder.JsonOutput.COMMA;
import static grails.plugin.json.builder.JsonOutput.EMPTY_LIST_CHARS;
import static grails.plugin.json.builder.JsonOutput.EMPTY_MAP_CHARS;
import static grails.plugin.json.builder.JsonOutput.EMPTY_STRING_CHARS;
import static grails.plugin.json.builder.JsonOutput.OPEN_BRACE;
import static grails.plugin.json.builder.JsonOutput.OPEN_BRACKET;
/**
* Temporary fork of DefaultJsonGenerator until Groovy 2.5.0 is out.
* 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 converters = new LinkedHashSet<>();
protected final Set excludedFieldNames = new HashSet<>();
protected final Set> excludedFieldTypes = new HashSet<>();
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.
*
* @param numberClass The object {@link Class}
* @param value a {@link Number}
* @param buffer a {@link CharBuf} buffer to write the serialized value
*/
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.
*
* @param key the name for the value
* @param object to convert to JSON
* @param buffer a {@link CharBuf} buffer to write the serialized value
*/
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) {
object = converter.convert(object, key);
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), 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
properties.entrySet().removeIf(entry -> entry.getValue() instanceof File);
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.
*
* @param seq a {@link CharSequence} to serialize and encode
* @param buffer a {@link CharBuf} buffer to write the serialized value
*/
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.
*
* @param seq a {@link CharSequence} to serialize to string
* @param buffer a {@link CharBuf} buffer to write the serialized value
*/
protected void writeRaw(CharSequence seq, CharBuf buffer) {
if (seq != null) {
buffer.add(seq.toString());
}
}
/**
* Serializes date and writes it into specified buffer.
*
* @param date A {@link Date} to serialize into JSON string
* @param buffer a {@link CharBuf} buffer to write the serialized value
*/
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.
*
* @param arrayClass the Array class
* @param array an array to serialize into JSON
* @param buffer a {@link CharBuf} buffer to write the serialized value
*/
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.
*
* @param map a {@link Map} to serialize to JSON string
* @param buffer a {@link CharBuf} buffer to write the serialized value
*/
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.
*
* @param key a {@link Map.Entry} key
* @param value a {@link Map.Entry} value
* @param buffer a {@link CharBuf} buffer to write the serialized value
*/
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.
*
* @param iterator a {@link Iterator} to serialize to JSON string
* @param buffer a {@link CharBuf} buffer to write the serialized value
*/
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> closure;
protected final int paramCount;
protected ClosureConverter(Class> type, Closure> 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);
}
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);
}
}
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
*/
@Override
public boolean handles(Class> type) {
return this.type.isAssignableFrom(type);
}
/**
* Converts a given value.
*
* @param value the object to convert
* @param key the key name for the value, may be {@code null}
* @return the converted object
*/
@Override
public Object 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() + ">";
}
}
}