Skip to content

Commit 1320259

Browse files
committed
JsonGenerator - JSON serialization options (closes groovy#371, closes groovy#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
1 parent 8213bd0 commit 1320259

17 files changed

+1729
-402
lines changed

subprojects/groovy-json/src/main/java/groovy/json/DefaultJsonGenerator.java

Lines changed: 549 additions & 0 deletions
Large diffs are not rendered by default.

subprojects/groovy-json/src/main/java/groovy/json/JsonBuilder.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,24 @@
6565
*/
6666
public class JsonBuilder extends GroovyObjectSupport implements Writable {
6767

68+
private final JsonGenerator generator;
6869
private Object content;
6970

7071
/**
7172
* Instantiates a JSON builder.
7273
*/
7374
public JsonBuilder() {
75+
this.generator = JsonOutput.DEFAULT_GENERATOR;
76+
}
77+
78+
/**
79+
* Instantiates a JSON builder with a configured generator.
80+
*
81+
* @param generator used to generate the output
82+
* @since 2.5
83+
*/
84+
public JsonBuilder(JsonGenerator generator) {
85+
this.generator = generator;
7486
}
7587

7688
/**
@@ -80,6 +92,20 @@ public JsonBuilder() {
8092
*/
8193
public JsonBuilder(Object content) {
8294
this.content = content;
95+
this.generator = JsonOutput.DEFAULT_GENERATOR;
96+
}
97+
98+
/**
99+
* Instantiates a JSON builder with some existing data structure
100+
* and a configured generator.
101+
*
102+
* @param content a pre-existing data structure
103+
* @param generator used to generate the output
104+
* @since 2.5
105+
*/
106+
public JsonBuilder(Object content, JsonGenerator generator) {
107+
this.content = content;
108+
this.generator = generator;
83109
}
84110

85111
public Object getContent() {
@@ -344,7 +370,7 @@ private Object setAndGetContent(String name, Object value) {
344370
* @return a JSON output
345371
*/
346372
public String toString() {
347-
return JsonOutput.toJson(content);
373+
return generator.toJson(content);
348374
}
349375

350376
/**
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package groovy.json;
20+
21+
import groovy.lang.Closure;
22+
import groovy.transform.stc.ClosureParams;
23+
import groovy.transform.stc.FromString;
24+
25+
import java.text.SimpleDateFormat;
26+
import java.util.Arrays;
27+
import java.util.HashSet;
28+
import java.util.LinkedHashSet;
29+
import java.util.Locale;
30+
import java.util.Set;
31+
import java.util.TimeZone;
32+
33+
/**
34+
* Generates JSON from objects.
35+
*
36+
* The {@link Options} builder can be used to configure an instance of a JsonGenerator.
37+
*
38+
* @see Options#build()
39+
* @since 2.5
40+
*/
41+
public interface JsonGenerator {
42+
43+
/**
44+
* Converts an object to its JSON representation.
45+
*
46+
* @param object to convert to JSON
47+
* @return JSON
48+
*/
49+
String toJson(Object object);
50+
51+
/**
52+
* Indicates whether this JsonGenerator is configured to exclude fields by
53+
* the given name.
54+
*
55+
* @param name of the field
56+
* @return true if that field is being excluded, else false
57+
*/
58+
boolean isExcludingFieldsNamed(String name);
59+
60+
/**
61+
* Indicates whether this JsonGenerator is configured to exclude values
62+
* of the given object (may be {@code null}).
63+
*
64+
* @param value an instance of an object
65+
* @return true if values like this are being excluded, else false
66+
*/
67+
boolean isExcludingValues(Object value);
68+
69+
/**
70+
* Handles converting a given type to a JSON value.
71+
*
72+
* @since 2.5
73+
*/
74+
interface Converter {
75+
76+
/**
77+
* Returns {@code true} if this converter can handle conversions
78+
* of the given type.
79+
*
80+
* @param type the type of the object to convert
81+
* @return {@code true} if this converter can successfully convert values of
82+
* the given type to a JSON value, else {@code false}
83+
*/
84+
boolean handles(Class<?> type);
85+
86+
/**
87+
* Converts a given object to a JSON value.
88+
*
89+
* @param value the object to convert
90+
* @return a JSON value representing the object
91+
*/
92+
CharSequence convert(Object value);
93+
94+
/**
95+
* Converts a given object to a JSON value.
96+
*
97+
* @param value the object to convert
98+
* @param key the key name for the value, may be {@code null}
99+
* @return a JSON value representing the object
100+
*/
101+
CharSequence convert(Object value, String key);
102+
103+
}
104+
105+
/**
106+
* A builder used to construct a {@link JsonGenerator} instance that allows
107+
* control over the serialized JSON output. If you do not need to customize the
108+
* output it is recommended to use the static {@code JsonOutput.toJson} methods.
109+
*
110+
* <p>
111+
* Example:
112+
* <pre><code class="groovyTestCase">
113+
* def generator = new groovy.json.JsonGenerator.Options()
114+
* .excludeNulls()
115+
* .dateFormat('yyyy')
116+
* .excludeFieldsByName('bar', 'baz')
117+
* .excludeFieldsByType(java.sql.Date)
118+
* .build()
119+
*
120+
* def input = [foo: null, lastUpdated: Date.parse('yyyy-MM-dd', '2014-10-24'),
121+
* bar: 'foo', baz: 'foo', systemDate: new java.sql.Date(new Date().getTime())]
122+
*
123+
* assert generator.toJson(input) == '{"lastUpdated":"2014"}'
124+
* </code></pre>
125+
*
126+
* @since 2.5
127+
*/
128+
class Options {
129+
130+
protected static final String JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
131+
protected static final Locale JSON_DATE_FORMAT_LOCALE = Locale.US;
132+
protected static final String DEFAULT_TIMEZONE = "GMT";
133+
134+
protected boolean excludeNulls;
135+
protected boolean disableUnicodeEscaping;
136+
protected String dateFormat = JSON_DATE_FORMAT;
137+
protected Locale dateLocale = JSON_DATE_FORMAT_LOCALE;
138+
protected TimeZone timezone = TimeZone.getTimeZone(DEFAULT_TIMEZONE);
139+
protected final Set<Converter> converters = new LinkedHashSet<Converter>();
140+
protected final Set<String> excludedFieldNames = new HashSet<String>();
141+
protected final Set<Class<?>> excludedFieldTypes = new HashSet<Class<?>>();
142+
143+
public Options() {}
144+
145+
/**
146+
* Do not serialize {@code null} values.
147+
*
148+
* @return a reference to this {@code Options} instance
149+
*/
150+
public Options excludeNulls() {
151+
excludeNulls = true;
152+
return this;
153+
}
154+
155+
/**
156+
* Disables the escaping of Unicode characters in JSON String values.
157+
*
158+
* @return a reference to this {@code Options} instance
159+
*/
160+
public Options disableUnicodeEscaping() {
161+
disableUnicodeEscaping = true;
162+
return this;
163+
}
164+
165+
/**
166+
* Sets the date format that will be used to serialize {@code Date} objects.
167+
* This must be a valid pattern for {@link java.text.SimpleDateFormat} and the
168+
* date formatter will be constructed with the default locale of {@link Locale#US}.
169+
*
170+
* @param format date format pattern used to serialize dates
171+
* @return a reference to this {@code Options} instance
172+
* @exception NullPointerException if the given pattern is null
173+
* @exception IllegalArgumentException if the given pattern is invalid
174+
*/
175+
public Options dateFormat(String format) {
176+
return dateFormat(format, JSON_DATE_FORMAT_LOCALE);
177+
}
178+
179+
/**
180+
* Sets the date format that will be used to serialize {@code Date} objects.
181+
* This must be a valid pattern for {@link java.text.SimpleDateFormat}.
182+
*
183+
* @param format date format pattern used to serialize dates
184+
* @param locale the locale whose date format symbols will be used
185+
* @return a reference to this {@code Options} instance
186+
* @exception IllegalArgumentException if the given pattern is invalid
187+
*/
188+
public Options dateFormat(String format, Locale locale) {
189+
// validate date format pattern
190+
new SimpleDateFormat(format, locale);
191+
dateFormat = format;
192+
dateLocale = locale;
193+
return this;
194+
}
195+
196+
/**
197+
* Sets the time zone that will be used to serialize dates.
198+
*
199+
* @param timezone used to serialize dates
200+
* @return a reference to this {@code Options} instance
201+
* @exception NullPointerException if the given timezone is null
202+
*/
203+
public Options timezone(String timezone) {
204+
this.timezone = TimeZone.getTimeZone(timezone);
205+
return this;
206+
}
207+
208+
/**
209+
* Registers a closure that will be called when the specified type or subtype
210+
* is serialized.
211+
*
212+
* <p>The closure must accept either 1 or 2 parameters. The first parameter
213+
* is required and will be instance of the {@code type} for which the closure
214+
* is registered. The second optional parameter should be of type {@code String}
215+
* and, if available, will be passed the name of the key associated with this
216+
* value if serializing a JSON Object. This parameter will be {@code null} when
217+
* serializing a JSON Array or when there is no way to determine the name of the key.
218+
*
219+
* <p>The return value from the closure must be a valid JSON value. The result
220+
* of the closure will be written to the internal buffer directly and no quoting,
221+
* escaping or other manipulation will be done to the resulting output.
222+
*
223+
* <p>
224+
* Example:
225+
* <pre><code class="groovyTestCase">
226+
* def generator = new groovy.json.JsonGenerator.Options()
227+
* .addConverter(URL) { URL u ->
228+
* "\"${u.getHost()}\""
229+
* }
230+
* .build()
231+
*
232+
* def input = [domain: new URL('http://groovy-lang.org/json.html#_parser_variants')]
233+
*
234+
* assert generator.toJson(input) == '{"domain":"groovy-lang.org"}'
235+
* </code></pre>
236+
*
237+
* <p>If two or more closures are registered for the exact same type the last
238+
* closure based on the order they were specified will be used. When serializing an
239+
* object its type is compared to the list of registered types in the order the were
240+
* given and the closure for the first suitable type will be called. Therefore, it is
241+
* important to register more specific types first.
242+
*
243+
* @param type the type to convert
244+
* @param closure called when the registered type or any type assignable to the given
245+
* type is encountered
246+
* @param <T> the type this converter is registered to handle
247+
* @return a reference to this {@code Options} instance
248+
* @exception NullPointerException if the given type or closure is null
249+
* @exception IllegalArgumentException if the given closure does not accept
250+
* a parameter of the given type
251+
*/
252+
public <T> Options addConverter(Class<T> type,
253+
@ClosureParams(value=FromString.class, options={"T","T,String"})
254+
Closure<? extends CharSequence> closure)
255+
{
256+
Converter converter = new DefaultJsonGenerator.ClosureConverter(type, closure);
257+
if (converters.contains(converter)) {
258+
converters.remove(converter);
259+
}
260+
converters.add(converter);
261+
return this;
262+
}
263+
264+
/**
265+
* Excludes from the output any fields that match the specified names.
266+
*
267+
* @param fieldNames name of the field to exclude from the output
268+
* @return a reference to this {@code Options} instance
269+
*/
270+
public Options excludeFieldsByName(CharSequence... fieldNames) {
271+
return excludeFieldsByName(Arrays.asList(fieldNames));
272+
}
273+
274+
/**
275+
* Excludes from the output any fields that match the specified names.
276+
*
277+
* @param fieldNames collection of names to exclude from the output
278+
* @return a reference to this {@code Options} instance
279+
*/
280+
public Options excludeFieldsByName(Iterable<? extends CharSequence> fieldNames) {
281+
for (CharSequence cs : fieldNames) {
282+
if (cs != null) {
283+
excludedFieldNames.add(cs.toString());
284+
}
285+
}
286+
return this;
287+
}
288+
289+
/**
290+
* Excludes from the output any fields whose type is the same or is
291+
* assignable to any of the given types.
292+
*
293+
* @param types excluded from the output
294+
* @return a reference to this {@code Options} instance
295+
*/
296+
public Options excludeFieldsByType(Class<?>... types) {
297+
return excludeFieldsByType(Arrays.asList(types));
298+
}
299+
300+
/**
301+
* Excludes from the output any fields whose type is the same or is
302+
* assignable to any of the given types.
303+
*
304+
* @param types collection of types to exclude from the output
305+
* @return a reference to this {@code Options} instance
306+
*/
307+
public Options excludeFieldsByType(Iterable<Class<?>> types) {
308+
for (Class<?> c : types) {
309+
if (c != null) {
310+
excludedFieldTypes.add(c);
311+
}
312+
}
313+
return this;
314+
}
315+
316+
/**
317+
* Creates a {@link JsonGenerator} that is based on the current options.
318+
*
319+
* @return a fully configured {@link JsonGenerator}
320+
*/
321+
public JsonGenerator build() {
322+
return new DefaultJsonGenerator(this);
323+
}
324+
}
325+
326+
}

0 commit comments

Comments
 (0)