Skip to content

Commit 20c2af1

Browse files
philwebbmhalbritter
andcommitted
Add JsonWriter utility interface
Add `JsonWriter` utility interface that can be used to write JSON without the need for a third-party library. Closes gh-41489 Co-authored-by: Moritz Halbritter <[email protected]>
1 parent bb8241f commit 20c2af1

File tree

4 files changed

+2048
-0
lines changed

4 files changed

+2048
-0
lines changed
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.json;
18+
19+
import java.io.IOException;
20+
import java.io.UncheckedIOException;
21+
import java.util.ArrayDeque;
22+
import java.util.Deque;
23+
import java.util.Map;
24+
import java.util.function.BiConsumer;
25+
import java.util.function.Consumer;
26+
27+
import org.assertj.core.util.Arrays;
28+
29+
import org.springframework.boot.json.JsonWriter.WritableJson;
30+
import org.springframework.util.ObjectUtils;
31+
import org.springframework.util.function.ThrowingConsumer;
32+
33+
/**
34+
* Internal class used by {@link JsonWriter} to handle the lower-level concerns of writing
35+
* JSON.
36+
*
37+
* @author Phillip Webb
38+
* @author Moritz Halbritter
39+
*/
40+
class JsonValueWriter {
41+
42+
private final Appendable out;
43+
44+
private final Deque<ActiveSeries> activeSeries = new ArrayDeque<>();
45+
46+
/**
47+
* Create a new {@link JsonValueWriter} instance.
48+
* @param out the {@link Appendable} used to receive the JSON output
49+
*/
50+
JsonValueWriter(Appendable out) {
51+
this.out = out;
52+
}
53+
54+
/**
55+
* Write a name value pair, or just a value if {@code name} is {@code null}.
56+
* @param <N> the name type in the pair
57+
* @param <V> the value type in the pair
58+
* @param name the name of the pair or {@code null} if only the value should be
59+
* written
60+
* @param value the value
61+
* @on IO error
62+
*/
63+
<N, V> void write(N name, V value) {
64+
if (name != null) {
65+
writePair(name, value);
66+
}
67+
else {
68+
write(value);
69+
}
70+
}
71+
72+
/**
73+
* Write a value to the JSON output. The following value types are supported:
74+
* <ul>
75+
* <li>Any {@code null} value</li>
76+
* <li>A {@link WritableJson} instance</li>
77+
* <li>Any {@link Iterable} or Array (written as a JSON array)</li>
78+
* <li>A {@link Map} (written as a JSON object)</li>
79+
* <li>Any {@link Number}</li>
80+
* <li>A {@link Boolean}</li>
81+
* </ul>
82+
* All other values are written as JSON strings.
83+
* @param <V> the value type
84+
* @param value the value to write
85+
* @on IO error
86+
*/
87+
<V> void write(V value) {
88+
if (value == null) {
89+
append("null");
90+
}
91+
else if (value instanceof WritableJson writableJson) {
92+
try {
93+
writableJson.to(this.out);
94+
}
95+
catch (IOException ex) {
96+
throw new UncheckedIOException(ex);
97+
}
98+
}
99+
else if (value instanceof Iterable<?> iterable) {
100+
writeArray(iterable::forEach);
101+
}
102+
else if (ObjectUtils.isArray(value)) {
103+
writeArray(Arrays.asList(ObjectUtils.toObjectArray(value))::forEach);
104+
}
105+
else if (value instanceof Map<?, ?> map) {
106+
writeObject(map::forEach);
107+
}
108+
else if (value instanceof Number) {
109+
append(value.toString());
110+
}
111+
else if (value instanceof Boolean) {
112+
append(Boolean.TRUE.equals(value) ? "true" : "false");
113+
}
114+
else {
115+
writeString(value);
116+
}
117+
}
118+
119+
/**
120+
* Start a new {@link Series} (JSON object or array).
121+
* @param series the series to start
122+
* @on IO error
123+
* @see #end(Series)
124+
* @see #writePairs(Consumer)
125+
* @see #writeElements(Consumer)
126+
*/
127+
void start(Series series) {
128+
if (series != null) {
129+
this.activeSeries.push(new ActiveSeries());
130+
append(series.openChar);
131+
}
132+
}
133+
134+
/**
135+
* End an active {@link Series} (JSON object or array).
136+
* @param series the series type being ended (must match {@link #start(Series)})
137+
* @on IO error
138+
* @see #start(Series)
139+
*/
140+
void end(Series series) {
141+
if (series != null) {
142+
this.activeSeries.pop();
143+
append(series.closeChar);
144+
}
145+
}
146+
147+
/**
148+
* Write the specified elements to a newly started {@link Series#ARRAY array series}.
149+
* @param <E> the element type
150+
* @param elements a callback that will be used to provide each element. Typically a
151+
* {@code forEach} method reference.
152+
* @on IO error
153+
* @see #writeElements(Consumer)
154+
*/
155+
<E> void writeArray(Consumer<Consumer<E>> elements) {
156+
start(Series.ARRAY);
157+
elements.accept(ThrowingConsumer.of(this::writeElement));
158+
end(Series.ARRAY);
159+
}
160+
161+
/**
162+
* Write the specified elements to an already started {@link Series#ARRAY array
163+
* series}.
164+
* @param <E> the element type
165+
* @param elements a callback that will be used to provide each element. Typically a
166+
* {@code forEach} method reference.
167+
* @see #writeElements(Consumer)
168+
*/
169+
<E> void writeElements(Consumer<Consumer<E>> elements) {
170+
elements.accept(ThrowingConsumer.of(this::writeElement));
171+
}
172+
173+
<E> void writeElement(E element) {
174+
ActiveSeries activeSeries = this.activeSeries.peek();
175+
activeSeries.appendCommaIfRequired();
176+
write(element);
177+
}
178+
179+
/**
180+
* Write the specified pairs to a newly started {@link Series#OBJECT object series}.
181+
* @param <N> the name type in the pair
182+
* @param <V> the value type in the pair
183+
* @param pairs a callback that will be used to provide each pair. Typically a
184+
* {@code forEach} method reference.
185+
* @on IO error
186+
* @see #writePairs(Consumer)
187+
*/
188+
<N, V> void writeObject(Consumer<BiConsumer<N, V>> pairs) {
189+
start(Series.OBJECT);
190+
pairs.accept(this::writePair);
191+
end(Series.OBJECT);
192+
}
193+
194+
/**
195+
* Write the specified pairs to an already started {@link Series#OBJECT object
196+
* series}.
197+
* @param <N> the name type in the pair
198+
* @param <V> the value type in the pair
199+
* @param pairs a callback that will be used to provide each pair. Typically a
200+
* {@code forEach} method reference.
201+
* @see #writePairs(Consumer)
202+
*/
203+
<N, V> void writePairs(Consumer<BiConsumer<N, V>> pairs) {
204+
pairs.accept(this::writePair);
205+
}
206+
207+
private <N, V> void writePair(N name, V value) {
208+
ActiveSeries activeSeries = this.activeSeries.peek();
209+
activeSeries.appendCommaIfRequired();
210+
writeString(name);
211+
append(":");
212+
write(value);
213+
}
214+
215+
private void writeString(Object value) {
216+
try {
217+
this.out.append('"');
218+
String string = value.toString();
219+
for (int i = 0; i < string.length(); i++) {
220+
char ch = string.charAt(i);
221+
switch (ch) {
222+
case '"' -> this.out.append("\\\"");
223+
case '\\' -> this.out.append("\\\\");
224+
case '/' -> this.out.append("\\/");
225+
case '\b' -> this.out.append("\\b");
226+
case '\f' -> this.out.append("\\f");
227+
case '\n' -> this.out.append("\\n");
228+
case '\r' -> this.out.append("\\r");
229+
case '\t' -> this.out.append("\\t");
230+
default -> {
231+
if (Character.isISOControl(ch)) {
232+
this.out.append("\\u");
233+
this.out.append(String.format("%04X", (int) ch));
234+
}
235+
else {
236+
this.out.append(ch);
237+
}
238+
}
239+
}
240+
}
241+
this.out.append('"');
242+
}
243+
catch (IOException ex) {
244+
throw new UncheckedIOException(ex);
245+
}
246+
}
247+
248+
private void append(String value) {
249+
try {
250+
this.out.append(value);
251+
}
252+
catch (IOException ex) {
253+
throw new UncheckedIOException(ex);
254+
}
255+
256+
}
257+
258+
private void append(char ch) {
259+
try {
260+
this.out.append(ch);
261+
}
262+
catch (IOException ex) {
263+
throw new UncheckedIOException(ex);
264+
}
265+
}
266+
267+
/**
268+
* A series of items that can be written to the JSON output.
269+
*/
270+
enum Series {
271+
272+
/**
273+
* A JSON object series consisting of name/value pairs.
274+
*/
275+
OBJECT('{', '}'),
276+
277+
/**
278+
* A JSON array series consisting of elements.
279+
*/
280+
ARRAY('[', ']');
281+
282+
final char openChar;
283+
284+
final char closeChar;
285+
286+
Series(char openChar, char closeChar) {
287+
this.openChar = openChar;
288+
this.closeChar = closeChar;
289+
}
290+
291+
}
292+
293+
/**
294+
* Details of the currently active {@link Series}.
295+
*/
296+
private final class ActiveSeries {
297+
298+
private boolean commaRequired;
299+
300+
private ActiveSeries() {
301+
}
302+
303+
void appendCommaIfRequired() {
304+
if (this.commaRequired) {
305+
append(',');
306+
}
307+
this.commaRequired = true;
308+
}
309+
310+
}
311+
312+
}

0 commit comments

Comments
 (0)