Skip to content

Commit

Permalink
JsonGenerator - JSON serialization options (closes groovy#371, closes g…
Browse files Browse the repository at this point in the history
…roovy#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
  • Loading branch information
jwagenleitner committed Oct 23, 2016
1 parent 8213bd0 commit 1320259
Show file tree
Hide file tree
Showing 17 changed files with 1,729 additions and 402 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,24 @@
*/
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;
}

/**
Expand All @@ -80,6 +92,20 @@ public JsonBuilder() {
*/
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() {
Expand Down Expand Up @@ -344,7 +370,7 @@ private Object setAndGetContent(String name, Object value) {
* @return a JSON output
*/
public String toString() {
return JsonOutput.toJson(content);
return generator.toJson(content);
}

/**
Expand Down
326 changes: 326 additions & 0 deletions subprojects/groovy-json/src/main/java/groovy/json/JsonGenerator.java
Original file line number Diff line number Diff line change
@@ -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);
}
}

}
Loading

0 comments on commit 1320259

Please sign in to comment.