Extract WritableJson from JsonWriter

Make `WritableJson` a top level class rather than a nested class inside
`WritableJson`.

Closes gh-42595
This commit is contained in:
Phillip Webb
2024-10-09 19:06:00 -07:00
parent b169439f86
commit aed4546c43
8 changed files with 329 additions and 281 deletions

View File

@@ -25,7 +25,6 @@ import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.springframework.boot.json.JsonWriter.WritableJson;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.function.ThrowingConsumer;

View File

@@ -16,14 +16,7 @@
package org.springframework.boot.json;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -37,7 +30,6 @@ import java.util.function.Supplier;
import org.springframework.boot.json.JsonValueWriter.Series;
import org.springframework.boot.json.JsonWriter.Member.Extractor;
import org.springframework.core.io.WritableResource;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -159,148 +151,6 @@ public interface JsonWriter<T> {
return (instance, out) -> initializedMembers.write(instance, new JsonValueWriter(out));
}
/**
* JSON content that can be written out.
*/
@FunctionalInterface
interface WritableJson {
/**
* Write the JSON to the provided {@link Appendable}.
* @param out the {@link Appendable} to receive the JSON
* @throws IOException on IO error
*/
void to(Appendable out) throws IOException;
/**
* Write the JSON to a {@link String}.
* @return the JSON string
*/
default String toJsonString() {
try {
StringBuilder stringBuilder = new StringBuilder();
to(stringBuilder);
return stringBuilder.toString();
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
/**
* Write the JSON to a UTF-8 encoded byte array.
* @return the JSON bytes
*/
default byte[] toByteArray() {
return toByteArray(StandardCharsets.UTF_8);
}
/**
* Write the JSON to a byte array.
* @param charset the charset
* @return the JSON bytes
*/
default byte[] toByteArray(Charset charset) {
Assert.notNull(charset, "'charset' must not be null");
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
toWriter(new OutputStreamWriter(out, charset));
return out.toByteArray();
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
/**
* Write the JSON to the provided {@link WritableResource} using
* {@link StandardCharsets#UTF_8 UTF8} encoding.
* @param out the {@link OutputStream} to receive the JSON
* @throws IOException on IO error
*/
default void toResource(WritableResource out) throws IOException {
Assert.notNull(out, "'out' must not be null");
try (OutputStream outputStream = out.getOutputStream()) {
toOutputStream(outputStream);
}
}
/**
* Write the JSON to the provided {@link WritableResource} using the given
* {@link Charset}.
* @param out the {@link OutputStream} to receive the JSON
* @param charset the charset to use
* @throws IOException on IO error
*/
default void toResource(WritableResource out, Charset charset) throws IOException {
Assert.notNull(out, "'out' must not be null");
Assert.notNull(charset, "'charset' must not be null");
try (OutputStream outputStream = out.getOutputStream()) {
toOutputStream(outputStream, charset);
}
}
/**
* Write the JSON to the provided {@link OutputStream} using
* {@link StandardCharsets#UTF_8 UTF8} encoding. The output stream will not be
* closed.
* @param out the {@link OutputStream} to receive the JSON
* @throws IOException on IO error
* @see #toOutputStream(OutputStream, Charset)
*/
default void toOutputStream(OutputStream out) throws IOException {
toOutputStream(out, StandardCharsets.UTF_8);
}
/**
* Write the JSON to the provided {@link OutputStream} using the given
* {@link Charset}. The output stream will not be closed.
* @param out the {@link OutputStream} to receive the JSON
* @param charset the charset to use
* @throws IOException on IO error
*/
default void toOutputStream(OutputStream out, Charset charset) throws IOException {
Assert.notNull(out, "'out' must not be null");
Assert.notNull(charset, "'charset' must not be null");
toWriter(new OutputStreamWriter(out, charset));
}
/**
* Write the JSON to the provided {@link Writer}. The writer will be flushed but
* not closed.
* @param out the {@link Writer} to receive the JSON
* @throws IOException on IO error
* @see #toOutputStream(OutputStream, Charset)
*/
default void toWriter(Writer out) throws IOException {
Assert.notNull(out, "'out' must not be null");
to(out);
out.flush();
}
/**
* Factory method used to create a {@link WritableJson} with a sensible
* {@link Object#toString()} that delegate to {@link WritableJson#toJsonString()}.
* @param writableJson the source {@link WritableJson}
* @return a new {@link WritableJson} with a sensible {@link Object#toString()}.
*/
static WritableJson of(WritableJson writableJson) {
return new WritableJson() {
@Override
public void to(Appendable out) throws IOException {
writableJson.to(out);
}
@Override
public String toString() {
return toJsonString();
}
};
}
}
/**
* Callback used to configure JSON members. Individual members can be declared using
* the various {@code add(...)} methods. Typically, members are declared with a

View File

@@ -0,0 +1,175 @@
/*
* 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.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.springframework.core.io.WritableResource;
import org.springframework.util.Assert;
/**
* JSON content that can be written out.
*
* @author Phillip Webb
* @author Moritz Halbritter
* @since 3.4.0
* @see JsonWriter
*/
@FunctionalInterface
public interface WritableJson {
/**
* Write the JSON to the provided {@link Appendable}.
* @param out the {@link Appendable} to receive the JSON
* @throws IOException on IO error
*/
void to(Appendable out) throws IOException;
/**
* Write the JSON to a {@link String}.
* @return the JSON string
*/
default String toJsonString() {
try {
StringBuilder stringBuilder = new StringBuilder();
to(stringBuilder);
return stringBuilder.toString();
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
/**
* Write the JSON to a UTF-8 encoded byte array.
* @return the JSON bytes
*/
default byte[] toByteArray() {
return toByteArray(StandardCharsets.UTF_8);
}
/**
* Write the JSON to a byte array.
* @param charset the charset
* @return the JSON bytes
*/
default byte[] toByteArray(Charset charset) {
Assert.notNull(charset, "'charset' must not be null");
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
toWriter(new OutputStreamWriter(out, charset));
return out.toByteArray();
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
/**
* Write the JSON to the provided {@link WritableResource} using
* {@link StandardCharsets#UTF_8 UTF8} encoding.
* @param out the {@link OutputStream} to receive the JSON
* @throws IOException on IO error
*/
default void toResource(WritableResource out) throws IOException {
Assert.notNull(out, "'out' must not be null");
try (OutputStream outputStream = out.getOutputStream()) {
toOutputStream(outputStream);
}
}
/**
* Write the JSON to the provided {@link WritableResource} using the given
* {@link Charset}.
* @param out the {@link OutputStream} to receive the JSON
* @param charset the charset to use
* @throws IOException on IO error
*/
default void toResource(WritableResource out, Charset charset) throws IOException {
Assert.notNull(out, "'out' must not be null");
Assert.notNull(charset, "'charset' must not be null");
try (OutputStream outputStream = out.getOutputStream()) {
toOutputStream(outputStream, charset);
}
}
/**
* Write the JSON to the provided {@link OutputStream} using
* {@link StandardCharsets#UTF_8 UTF8} encoding. The output stream will not be closed.
* @param out the {@link OutputStream} to receive the JSON
* @throws IOException on IO error
* @see #toOutputStream(OutputStream, Charset)
*/
default void toOutputStream(OutputStream out) throws IOException {
toOutputStream(out, StandardCharsets.UTF_8);
}
/**
* Write the JSON to the provided {@link OutputStream} using the given
* {@link Charset}. The output stream will not be closed.
* @param out the {@link OutputStream} to receive the JSON
* @param charset the charset to use
* @throws IOException on IO error
*/
default void toOutputStream(OutputStream out, Charset charset) throws IOException {
Assert.notNull(out, "'out' must not be null");
Assert.notNull(charset, "'charset' must not be null");
toWriter(new OutputStreamWriter(out, charset));
}
/**
* Write the JSON to the provided {@link Writer}. The writer will be flushed but not
* closed.
* @param out the {@link Writer} to receive the JSON
* @throws IOException on IO error
* @see #toOutputStream(OutputStream, Charset)
*/
default void toWriter(Writer out) throws IOException {
Assert.notNull(out, "'out' must not be null");
to(out);
out.flush();
}
/**
* Factory method used to create a {@link WritableJson} with a sensible
* {@link Object#toString()} that delegate to {@link WritableJson#toJsonString()}.
* @param writableJson the source {@link WritableJson}
* @return a new {@link WritableJson} with a sensible {@link Object#toString()}.
*/
static WritableJson of(WritableJson writableJson) {
return new WritableJson() {
@Override
public void to(Appendable out) throws IOException {
writableJson.to(out);
}
@Override
public String toString() {
return toJsonString();
}
};
}
}

View File

@@ -33,7 +33,7 @@ import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.json.JsonWriter.Members;
import org.springframework.boot.json.JsonWriter.WritableJson;
import org.springframework.boot.json.WritableJson;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatService;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;

View File

@@ -21,7 +21,7 @@ import java.io.IOException;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable;
import org.springframework.boot.json.JsonWriter.WritableJson;
import org.springframework.boot.json.WritableJson;
/**
* Helper used to adapt {@link Message} for structured writing.

View File

@@ -33,7 +33,7 @@ import org.slf4j.event.KeyValuePair;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.json.JsonWriter.Members;
import org.springframework.boot.json.JsonWriter.WritableJson;
import org.springframework.boot.json.WritableJson;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatService;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;

View File

@@ -16,12 +16,6 @@
package org.springframework.boot.json;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
@@ -31,15 +25,10 @@ import java.util.function.Function;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.json.JsonWriter.PairExtractor;
import org.springframework.boot.json.JsonWriter.WritableJson;
import org.springframework.core.io.FileSystemResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
@@ -52,9 +41,6 @@ class JsonWriterTests {
private static final Person PERSON = new Person("Spring", "Boot", 10);
@TempDir
File temp;
@Test
void writeToStringWritesToString() {
assertThat(ofFormatString("%s").writeToString(123)).isEqualTo("123");
@@ -468,119 +454,6 @@ class JsonWriterTests {
}
@Nested
class WritableJsonTests {
@Test
void toJsonStringReturnsString() {
WritableJson writable = (out) -> out.append("{}");
assertThat(writable.toJsonString()).isEqualTo("{}");
}
@Test
void toJsonStringWhenIOExceptionIsThrownThrowsUncheckedIOException() {
WritableJson writable = (out) -> {
throw new IOException("bad");
};
assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(() -> writable.toJsonString())
.havingCause()
.withMessage("bad");
}
@Test
void toByteArrayReturnsByteArray() {
WritableJson writable = (out) -> out.append("{}");
assertThat(writable.toByteArray()).isEqualTo("{}".getBytes());
}
@Test
void toResourceWritesJson() throws Exception {
File file = new File(JsonWriterTests.this.temp, "out.json");
WritableJson writable = (out) -> out.append("{}");
writable.toResource(new FileSystemResource(file));
assertThat(file).content().isEqualTo("{}");
}
@Test
void toResourceWithCharsetWritesJson() throws Exception {
File file = new File(JsonWriterTests.this.temp, "out.json");
WritableJson writable = (out) -> out.append("{}");
writable.toResource(new FileSystemResource(file), StandardCharsets.ISO_8859_1);
assertThat(file).content(StandardCharsets.ISO_8859_1).isEqualTo("{}");
}
@Test
void toResourceWithCharsetWhenOutIsNullThrowsException() {
WritableJson writable = (out) -> out.append("{}");
assertThatIllegalArgumentException().isThrownBy(() -> writable.toResource(null, StandardCharsets.UTF_8))
.withMessage("'out' must not be null");
}
@Test
void toResourceWithCharsetWhenCharsetIsNullThrowsException() {
File file = new File(JsonWriterTests.this.temp, "out.json");
WritableJson writable = (out) -> out.append("{}");
assertThatIllegalArgumentException()
.isThrownBy(() -> writable.toResource(new FileSystemResource(file), null))
.withMessage("'charset' must not be null");
}
@Test
void toOutputStreamWritesJson() throws Exception {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
WritableJson writable = (out) -> out.append("{}");
writable.toOutputStream(outputStream);
assertThat(outputStream.toString(StandardCharsets.UTF_8)).isEqualTo("{}");
}
@Test
void toOutputStreamWithCharsetWritesJson() throws Exception {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
WritableJson writable = (out) -> out.append("{}");
writable.toOutputStream(outputStream, StandardCharsets.ISO_8859_1);
assertThat(outputStream.toString(StandardCharsets.ISO_8859_1)).isEqualTo("{}");
}
@Test
void toOutputStreamWithCharsetWhenOutIsNullThrowsException() {
WritableJson writable = (out) -> out.append("{}");
assertThatIllegalArgumentException().isThrownBy(() -> writable.toOutputStream(null, StandardCharsets.UTF_8))
.withMessage("'out' must not be null");
}
@Test
void toOutputStreamWithCharsetWhenCharsetIsNullThrowsException() {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
WritableJson writable = (out) -> out.append("{}");
assertThatIllegalArgumentException().isThrownBy(() -> writable.toOutputStream(outputStream, null))
.withMessage("'charset' must not be null");
}
//
@Test
void toWriterWritesJson() throws Exception {
StringWriter writer = new StringWriter();
WritableJson writable = (out) -> out.append("{}");
writable.toWriter(writer);
assertThat(writer).hasToString("{}");
}
@Test
void toWriterWhenWriterIsNullThrowsException() {
WritableJson writable = (out) -> out.append("{}");
assertThatIllegalArgumentException().isThrownBy(() -> writable.toWriter(null))
.withMessage("'out' must not be null");
}
@Test
void ofReturnsInstanceWithSensibleToString() {
WritableJson writable = WritableJson.of((out) -> out.append("{}"));
assertThat(writable).hasToString("{}");
}
}
record Person(String firstName, String lastName, int age) {
@Override

View File

@@ -0,0 +1,151 @@
/*
* 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.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.core.io.FileSystemResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link WritableJson}.
*
* @author Phillip Webb
* @author Moritz Halbritter
*/
class WritableJsonTests {
@TempDir
File temp;
@Test
void toJsonStringReturnsString() {
WritableJson writable = (out) -> out.append("{}");
assertThat(writable.toJsonString()).isEqualTo("{}");
}
@Test
void toJsonStringWhenIOExceptionIsThrownThrowsUncheckedIOException() {
WritableJson writable = (out) -> {
throw new IOException("bad");
};
assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(() -> writable.toJsonString())
.havingCause()
.withMessage("bad");
}
@Test
void toByteArrayReturnsByteArray() {
WritableJson writable = (out) -> out.append("{}");
assertThat(writable.toByteArray()).isEqualTo("{}".getBytes());
}
@Test
void toResourceWritesJson() throws Exception {
File file = new File(this.temp, "out.json");
WritableJson writable = (out) -> out.append("{}");
writable.toResource(new FileSystemResource(file));
assertThat(file).content().isEqualTo("{}");
}
@Test
void toResourceWithCharsetWritesJson() throws Exception {
File file = new File(this.temp, "out.json");
WritableJson writable = (out) -> out.append("{}");
writable.toResource(new FileSystemResource(file), StandardCharsets.ISO_8859_1);
assertThat(file).content(StandardCharsets.ISO_8859_1).isEqualTo("{}");
}
@Test
void toResourceWithCharsetWhenOutIsNullThrowsException() {
WritableJson writable = (out) -> out.append("{}");
assertThatIllegalArgumentException().isThrownBy(() -> writable.toResource(null, StandardCharsets.UTF_8))
.withMessage("'out' must not be null");
}
@Test
void toResourceWithCharsetWhenCharsetIsNullThrowsException() {
File file = new File(this.temp, "out.json");
WritableJson writable = (out) -> out.append("{}");
assertThatIllegalArgumentException().isThrownBy(() -> writable.toResource(new FileSystemResource(file), null))
.withMessage("'charset' must not be null");
}
@Test
void toOutputStreamWritesJson() throws Exception {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
WritableJson writable = (out) -> out.append("{}");
writable.toOutputStream(outputStream);
assertThat(outputStream.toString(StandardCharsets.UTF_8)).isEqualTo("{}");
}
@Test
void toOutputStreamWithCharsetWritesJson() throws Exception {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
WritableJson writable = (out) -> out.append("{}");
writable.toOutputStream(outputStream, StandardCharsets.ISO_8859_1);
assertThat(outputStream.toString(StandardCharsets.ISO_8859_1)).isEqualTo("{}");
}
@Test
void toOutputStreamWithCharsetWhenOutIsNullThrowsException() {
WritableJson writable = (out) -> out.append("{}");
assertThatIllegalArgumentException().isThrownBy(() -> writable.toOutputStream(null, StandardCharsets.UTF_8))
.withMessage("'out' must not be null");
}
@Test
void toOutputStreamWithCharsetWhenCharsetIsNullThrowsException() {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
WritableJson writable = (out) -> out.append("{}");
assertThatIllegalArgumentException().isThrownBy(() -> writable.toOutputStream(outputStream, null))
.withMessage("'charset' must not be null");
}
@Test
void toWriterWritesJson() throws Exception {
StringWriter writer = new StringWriter();
WritableJson writable = (out) -> out.append("{}");
writable.toWriter(writer);
assertThat(writer).hasToString("{}");
}
@Test
void toWriterWhenWriterIsNullThrowsException() {
WritableJson writable = (out) -> out.append("{}");
assertThatIllegalArgumentException().isThrownBy(() -> writable.toWriter(null))
.withMessage("'out' must not be null");
}
@Test
void ofReturnsInstanceWithSensibleToString() {
WritableJson writable = WritableJson.of((out) -> out.append("{}"));
assertThat(writable).hasToString("{}");
}
}