
org.springframework.boot.json.JsonValueWriter Maven / Gradle / Ivy
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed 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
*
* https://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 org.springframework.boot.json;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.springframework.boot.json.JsonWriter.MemberPath;
import org.springframework.boot.json.JsonWriter.NameProcessor;
import org.springframework.boot.json.JsonWriter.ValueProcessor;
import org.springframework.boot.util.LambdaSafe;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.function.ThrowingConsumer;
/**
* Internal class used by {@link JsonWriter} to handle the lower-level concerns of writing
* JSON.
*
* @author Phillip Webb
* @author Moritz Halbritter
*/
class JsonValueWriter {
private final Appendable out;
private MemberPath path = MemberPath.ROOT;
private final Deque filtersAndProcessors = new ArrayDeque<>();
private final Deque activeSeries = new ArrayDeque<>();
/**
* Create a new {@link JsonValueWriter} instance.
* @param out the {@link Appendable} used to receive the JSON output
*/
JsonValueWriter(Appendable out) {
this.out = out;
}
void pushProcessors(JsonWriterFiltersAndProcessors jsonProcessors) {
this.filtersAndProcessors.addLast(jsonProcessors);
}
void popProcessors() {
this.filtersAndProcessors.removeLast();
}
/**
* Write a name value pair, or just a value if {@code name} is {@code null}.
* @param the name type in the pair
* @param the value type in the pair
* @param name the name of the pair or {@code null} if only the value should be
* written
* @param value the value
*/
void write(N name, V value) {
if (name != null) {
writePair(name, value);
}
else {
write(value);
}
}
/**
* Write a value to the JSON output. The following value types are supported:
*
* - Any {@code null} value
* - A {@link WritableJson} instance
* - Any {@link Iterable} or Array (written as a JSON array)
* - A {@link Map} (written as a JSON object)
* - Any {@link Number}
* - A {@link Boolean}
*
* All other values are written as JSON strings.
* @param the value type
* @param value the value to write
*/
void write(V value) {
value = processValue(value);
if (value == null) {
append("null");
}
else if (value instanceof WritableJson writableJson) {
try {
writableJson.to(this.out);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
else if (value instanceof Iterable> iterable) {
writeArray(iterable::forEach);
}
else if (ObjectUtils.isArray(value)) {
writeArray(Arrays.asList(ObjectUtils.toObjectArray(value))::forEach);
}
else if (value instanceof Map, ?> map) {
writeObject(map::forEach);
}
else if (value instanceof Number || value instanceof Boolean) {
append(value.toString());
}
else {
writeString(value);
}
}
/**
* Start a new {@link Series} (JSON object or array).
* @param series the series to start
* @see #end(Series)
* @see #writePairs(Consumer)
* @see #writeElements(Consumer)
*/
void start(Series series) {
if (series != null) {
this.activeSeries.push(new ActiveSeries(series));
append(series.openChar);
}
}
/**
* End an active {@link Series} (JSON object or array).
* @param series the series type being ended (must match {@link #start(Series)})
* @see #start(Series)
*/
void end(Series series) {
if (series != null) {
this.activeSeries.pop();
append(series.closeChar);
}
}
/**
* Write the specified elements to a newly started {@link Series#ARRAY array series}.
* @param the element type
* @param elements a callback that will be used to provide each element. Typically a
* {@code forEach} method reference.
* @see #writeElements(Consumer)
*/
void writeArray(Consumer> elements) {
start(Series.ARRAY);
elements.accept(ThrowingConsumer.of(this::writeElement));
end(Series.ARRAY);
}
/**
* Write the specified elements to an already started {@link Series#ARRAY array
* series}.
* @param the element type
* @param elements a callback that will be used to provide each element. Typically a
* {@code forEach} method reference.
* @see #writeElements(Consumer)
*/
void writeElements(Consumer> elements) {
elements.accept(ThrowingConsumer.of(this::writeElement));
}
void writeElement(E element) {
ActiveSeries activeSeries = this.activeSeries.peek();
Assert.notNull(activeSeries, "No series has been started");
this.path = activeSeries.updatePath(this.path);
activeSeries.incrementIndexAndAddCommaIfRequired();
write(element);
this.path = activeSeries.restorePath(this.path);
}
/**
* Write the specified pairs to a newly started {@link Series#OBJECT object series}.
* @param the name type in the pair
* @param the value type in the pair
* @param pairs a callback that will be used to provide each pair. Typically a
* {@code forEach} method reference.
* @see #writePairs(Consumer)
*/
void writeObject(Consumer> pairs) {
start(Series.OBJECT);
pairs.accept(this::writePair);
end(Series.OBJECT);
}
/**
* Write the specified pairs to an already started {@link Series#OBJECT object
* series}.
* @param the name type in the pair
* @param the value type in the pair
* @param pairs a callback that will be used to provide each pair. Typically a
* {@code forEach} method reference.
* @see #writePairs(Consumer)
*/
void writePairs(Consumer> pairs) {
pairs.accept(this::writePair);
}
private void writePair(N name, V value) {
this.path = this.path.child(name.toString());
if (!isFilteredPath()) {
String processedName = processName(name.toString());
ActiveSeries activeSeries = this.activeSeries.peek();
Assert.notNull(activeSeries, "No series has been started");
activeSeries.incrementIndexAndAddCommaIfRequired();
Assert.state(activeSeries.addName(processedName),
() -> "The name '" + processedName + "' has already been written");
writeString(processedName);
append(":");
write(value);
}
this.path = this.path.parent();
}
private void writeString(Object value) {
try {
this.out.append('"');
String string = value.toString();
for (int i = 0; i < string.length(); i++) {
char ch = string.charAt(i);
switch (ch) {
case '"' -> this.out.append("\\\"");
case '\\' -> this.out.append("\\\\");
case '/' -> this.out.append("\\/");
case '\b' -> this.out.append("\\b");
case '\f' -> this.out.append("\\f");
case '\n' -> this.out.append("\\n");
case '\r' -> this.out.append("\\r");
case '\t' -> this.out.append("\\t");
default -> {
if (Character.isISOControl(ch)) {
this.out.append("\\u");
this.out.append(String.format("%04X", (int) ch));
}
else {
this.out.append(ch);
}
}
}
}
this.out.append('"');
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private void append(String value) {
try {
this.out.append(value);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private void append(char ch) {
try {
this.out.append(ch);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private boolean isFilteredPath() {
for (JsonWriterFiltersAndProcessors filtersAndProcessors : this.filtersAndProcessors) {
for (Predicate pathFilter : filtersAndProcessors.pathFilters()) {
if (pathFilter.test(this.path)) {
return true;
}
}
}
return false;
}
private String processName(String name) {
for (JsonWriterFiltersAndProcessors filtersAndProcessors : this.filtersAndProcessors) {
for (NameProcessor nameProcessor : filtersAndProcessors.nameProcessors()) {
name = processName(name, nameProcessor);
}
}
return name;
}
private String processName(String name, NameProcessor nameProcessor) {
name = nameProcessor.processName(this.path, name);
Assert.state(StringUtils.hasLength(name), "NameProcessor " + nameProcessor + " returned an empty result");
return name;
}
private V processValue(V value) {
for (JsonWriterFiltersAndProcessors filtersAndProcessors : this.filtersAndProcessors) {
for (ValueProcessor> valueProcessor : filtersAndProcessors.valueProcessors()) {
value = processValue(value, valueProcessor);
}
}
return value;
}
@SuppressWarnings({ "unchecked", "unchecked" })
private V processValue(V value, ValueProcessor> valueProcessor) {
return (V) LambdaSafe.callback(ValueProcessor.class, valueProcessor, this.path, value)
.invokeAnd((call) -> call.processValue(this.path, value))
.get(value);
}
/**
* A series of items that can be written to the JSON output.
*/
enum Series {
/**
* A JSON object series consisting of name/value pairs.
*/
OBJECT('{', '}'),
/**
* A JSON array series consisting of elements.
*/
ARRAY('[', ']');
final char openChar;
final char closeChar;
Series(char openChar, char closeChar) {
this.openChar = openChar;
this.closeChar = closeChar;
}
}
/**
* Details of the currently active {@link Series}.
*/
private final class ActiveSeries {
private final Series series;
private int index;
private Set names = new HashSet<>();
private ActiveSeries(Series series) {
this.series = series;
}
boolean addName(String processedName) {
return this.names.add(processedName);
}
MemberPath updatePath(MemberPath path) {
return (this.series != Series.ARRAY) ? path : path.child(this.index);
}
MemberPath restorePath(MemberPath path) {
return (this.series != Series.ARRAY) ? path : path.parent();
}
void incrementIndexAndAddCommaIfRequired() {
if (this.index > 0) {
append(',');
}
this.index++;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy