/* * 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) { 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 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 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.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 */ @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() + ">"; } } }