Github user paulk-asert commented on a diff in the pull request:
https://github.com/apache/groovy/pull/371#discussion_r82374539
--- Diff: subprojects/groovy-json/src/main/java/groovy/json/JsonOutput.java
---
@@ -601,4 +280,966 @@ public String toString() {
}
}
+ /**
+ * Creates a builder for various options that can be set to alter the
+ * generated JSON. After setting the options a call to
+ * {@link Options#createGenerator()} will return a fully configured
+ * {@link JsonOutput.Generator} object and the {@code toJson} methods
+ * can be used.
+ *
+ * @return a builder for building a JsonOutput.Generator
+ * with the specified options set.
+ * @since 2.5
+ */
+ public static Options options() {
+ return new Options();
+ }
+
+ /**
+ * A builder used to construct a {@link JsonOutput.Generator} 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 = groovy.json.JsonOutput.options()
+ * .excludeNulls()
+ * .dateFormat('yyyy')
+ * .excludeFieldsByName('bar', 'baz')
+ * .excludeFieldsByType(java.sql.Date)
+ * .createGenerator()
+ *
+ * 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
+ */
+ public static class Options {
+
+ private boolean excludeNulls;
+
+ private boolean disableUnicodeEscaping;
+
+ private String dateFormat = JsonOutput.JSON_DATE_FORMAT;
+
+ private Locale dateLocale = JsonOutput.JSON_DATE_FORMAT_LOCALE;
+
+ private TimeZone timezone =
TimeZone.getTimeZone(JsonOutput.DEFAULT_TIMEZONE);
+
+ private final Set<Converter> converters = new
LinkedHashSet<Converter>();
+
+ private final Set<String> excludedFieldNames = new
HashSet<String>();
+
+ private final Set<Class<?>> excludedFieldTypes = new
HashSet<Class<?>>();
+
+ private 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, JsonOutput.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 = groovy.json.JsonOutput.options()
+ * .addConverter(URL) { URL u ->
+ * "\"${u.getHost()}\""
+ * }
+ * .createGenerator()
+ *
+ * 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 = Converter.of(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 JsonOutput.Generator} that is based on the
current options.
+ *
+ * @return a fully configured {@link JsonOutput.Generator}
+ */
+ public Generator createGenerator() {
+ return new Generator(this);
+ }
+ }
+
+ /**
+ * A JsonOutput Generator that can be configured with various {@link
JsonOutput.Options}.
+ * If the default options are sufficient consider using the static
{@code JsonOutput.toJson}
+ * methods.
+ *
+ * @see JsonOutput#options()
+ * @see Options#createGenerator()
+ * @since 2.5
+ */
+ public static class Generator {
+
+ private final boolean excludeNulls;
+ private final boolean disableUnicodeEscaping;
+ private final String dateFormat;
+ private final Locale dateLocale;
+ private final TimeZone timezone;
+
+ private final Set<Converter> converters = new
LinkedHashSet<Converter>();
+
+ private final Set<String> excludedFieldNames = new
HashSet<String>();
+
+ private final Set<Class<?>> excludedFieldTypes = new
HashSet<Class<?>>();
+
+ private final String nullValue;
+
+ private final boolean hasConverters;
+ private final boolean hasExcludedFieldNames;
+ private final boolean hasExcludedFieldTypes;
+
+ private Generator(Options options) {
+ excludeNulls = options.excludeNulls;
+ disableUnicodeEscaping = options.disableUnicodeEscaping;
+ nullValue = (excludeNulls) ? JsonOutput.EMPTY_VALUE :
JsonOutput.NULL_VALUE;
+ dateFormat = options.dateFormat;
+ dateLocale = options.dateLocale;
+ timezone = options.timezone;
+ if (!options.converters.isEmpty()) {
+ converters.addAll(options.converters);
+ hasConverters = true;
+ } else {
+ hasConverters = false;
+ }
+ if (!options.excludedFieldNames.isEmpty()) {
+ excludedFieldNames.addAll(options.excludedFieldNames);
+ hasExcludedFieldNames = true;
+ } else {
+ hasExcludedFieldNames = false;
+ }
+ if (!options.excludedFieldTypes.isEmpty()) {
+ excludedFieldTypes.addAll(options.excludedFieldTypes);
+ hasExcludedFieldTypes = true;
+ } else {
+ hasExcludedFieldTypes = false;
+ }
+ }
+
+ /**
+ * @see JsonOutput#toJson(Boolean)
+ */
+ public String toJson(Boolean bool) {
+ CharBuf buffer = CharBuf.create(4);
+ writeObject(bool, buffer); // checking null inside
+
+ return buffer.toString();
+ }
+
+ /**
+ * @see JsonOutput#toJson(Number)
+ */
+ public String toJson(Number n) {
+ if (n == null) {
+ return nullValue;
+ }
+
+ CharBuf buffer = CharBuf.create(3);
+ Class<?> numberClass = n.getClass();
+
+ if (shouldExcludeType(numberClass)) {
+ return EMPTY_VALUE;
+ }
+
+ Converter converter = findConverter(numberClass);
+ if (converter != null) {
+ writeRaw(converter.convert(n), buffer);
+ } else {
+ writeNumber(numberClass, n, buffer);
+ }
+
+ return buffer.toString();
+ }
+
+ /**
+ * @see JsonOutput#toJson(Character)
+ */
+ public String toJson(Character c) {
+ CharBuf buffer = CharBuf.create(3);
+ writeObject(c, buffer); // checking null inside
+
+ return buffer.toString();
+ }
+
+ /**
+ * @see JsonOutput#toJson(String)
+ */
+ public String toJson(String s) {
+ if (s == null) {
+ return nullValue;
+ }
+
+ CharBuf buffer = CharBuf.create(s.length() + 2);
+ writeCharSequence(s, buffer);
+
+ return buffer.toString();
+ }
+
+ /**
+ * @see JsonOutput#toJson(Date)
+ */
+ public String toJson(Date date) {
+ if (date == null) {
+ return nullValue;
+ }
+
+ if (shouldExcludeType(date.getClass())) {
+ return EMPTY_VALUE;
+ }
+
+ CharBuf buffer = CharBuf.create(26);
+
+ Converter converter = findConverter(date.getClass());
+ if (converter != null) {
+ writeRaw(converter.convert(date), buffer);
+ } else {
+ writeDate(date, buffer);
+ }
+
+ return buffer.toString();
+ }
+
+ /**
+ * @see JsonOutput#toJson(Calendar)
+ */
+ public String toJson(Calendar cal) {
+ if (cal == null) {
+ return nullValue;
+ }
+
+ if (shouldExcludeType(cal.getClass())) {
+ return EMPTY_VALUE;
+ }
+
+ CharBuf buffer = CharBuf.create(26);
+
+ Converter converter = findConverter(cal.getClass());
+ if (converter != null) {
+ writeRaw(converter.convert(cal), buffer);
+ } else {
+ writeDate(cal.getTime(), buffer);
+ }
+
+ return buffer.toString();
+ }
+
+ /**
+ * @see JsonOutput#toJson(UUID)
+ */
+ public String toJson(UUID uuid) {
+ CharBuf buffer = CharBuf.create(64);
+ writeObject(uuid, buffer); // checking null inside
+
+ return buffer.toString();
+ }
+
+ /**
+ * @see JsonOutput#toJson(URL)
+ */
+ public String toJson(URL url) {
+ CharBuf buffer = CharBuf.create(64);
+ writeObject(url, buffer); // checking null inside
+
+ return buffer.toString();
+ }
+
+ /**
+ * @see JsonOutput#toJson(Closure)
+ */
+ public String toJson(Closure closure) {
+ if (closure == null) {
+ return nullValue;
+ }
+
+ if (shouldExcludeType(closure.getClass())) {
+ return EMPTY_VALUE;
+ }
+
+ CharBuf buffer = CharBuf.create(255);
+ writeMap(JsonDelegate.cloneDelegateAndGetContent(closure),
buffer);
+
+ return buffer.toString();
+ }
+
+ /**
+ * @see JsonOutput#toJson(Expando)
+ */
+ public String toJson(Expando expando) {
+ if (expando == null) {
+ return nullValue;
+ }
+
+ if (shouldExcludeType(expando.getClass())) {
+ return EMPTY_VALUE;
+ }
+
+ CharBuf buffer = CharBuf.create(255);
+ writeMap(expando.getProperties(), buffer);
+
+ return buffer.toString();
+ }
+
+ /**
+ * @see JsonOutput#toJson(Object)
+ */
+ public String toJson(Object object) {
+ CharBuf buffer = CharBuf.create(255);
+ writeObject(object, buffer); // checking null inside
+
+ return buffer.toString();
+ }
+
+ /**
+ * @see JsonOutput#toJson(Map)
+ */
+ public String toJson(Map m) {
+ if (m == null) {
+ return nullValue;
+ }
+
+ if (shouldExcludeType(m.getClass())) {
+ return EMPTY_VALUE;
+ }
+
+ CharBuf buffer = CharBuf.create(255);
+ writeMap(m, buffer);
+
+ return buffer.toString();
+ }
+
+ /**
+ * Serializes Number value and writes it into specified buffer.
+ */
+ private 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());
+ }
+ }
+
+ private void writeObject(Object object, CharBuf buffer) {
+ writeObject(null, object, buffer);
+ }
+
+ /**
+ * Serializes object and writes it into specified buffer.
+ */
+ private void writeObject(String key, Object object, CharBuf
buffer) {
+ if (object == null) {
+ if (!excludeNulls) {
+ buffer.addNull();
+ }
+ return;
+ }
+
+ Class<?> objectClass = object.getClass();
+
+ if (shouldExcludeType(objectClass)) {
+ return;
+ }
+
+ 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 == 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 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.
+ */
+ private void writeRaw(CharSequence seq, CharBuf buffer) {
+ if (seq != null) {
+ buffer.add(seq.toString());
+ }
+ }
+
+ /**
+ * Serializes date and writes it into specified buffer.
+ */
+ private 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.
+ */
+ private 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.
+ */
+ private 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");
+ }
+
+ String key = entry.getKey().toString();
+ Object value = entry.getValue();
+
+ if (excludeNulls && value == null) {
+ continue;
+ }
+ if (hasExcludedFieldNames &&
excludedFieldNames.contains(key)) {
+ continue;
+ }
+ if (value != null &&
shouldExcludeType(value.getClass())) {
+ continue;
+ }
+
+ if (!firstItem) {
+ buffer.addChar(COMMA);
+ } else {
+ firstItem = false;
+ }
+
+ buffer.addJsonFieldName(key, disableUnicodeEscaping);
+ writeObject(key, value, buffer);
+ }
+ buffer.addChar(CLOSE_BRACE);
+ } else {
+ buffer.addChars(EMPTY_MAP_CHARS);
+ }
+ }
+
+ /**
+ * Serializes iterator and writes it into specified buffer.
+ */
+ private void writeIterator(Iterator<?> iterator, CharBuf buffer) {
+ if (iterator.hasNext()) {
+ buffer.addChar(OPEN_BRACKET);
+ boolean needComma = false;
+ while (iterator.hasNext()) {
+ Object it = iterator.next();
+ if (excludeNulls && it == null) {
+ continue;
+ }
+ if (it != null && shouldExcludeType(it.getClass())) {
+ continue;
+ }
+ if (needComma) buffer.addChar(COMMA);
+ writeObject(it, buffer);
+ needComma = true;
+ }
+ buffer.addChar(CLOSE_BRACKET);
+ } else {
+ buffer.addChars(EMPTY_LIST_CHARS);
+ }
+ }
+
+ /**
+ * 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.
+ */
+ private Converter findConverter(Class<?> type) {
+ if (!hasConverters) {
+ return null;
+ }
+ 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}
+ */
+ private boolean shouldExcludeType(Class<?> type) {
+ if (hasExcludedFieldTypes) {
+ for (Class<?> t : excludedFieldTypes) {
+ if (t.isAssignableFrom(type)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Package-private helper method used by StreamingJsonBuilder.
+ *
+ * @param name of the field
+ * @return true if that field is being excluded, else false
+ */
+ boolean isExcludingFieldsNamed(String name) {
+ return hasExcludedFieldNames &&
excludedFieldNames.contains(name);
+ }
+
+ /**
+ * Package-private helper method used by StreamingJsonBuilder.
+ *
+ * @param value an instance of an object
+ * @return true if values like this are being excluded, else false
+ */
+ boolean isExcludingValues(Object value) {
+ if (value == null) {
+ if (excludeNulls) {
+ return true;
+ }
+ } else {
+ if (shouldExcludeType(value.getClass())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ }
+
+ /**
+ * A converter that handles converting a given type to a JSON value
+ * using a closure.
+ */
+ private static class Converter {
+
+ private final Class<?> type;
+ private final Closure<? extends CharSequence> closure;
+ private final int paramCount;
+
+ static Converter of(Class<?> type, Closure<? extends CharSequence>
closure) {
+ return new Converter(type, closure);
+ }
+
+ private Converter(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
+ */
+ boolean handles(Class<?> type) {
--- End diff --
Does this method and the next two need to be package private?
---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at [email protected] or file a JIRA ticket
with INFRA.
---