From 712af2a0ab28332900b745e7c660e15be919cfa4 Mon Sep 17 00:00:00 2001 From: Karen Asmarian Date: Fri, 10 Jan 2025 10:47:18 +0100 Subject: [PATCH 1/6] Allow using precise floats in logs --- README.md | 11 ++++++++++ .../json/CompactingJsonBodyFilter.java | 4 ++++ .../json/JacksonJsonFieldBodyFilter.java | 17 ++++++++++++-- .../logbook/json/ParsingJsonCompactor.java | 19 ++++++++++++++-- .../json/PrettyPrintingJsonBodyFilter.java | 15 +++++++++++-- .../json/CompactingJsonBodyFilterTest.java | 13 +++++++++-- .../json/JacksonJsonFieldBodyFilterTest.java | 10 +++++++++ .../json/JsonHttpLogFormatterTest.java | 5 +++-- .../PrettyPrintingJsonBodyFilterTest.java | 22 +++++++++++++++++-- logbook-json/src/test/resources/student.json | 5 +++-- 10 files changed, 107 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 3b8142d94..e858ab9ea 100644 --- a/README.md +++ b/README.md @@ -534,6 +534,17 @@ a JSON response body will **not** be escaped and represented as a string: } ``` + +> [!NOTE] +> Logbook is using [BodyFilters](#Filtering) to inline json payload or to find fields for obfuscation. +> Filters for JSON bodies are using Jackson, which comes with a defect of dropping off precision from floating point +> numbers (see [FasterXML/jackson-core/issues/984](https://github.com/FasterXML/jackson-core/issues/984)). +> +> This can be changed by setting the `usePreciseFloats` flag to true in the filter respective filters. +> +> E.g. `new CompactingJsonBodyFilter(true)` will keep the precision of floating point numbers. + + ##### Common Log Format The Common Log Format ([CLF](https://httpd.apache.org/docs/trunk/logs.html#common)) is a standardized text file format used by web servers when generating server log files. The format is supported via diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/CompactingJsonBodyFilter.java b/logbook-json/src/main/java/org/zalando/logbook/json/CompactingJsonBodyFilter.java index a437877ae..d103a332a 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/CompactingJsonBodyFilter.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/CompactingJsonBodyFilter.java @@ -21,6 +21,10 @@ public final class CompactingJsonBodyFilter implements BodyFilter { private final JsonCompactor compactor; + public CompactingJsonBodyFilter(final boolean usePreciseFloats) { + this(new ParsingJsonCompactor(usePreciseFloats)); + } + public CompactingJsonBodyFilter() { this(new ParsingJsonCompactor()); } diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java b/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java index 1dc10c3a7..1af46ae00 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java @@ -29,11 +29,20 @@ public class JacksonJsonFieldBodyFilter implements BodyFilter { private final String replacement; private final Set fields; private final JsonFactory factory; + private final boolean usePreciseFloats; - public JacksonJsonFieldBodyFilter(final Collection fieldNames, final String replacement, final JsonFactory factory) { + public JacksonJsonFieldBodyFilter(final Collection fieldNames, + final String replacement, + final JsonFactory factory, + final boolean usePreciseFloats) { this.fields = new HashSet<>(fieldNames); // thread safe for reading this.replacement = replacement; this.factory = factory; + this.usePreciseFloats = usePreciseFloats; + } + + public JacksonJsonFieldBodyFilter(final Collection fieldNames, final String replacement, final JsonFactory factory) { + this(fieldNames, replacement, factory, false); } public JacksonJsonFieldBodyFilter(final Collection fieldNames, final String replacement) { @@ -54,7 +63,11 @@ public String filter(final String body) { JsonToken nextToken; while ((nextToken = parser.nextToken()) != null) { - generator.copyCurrentEvent(parser); + if (usePreciseFloats) { + generator.copyCurrentEventExact(parser); + } else { + generator.copyCurrentEvent(parser); + } if (nextToken == JsonToken.FIELD_NAME && fields.contains(parser.currentName())) { nextToken = parser.nextToken(); generator.writeString(replacement); diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java b/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java index 119bc4f11..d5b1ccd1d 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java @@ -11,12 +11,23 @@ final class ParsingJsonCompactor implements JsonCompactor { private final JsonFactory factory; + private final boolean usePreciseFloats; + + public ParsingJsonCompactor(final JsonFactory factory, final boolean usePreciseFloats) { + this.factory = factory; + this.usePreciseFloats = usePreciseFloats; + } + + public ParsingJsonCompactor(final boolean usePreciseFloats) { + this(new JsonFactory(), usePreciseFloats); + } + public ParsingJsonCompactor() { this(new JsonFactory()); } public ParsingJsonCompactor(final JsonFactory factory) { - this.factory = factory; + this(factory, false); } @Override @@ -27,7 +38,11 @@ public String compact(final String json) throws IOException { final JsonGenerator generator = factory.createGenerator(output)) { while (parser.nextToken() != null) { - generator.copyCurrentEvent(parser); + if (usePreciseFloats) { + generator.copyCurrentEventExact(parser); + } else { + generator.copyCurrentEvent(parser); + } } generator.flush(); diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java b/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java index 059a620a2..f02168059 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java @@ -20,9 +20,16 @@ public final class PrettyPrintingJsonBodyFilter implements BodyFilter { private final JsonFactory factory; + private final boolean usePreciseFloats; - public PrettyPrintingJsonBodyFilter(final JsonFactory factory) { + public PrettyPrintingJsonBodyFilter(final JsonFactory factory, + final boolean usePreciseFloats) { this.factory = factory; + this.usePreciseFloats = usePreciseFloats; + } + + public PrettyPrintingJsonBodyFilter(final JsonFactory factory) { + this(factory, false); } public PrettyPrintingJsonBodyFilter() { @@ -52,7 +59,11 @@ public String filter(@Nullable final String contentType, final String body) { generator.useDefaultPrettyPrinter(); while (parser.nextToken() != null) { - generator.copyCurrentEvent(parser); + if (usePreciseFloats) { + generator.copyCurrentEventExact(parser); + } else { + generator.copyCurrentEvent(parser); + } } generator.flush(); diff --git a/logbook-json/src/test/java/org/zalando/logbook/json/CompactingJsonBodyFilterTest.java b/logbook-json/src/test/java/org/zalando/logbook/json/CompactingJsonBodyFilterTest.java index 3657d7bac..859f9efaa 100644 --- a/logbook-json/src/test/java/org/zalando/logbook/json/CompactingJsonBodyFilterTest.java +++ b/logbook-json/src/test/java/org/zalando/logbook/json/CompactingJsonBodyFilterTest.java @@ -12,12 +12,14 @@ class CompactingJsonBodyFilterTest { /*language=JSON*/ private final String pretty = "{\n" + " \"root\": {\n" + - " \"child\": \"text\"\n" + + " \"child\": \"text\",\n" + + " \"float_child\" : 0.40000000000000002" + " }\n" + "}"; /*language=JSON*/ - private final String compacted = "{\"root\":{\"child\":\"text\"}}"; + private final String compacted = "{\"root\":{\"child\":\"text\",\"float_child\":0.4}}"; + private final String compactedWithPreciseFloat = "{\"root\":{\"child\":\"text\",\"float_child\":0.40000000000000002}}"; @Test void shouldIgnoreEmptyBody() { @@ -50,6 +52,13 @@ void shouldTransformValidJsonRequestWithCompatibleContentType() { assertThat(filtered).isEqualTo(compacted); } + @Test + void shouldPreserveBigFloatOnCopy() { + final String filtered = new CompactingJsonBodyFilter(true) + .filter("application/custom+json", pretty); + assertThat(filtered).isEqualTo(compactedWithPreciseFloat); + } + @Test void shouldSkipInvalidJsonLookingLikeAValidOne() { final String invalidJson = "{invalid}"; diff --git a/logbook-json/src/test/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilterTest.java b/logbook-json/src/test/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilterTest.java index 22e9743d4..08836d6bb 100644 --- a/logbook-json/src/test/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilterTest.java +++ b/logbook-json/src/test/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilterTest.java @@ -1,5 +1,6 @@ package org.zalando.logbook.json; +import com.fasterxml.jackson.core.JsonFactory; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -7,6 +8,7 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -75,6 +77,14 @@ public void doesNotFilterNonJson() throws Exception { assertThat(filtered).contains("Ford"); } + @Test + public void shouldPreserveBigFloatOnCopy() throws Exception { + final String string = getResource("/student.json").trim(); + final JacksonJsonFieldBodyFilter filter = new JacksonJsonFieldBodyFilter(Collections.emptyList(), "XXX", new JsonFactory(), true); + final String filtered = filter.filter("application/json", string); + assertThat(filtered).contains("\"debt\":123450.40000000000000002"); + } + private String getResource(final String path) throws IOException { final byte[] bytes = Files.readAllBytes(Paths.get("src/test/resources/" + path)); return new String(bytes, UTF_8); diff --git a/logbook-json/src/test/java/org/zalando/logbook/json/JsonHttpLogFormatterTest.java b/logbook-json/src/test/java/org/zalando/logbook/json/JsonHttpLogFormatterTest.java index a74248921..65863045c 100644 --- a/logbook-json/src/test/java/org/zalando/logbook/json/JsonHttpLogFormatterTest.java +++ b/logbook-json/src/test/java/org/zalando/logbook/json/JsonHttpLogFormatterTest.java @@ -204,12 +204,13 @@ void shouldNotEmbedReplacedJsonRequestBody(final HttpLogFormatter unit) throws I void shouldEmbedCustomJsonRequestBody(final HttpLogFormatter unit) throws IOException { final HttpRequest request = MockHttpRequest.create() .withContentType("application/custom+json") - .withBodyAsString("{\"name\":\"Bob\"}"); + .withBodyAsString("{\"name\":\"Bob\", \"float_value\": 0.40000000000000002 }"); final String json = unit.format(new SimplePrecorrelation("", systemUTC()), request); with(json) - .assertEquals("$.body.name", "Bob"); + .assertEquals("$.body.name", "Bob") + .assertEquals("$.body.float_value", 0.40000000000000002); } @ParameterizedTest diff --git a/logbook-json/src/test/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilterTest.java b/logbook-json/src/test/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilterTest.java index cc23bfa2a..900d7e00f 100644 --- a/logbook-json/src/test/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilterTest.java +++ b/logbook-json/src/test/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilterTest.java @@ -1,5 +1,6 @@ package org.zalando.logbook.json; +import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.zalando.logbook.BodyFilter; @@ -16,13 +17,23 @@ class PrettyPrintingJsonBodyFilterTest { private final String pretty = Stream.of( "{", " \"root\" : {", - " \"child\" : \"text\"", + " \"child\" : \"text\",", + " \"float_child\" : 0.4", + " }", + "}" + ).collect(Collectors.joining(System.lineSeparator())); + + private final String compactedWithPreciseFloat = Stream.of( + "{", + " \"root\" : {", + " \"child\" : \"text\",", + " \"float_child\" : 0.40000000000000002", " }", "}" ).collect(Collectors.joining(System.lineSeparator())); /*language=JSON*/ - private final String compacted = "{\"root\":{\"child\":\"text\"}}"; + private final String compacted = "{\"root\":{\"child\":\"text\", \"float_child\": 0.40000000000000002 }}"; @Test void shouldIgnoreEmptyBody() { @@ -68,4 +79,11 @@ void shouldConstructFromObjectMapper() { assertThat(filtered).isEqualTo(pretty); } + @Test + void shouldPreserveBigFloatOnCopy() { + final String filtered = new PrettyPrintingJsonBodyFilter(new JsonFactory(), true) + .filter("application/json", compacted); + assertThat(filtered).isEqualTo(compactedWithPreciseFloat); + } + } diff --git a/logbook-json/src/test/resources/student.json b/logbook-json/src/test/resources/student.json index 9935be1b8..7fffcf7ec 100644 --- a/logbook-json/src/test/resources/student.json +++ b/logbook-json/src/test/resources/student.json @@ -20,5 +20,6 @@ "Science": 1.9, "PE": 4.0 }, - "nickname": null -} \ No newline at end of file + "nickname": null, + "debt": 123450.40000000000000002 +} From 27515ed39aab1825789c59480936ed78a3de2f82 Mon Sep 17 00:00:00 2001 From: Karen Asmarian Date: Mon, 13 Jan 2025 13:58:01 +0100 Subject: [PATCH 2/6] extract usePreciseFloats check into a separate method --- .../logbook/json/JacksonJsonFieldBodyFilter.java | 16 ++++++++++------ .../logbook/json/ParsingJsonCompactor.java | 14 +++++++++----- .../json/PrettyPrintingJsonBodyFilter.java | 14 +++++++++----- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java b/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java index 1af46ae00..4ed78dd44 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java @@ -10,6 +10,7 @@ import javax.annotation.Nullable; import java.io.CharArrayWriter; +import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -62,12 +63,7 @@ public String filter(final String body) { JsonToken nextToken; while ((nextToken = parser.nextToken()) != null) { - - if (usePreciseFloats) { - generator.copyCurrentEventExact(parser); - } else { - generator.copyCurrentEvent(parser); - } + copyCurrentEvent(generator, parser); if (nextToken == JsonToken.FIELD_NAME && fields.contains(parser.currentName())) { nextToken = parser.nextToken(); generator.writeString(replacement); @@ -84,4 +80,12 @@ public String filter(final String body) { } } + private void copyCurrentEvent(JsonGenerator generator, JsonParser parser) throws IOException { + if (usePreciseFloats) { + generator.copyCurrentEventExact(parser); + } else { + generator.copyCurrentEvent(parser); + } + } + } diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java b/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java index d5b1ccd1d..a519c7fa5 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java @@ -38,11 +38,7 @@ public String compact(final String json) throws IOException { final JsonGenerator generator = factory.createGenerator(output)) { while (parser.nextToken() != null) { - if (usePreciseFloats) { - generator.copyCurrentEventExact(parser); - } else { - generator.copyCurrentEvent(parser); - } + copyCurrentEvent(generator, parser); } generator.flush(); @@ -51,4 +47,12 @@ public String compact(final String json) throws IOException { } } + private void copyCurrentEvent(JsonGenerator generator, JsonParser parser) throws IOException { + if (usePreciseFloats) { + generator.copyCurrentEventExact(parser); + } else { + generator.copyCurrentEvent(parser); + } + } + } diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java b/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java index f02168059..e9870bccf 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java @@ -59,11 +59,7 @@ public String filter(@Nullable final String contentType, final String body) { generator.useDefaultPrettyPrinter(); while (parser.nextToken() != null) { - if (usePreciseFloats) { - generator.copyCurrentEventExact(parser); - } else { - generator.copyCurrentEvent(parser); - } + copyCurrentEvent(generator, parser); } generator.flush(); @@ -75,4 +71,12 @@ public String filter(@Nullable final String contentType, final String body) { } } + private void copyCurrentEvent(JsonGenerator generator, JsonParser parser) throws IOException { + if (usePreciseFloats) { + generator.copyCurrentEventExact(parser); + } else { + generator.copyCurrentEvent(parser); + } + } + } From 65bcb76374bf8fdebf78a82880bd07c6bd34c2d2 Mon Sep 17 00:00:00 2001 From: Karen Asmarian Date: Mon, 13 Jan 2025 15:05:22 +0100 Subject: [PATCH 3/6] add a comment about performance penalty --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e858ab9ea..906ef5185 100644 --- a/README.md +++ b/README.md @@ -540,7 +540,8 @@ a JSON response body will **not** be escaped and represented as a string: > Filters for JSON bodies are using Jackson, which comes with a defect of dropping off precision from floating point > numbers (see [FasterXML/jackson-core/issues/984](https://github.com/FasterXML/jackson-core/issues/984)). > -> This can be changed by setting the `usePreciseFloats` flag to true in the filter respective filters. +> This can be changed by setting the `usePreciseFloats` flag to true in the filter respective filters. Using this flag +> may lead to a performance penalty as BigDecimal is usually used as the representation accessed from JsonParser. > > E.g. `new CompactingJsonBodyFilter(true)` will keep the precision of floating point numbers. From 20ba26678aaeaf2cfa8f6df8d64aca97a18bac5f Mon Sep 17 00:00:00 2001 From: Karen Asmarian Date: Fri, 17 Jan 2025 14:30:37 +0100 Subject: [PATCH 4/6] add strategy to handle different ways of handling floats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ...because no one likes boolean flags anymore ¯\_(ツ)_/¯ --- .../json/CompactingJsonBodyFilter.java | 4 +- .../json/DefaultJsonGeneratorWrapper.java | 9 +++++ .../DefaultJsonGeneratorWrapperCreator.java | 12 ++++++ .../json/JacksonJsonFieldBodyFilter.java | 22 +++-------- .../logbook/json/JsonGeneratorWrapper.java | 38 +++++++++++++++++++ .../json/JsonGeneratorWrapperCreator.java | 10 +++++ .../NumberAsStringJsonGeneratorWrapper.java | 11 ++++++ ...erAsStringJsonGeneratorWrapperCreator.java | 19 ++++++++++ .../logbook/json/ParsingJsonCompactor.java | 25 ++++-------- .../PreciseFloatJsonGeneratorWrapper.java | 18 +++++++++ ...eciseFloatJsonGeneratorWrapperCreator.java | 12 ++++++ .../json/PrettyPrintingJsonBodyFilter.java | 21 +++------- .../json/CompactingJsonBodyFilterTest.java | 12 +++++- .../json/JacksonJsonFieldBodyFilterTest.java | 2 +- .../PrettyPrintingJsonBodyFilterTest.java | 2 +- 15 files changed, 163 insertions(+), 54 deletions(-) create mode 100644 logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapper.java create mode 100644 logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapperCreator.java create mode 100644 logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapper.java create mode 100644 logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapperCreator.java create mode 100644 logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapper.java create mode 100644 logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapperCreator.java create mode 100644 logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapper.java create mode 100644 logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapperCreator.java diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/CompactingJsonBodyFilter.java b/logbook-json/src/main/java/org/zalando/logbook/json/CompactingJsonBodyFilter.java index d103a332a..a84ac134b 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/CompactingJsonBodyFilter.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/CompactingJsonBodyFilter.java @@ -21,8 +21,8 @@ public final class CompactingJsonBodyFilter implements BodyFilter { private final JsonCompactor compactor; - public CompactingJsonBodyFilter(final boolean usePreciseFloats) { - this(new ParsingJsonCompactor(usePreciseFloats)); + public CompactingJsonBodyFilter(final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator) { + this(new ParsingJsonCompactor(jsonGeneratorWrapperCreator)); } public CompactingJsonBodyFilter() { diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapper.java b/logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapper.java new file mode 100644 index 000000000..84f6d98de --- /dev/null +++ b/logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapper.java @@ -0,0 +1,9 @@ +package org.zalando.logbook.json; + +import com.fasterxml.jackson.core.JsonGenerator; + +final class DefaultJsonGeneratorWrapper extends JsonGeneratorWrapper { + public DefaultJsonGeneratorWrapper(JsonGenerator delegate) { + super(delegate); + } +} diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapperCreator.java b/logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapperCreator.java new file mode 100644 index 000000000..82025f9bc --- /dev/null +++ b/logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapperCreator.java @@ -0,0 +1,12 @@ +package org.zalando.logbook.json; + +import com.fasterxml.jackson.core.JsonFactory; + +import java.io.CharArrayWriter; +import java.io.IOException; + +public final class DefaultJsonGeneratorWrapperCreator implements JsonGeneratorWrapperCreator { + public JsonGeneratorWrapper create(JsonFactory factory, CharArrayWriter output) throws IOException { + return new DefaultJsonGeneratorWrapper(factory.createGenerator(output)); + } +} diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java b/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java index 4ed78dd44..c06da7abd 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java @@ -1,7 +1,6 @@ package org.zalando.logbook.json; import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import lombok.extern.slf4j.Slf4j; @@ -10,7 +9,6 @@ import javax.annotation.Nullable; import java.io.CharArrayWriter; -import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -30,20 +28,20 @@ public class JacksonJsonFieldBodyFilter implements BodyFilter { private final String replacement; private final Set fields; private final JsonFactory factory; - private final boolean usePreciseFloats; + private final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator; public JacksonJsonFieldBodyFilter(final Collection fieldNames, final String replacement, final JsonFactory factory, - final boolean usePreciseFloats) { + final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator) { this.fields = new HashSet<>(fieldNames); // thread safe for reading this.replacement = replacement; this.factory = factory; - this.usePreciseFloats = usePreciseFloats; + this.jsonGeneratorWrapperCreator = jsonGeneratorWrapperCreator; } public JacksonJsonFieldBodyFilter(final Collection fieldNames, final String replacement, final JsonFactory factory) { - this(fieldNames, replacement, factory, false); + this(fieldNames, replacement, factory, new DefaultJsonGeneratorWrapperCreator()); } public JacksonJsonFieldBodyFilter(final Collection fieldNames, final String replacement) { @@ -59,11 +57,11 @@ public String filter(final String body) { try ( final CharArrayWriter writer = new CharArrayWriter(body.length() * 2) ){ // rough estimate of final size) try (final JsonParser parser = factory.createParser(body); - final JsonGenerator generator = factory.createGenerator(writer)){ + final JsonGeneratorWrapper generator = jsonGeneratorWrapperCreator.create(factory, writer)){ JsonToken nextToken; while ((nextToken = parser.nextToken()) != null) { - copyCurrentEvent(generator, parser); + generator.copyCurrentEvent(parser); if (nextToken == JsonToken.FIELD_NAME && fields.contains(parser.currentName())) { nextToken = parser.nextToken(); generator.writeString(replacement); @@ -80,12 +78,4 @@ public String filter(final String body) { } } - private void copyCurrentEvent(JsonGenerator generator, JsonParser parser) throws IOException { - if (usePreciseFloats) { - generator.copyCurrentEventExact(parser); - } else { - generator.copyCurrentEvent(parser); - } - } - } diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapper.java b/logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapper.java new file mode 100644 index 000000000..734b803e9 --- /dev/null +++ b/logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapper.java @@ -0,0 +1,38 @@ +package org.zalando.logbook.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; + +public abstract class JsonGeneratorWrapper implements Closeable, Flushable { + protected final JsonGenerator delegate; + + public JsonGeneratorWrapper(JsonGenerator delegate) { + this.delegate = delegate; + } + + public void copyCurrentEvent(JsonParser parser) throws IOException { + delegate.copyCurrentEvent(parser); + } + + public void useDefaultPrettyPrinter() { + delegate.useDefaultPrettyPrinter(); + } + + @Override + public void close() throws IOException { + delegate.close(); + } + + @Override + public void flush() throws IOException { + delegate.flush(); + } + + public void writeString(String text) throws IOException { + delegate.writeString(text); + } +} diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapperCreator.java b/logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapperCreator.java new file mode 100644 index 000000000..420170025 --- /dev/null +++ b/logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapperCreator.java @@ -0,0 +1,10 @@ +package org.zalando.logbook.json; + +import com.fasterxml.jackson.core.JsonFactory; + +import java.io.CharArrayWriter; +import java.io.IOException; + +public interface JsonGeneratorWrapperCreator { + JsonGeneratorWrapper create(JsonFactory factory, CharArrayWriter output) throws IOException; +} diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapper.java b/logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapper.java new file mode 100644 index 000000000..3d9c9765d --- /dev/null +++ b/logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapper.java @@ -0,0 +1,11 @@ +package org.zalando.logbook.json; + +import com.fasterxml.jackson.core.JsonGenerator; + +final class NumberAsStringJsonGeneratorWrapper extends JsonGeneratorWrapper { + @SuppressWarnings("deprecation") + public NumberAsStringJsonGeneratorWrapper(JsonGenerator delegate) { + super(delegate); + delegate.enable(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS); + } +} diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapperCreator.java b/logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapperCreator.java new file mode 100644 index 000000000..c1ea11bf3 --- /dev/null +++ b/logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapperCreator.java @@ -0,0 +1,19 @@ +package org.zalando.logbook.json; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.json.JsonWriteFeature; + +import java.io.CharArrayWriter; +import java.io.IOException; + + +public final class NumberAsStringJsonGeneratorWrapperCreator implements JsonGeneratorWrapperCreator { + public JsonGeneratorWrapper create(JsonFactory factory, CharArrayWriter output) throws IOException { + final JsonGenerator generator = factory.rebuild() + .enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS) + .build() + .createGenerator(output); + return new NumberAsStringJsonGeneratorWrapper(generator); + } +} diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java b/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java index a519c7fa5..3b39c30d8 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java @@ -1,7 +1,6 @@ package org.zalando.logbook.json; import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import java.io.CharArrayWriter; @@ -11,15 +10,15 @@ final class ParsingJsonCompactor implements JsonCompactor { private final JsonFactory factory; - private final boolean usePreciseFloats; + private final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator; - public ParsingJsonCompactor(final JsonFactory factory, final boolean usePreciseFloats) { + public ParsingJsonCompactor(final JsonFactory factory, final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator) { this.factory = factory; - this.usePreciseFloats = usePreciseFloats; + this.jsonGeneratorWrapperCreator = jsonGeneratorWrapperCreator; } - public ParsingJsonCompactor(final boolean usePreciseFloats) { - this(new JsonFactory(), usePreciseFloats); + public ParsingJsonCompactor(final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator) { + this(new JsonFactory(), jsonGeneratorWrapperCreator); } public ParsingJsonCompactor() { @@ -27,7 +26,7 @@ public ParsingJsonCompactor() { } public ParsingJsonCompactor(final JsonFactory factory) { - this(factory, false); + this(factory, new DefaultJsonGeneratorWrapperCreator()); } @Override @@ -35,10 +34,10 @@ public String compact(final String json) throws IOException { try ( final CharArrayWriter output = new CharArrayWriter(json.length()); final JsonParser parser = factory.createParser(json); - final JsonGenerator generator = factory.createGenerator(output)) { + final JsonGeneratorWrapper generator = jsonGeneratorWrapperCreator.create(factory, output)) { while (parser.nextToken() != null) { - copyCurrentEvent(generator, parser); + generator.copyCurrentEvent(parser); } generator.flush(); @@ -47,12 +46,4 @@ public String compact(final String json) throws IOException { } } - private void copyCurrentEvent(JsonGenerator generator, JsonParser parser) throws IOException { - if (usePreciseFloats) { - generator.copyCurrentEventExact(parser); - } else { - generator.copyCurrentEvent(parser); - } - } - } diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapper.java b/logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapper.java new file mode 100644 index 000000000..b88a9f5a0 --- /dev/null +++ b/logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapper.java @@ -0,0 +1,18 @@ +package org.zalando.logbook.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; + +import java.io.IOException; + +final class PreciseFloatJsonGeneratorWrapper extends JsonGeneratorWrapper { + + public PreciseFloatJsonGeneratorWrapper(JsonGenerator delegate) { + super(delegate); + } + + @Override + public void copyCurrentEvent(JsonParser parser) throws IOException { + delegate.copyCurrentEventExact(parser); + } +} diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapperCreator.java b/logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapperCreator.java new file mode 100644 index 000000000..5f5e21fff --- /dev/null +++ b/logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapperCreator.java @@ -0,0 +1,12 @@ +package org.zalando.logbook.json; + +import com.fasterxml.jackson.core.JsonFactory; + +import java.io.CharArrayWriter; +import java.io.IOException; + +public final class PreciseFloatJsonGeneratorWrapperCreator implements JsonGeneratorWrapperCreator { + public JsonGeneratorWrapper create(JsonFactory factory, CharArrayWriter output) throws IOException { + return new PreciseFloatJsonGeneratorWrapper(factory.createGenerator(output)); + } +} diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java b/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java index e9870bccf..74411c124 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java @@ -1,7 +1,6 @@ package org.zalando.logbook.json; import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; @@ -20,16 +19,16 @@ public final class PrettyPrintingJsonBodyFilter implements BodyFilter { private final JsonFactory factory; - private final boolean usePreciseFloats; + private final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator; public PrettyPrintingJsonBodyFilter(final JsonFactory factory, - final boolean usePreciseFloats) { + final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator) { this.factory = factory; - this.usePreciseFloats = usePreciseFloats; + this.jsonGeneratorWrapperCreator = jsonGeneratorWrapperCreator; } public PrettyPrintingJsonBodyFilter(final JsonFactory factory) { - this(factory, false); + this(factory, new DefaultJsonGeneratorWrapperCreator()); } public PrettyPrintingJsonBodyFilter() { @@ -54,12 +53,12 @@ public String filter(@Nullable final String contentType, final String body) { try ( final CharArrayWriter output = new CharArrayWriter(body.length() * 2); // rough estimate of output size final JsonParser parser = factory.createParser(body); - final JsonGenerator generator = factory.createGenerator(output)) { + final JsonGeneratorWrapper generator = jsonGeneratorWrapperCreator.create(factory, output)) { generator.useDefaultPrettyPrinter(); while (parser.nextToken() != null) { - copyCurrentEvent(generator, parser); + generator.copyCurrentEvent(parser); } generator.flush(); @@ -71,12 +70,4 @@ public String filter(@Nullable final String contentType, final String body) { } } - private void copyCurrentEvent(JsonGenerator generator, JsonParser parser) throws IOException { - if (usePreciseFloats) { - generator.copyCurrentEventExact(parser); - } else { - generator.copyCurrentEvent(parser); - } - } - } diff --git a/logbook-json/src/test/java/org/zalando/logbook/json/CompactingJsonBodyFilterTest.java b/logbook-json/src/test/java/org/zalando/logbook/json/CompactingJsonBodyFilterTest.java index 859f9efaa..31213a27a 100644 --- a/logbook-json/src/test/java/org/zalando/logbook/json/CompactingJsonBodyFilterTest.java +++ b/logbook-json/src/test/java/org/zalando/logbook/json/CompactingJsonBodyFilterTest.java @@ -19,7 +19,6 @@ class CompactingJsonBodyFilterTest { /*language=JSON*/ private final String compacted = "{\"root\":{\"child\":\"text\",\"float_child\":0.4}}"; - private final String compactedWithPreciseFloat = "{\"root\":{\"child\":\"text\",\"float_child\":0.40000000000000002}}"; @Test void shouldIgnoreEmptyBody() { @@ -54,11 +53,20 @@ void shouldTransformValidJsonRequestWithCompatibleContentType() { @Test void shouldPreserveBigFloatOnCopy() { - final String filtered = new CompactingJsonBodyFilter(true) + final String filtered = new CompactingJsonBodyFilter(new PreciseFloatJsonGeneratorWrapperCreator()) .filter("application/custom+json", pretty); + final String compactedWithPreciseFloat = "{\"root\":{\"child\":\"text\",\"float_child\":0.40000000000000002}}"; assertThat(filtered).isEqualTo(compactedWithPreciseFloat); } + @Test + void shouldLogFloatAsString() { + final String filtered = new CompactingJsonBodyFilter(new NumberAsStringJsonGeneratorWrapperCreator()) + .filter("application/custom+json", pretty); + final String compactedWithFloatAsString = "{\"root\":{\"child\":\"text\",\"float_child\":\"0.40000000000000002\"}}"; + assertThat(filtered).isEqualTo(compactedWithFloatAsString); + } + @Test void shouldSkipInvalidJsonLookingLikeAValidOne() { final String invalidJson = "{invalid}"; diff --git a/logbook-json/src/test/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilterTest.java b/logbook-json/src/test/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilterTest.java index 08836d6bb..16ca81239 100644 --- a/logbook-json/src/test/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilterTest.java +++ b/logbook-json/src/test/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilterTest.java @@ -80,7 +80,7 @@ public void doesNotFilterNonJson() throws Exception { @Test public void shouldPreserveBigFloatOnCopy() throws Exception { final String string = getResource("/student.json").trim(); - final JacksonJsonFieldBodyFilter filter = new JacksonJsonFieldBodyFilter(Collections.emptyList(), "XXX", new JsonFactory(), true); + final JacksonJsonFieldBodyFilter filter = new JacksonJsonFieldBodyFilter(Collections.emptyList(), "XXX", new JsonFactory(), new PreciseFloatJsonGeneratorWrapperCreator()); final String filtered = filter.filter("application/json", string); assertThat(filtered).contains("\"debt\":123450.40000000000000002"); } diff --git a/logbook-json/src/test/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilterTest.java b/logbook-json/src/test/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilterTest.java index 900d7e00f..85edac112 100644 --- a/logbook-json/src/test/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilterTest.java +++ b/logbook-json/src/test/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilterTest.java @@ -81,7 +81,7 @@ void shouldConstructFromObjectMapper() { @Test void shouldPreserveBigFloatOnCopy() { - final String filtered = new PrettyPrintingJsonBodyFilter(new JsonFactory(), true) + final String filtered = new PrettyPrintingJsonBodyFilter(new JsonFactory(), new PreciseFloatJsonGeneratorWrapperCreator()) .filter("application/json", compacted); assertThat(filtered).isEqualTo(compactedWithPreciseFloat); } From 2499c374b076c0e6e8660c2a0f2f3e7ff0ccd8f0 Mon Sep 17 00:00:00 2001 From: Karen Asmarian Date: Mon, 20 Jan 2025 09:57:37 +0100 Subject: [PATCH 5/6] leve only one level of wrappers (remove creators) --- .../json/CompactingJsonBodyFilter.java | 4 +-- .../json/DefaultJsonGeneratorWrapper.java | 7 ++--- .../DefaultJsonGeneratorWrapperCreator.java | 12 -------- .../json/JacksonJsonFieldBodyFilter.java | 13 +++++---- .../logbook/json/JsonGeneratorWrapper.java | 28 ++----------------- .../json/JsonGeneratorWrapperCreator.java | 10 ------- .../NumberAsStringJsonGeneratorWrapper.java | 17 +++++++---- ...erAsStringJsonGeneratorWrapperCreator.java | 19 ------------- .../logbook/json/ParsingJsonCompactor.java | 18 ++++++------ .../PreciseFloatJsonGeneratorWrapper.java | 8 ++---- ...eciseFloatJsonGeneratorWrapperCreator.java | 12 -------- .../json/PrettyPrintingJsonBodyFilter.java | 13 +++++---- .../json/CompactingJsonBodyFilterTest.java | 4 +-- .../json/JacksonJsonFieldBodyFilterTest.java | 11 +++++++- .../PrettyPrintingJsonBodyFilterTest.java | 2 +- logbook-json/src/test/resources/student.json | 3 +- 16 files changed, 59 insertions(+), 122 deletions(-) delete mode 100644 logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapperCreator.java delete mode 100644 logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapperCreator.java delete mode 100644 logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapperCreator.java delete mode 100644 logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapperCreator.java diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/CompactingJsonBodyFilter.java b/logbook-json/src/main/java/org/zalando/logbook/json/CompactingJsonBodyFilter.java index a84ac134b..379d5ff1e 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/CompactingJsonBodyFilter.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/CompactingJsonBodyFilter.java @@ -21,8 +21,8 @@ public final class CompactingJsonBodyFilter implements BodyFilter { private final JsonCompactor compactor; - public CompactingJsonBodyFilter(final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator) { - this(new ParsingJsonCompactor(jsonGeneratorWrapperCreator)); + public CompactingJsonBodyFilter(final JsonGeneratorWrapper jsonGeneratorWrapper) { + this(new ParsingJsonCompactor(jsonGeneratorWrapper)); } public CompactingJsonBodyFilter() { diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapper.java b/logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapper.java index 84f6d98de..85774dbdb 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapper.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapper.java @@ -1,9 +1,6 @@ package org.zalando.logbook.json; -import com.fasterxml.jackson.core.JsonGenerator; -final class DefaultJsonGeneratorWrapper extends JsonGeneratorWrapper { - public DefaultJsonGeneratorWrapper(JsonGenerator delegate) { - super(delegate); - } +final class DefaultJsonGeneratorWrapper implements JsonGeneratorWrapper { + } diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapperCreator.java b/logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapperCreator.java deleted file mode 100644 index 82025f9bc..000000000 --- a/logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapperCreator.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.zalando.logbook.json; - -import com.fasterxml.jackson.core.JsonFactory; - -import java.io.CharArrayWriter; -import java.io.IOException; - -public final class DefaultJsonGeneratorWrapperCreator implements JsonGeneratorWrapperCreator { - public JsonGeneratorWrapper create(JsonFactory factory, CharArrayWriter output) throws IOException { - return new DefaultJsonGeneratorWrapper(factory.createGenerator(output)); - } -} diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java b/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java index c06da7abd..291828b0c 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilter.java @@ -1,6 +1,7 @@ package org.zalando.logbook.json; import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import lombok.extern.slf4j.Slf4j; @@ -28,20 +29,20 @@ public class JacksonJsonFieldBodyFilter implements BodyFilter { private final String replacement; private final Set fields; private final JsonFactory factory; - private final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator; + private final JsonGeneratorWrapper jsonGeneratorWrapper; public JacksonJsonFieldBodyFilter(final Collection fieldNames, final String replacement, final JsonFactory factory, - final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator) { + final JsonGeneratorWrapper jsonGeneratorWrapper) { this.fields = new HashSet<>(fieldNames); // thread safe for reading this.replacement = replacement; this.factory = factory; - this.jsonGeneratorWrapperCreator = jsonGeneratorWrapperCreator; + this.jsonGeneratorWrapper = jsonGeneratorWrapper; } public JacksonJsonFieldBodyFilter(final Collection fieldNames, final String replacement, final JsonFactory factory) { - this(fieldNames, replacement, factory, new DefaultJsonGeneratorWrapperCreator()); + this(fieldNames, replacement, factory, new DefaultJsonGeneratorWrapper()); } public JacksonJsonFieldBodyFilter(final Collection fieldNames, final String replacement) { @@ -57,11 +58,11 @@ public String filter(final String body) { try ( final CharArrayWriter writer = new CharArrayWriter(body.length() * 2) ){ // rough estimate of final size) try (final JsonParser parser = factory.createParser(body); - final JsonGeneratorWrapper generator = jsonGeneratorWrapperCreator.create(factory, writer)){ + final JsonGenerator generator = factory.createGenerator(writer)){ JsonToken nextToken; while ((nextToken = parser.nextToken()) != null) { - generator.copyCurrentEvent(parser); + jsonGeneratorWrapper.copyCurrentEvent(generator, parser); if (nextToken == JsonToken.FIELD_NAME && fields.contains(parser.currentName())) { nextToken = parser.nextToken(); generator.writeString(replacement); diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapper.java b/logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapper.java index 734b803e9..6ee12da81 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapper.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapper.java @@ -3,36 +3,12 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import java.io.Closeable; -import java.io.Flushable; import java.io.IOException; -public abstract class JsonGeneratorWrapper implements Closeable, Flushable { - protected final JsonGenerator delegate; +public interface JsonGeneratorWrapper { - public JsonGeneratorWrapper(JsonGenerator delegate) { - this.delegate = delegate; - } - - public void copyCurrentEvent(JsonParser parser) throws IOException { + default void copyCurrentEvent(final JsonGenerator delegate, final JsonParser parser) throws IOException { delegate.copyCurrentEvent(parser); } - public void useDefaultPrettyPrinter() { - delegate.useDefaultPrettyPrinter(); - } - - @Override - public void close() throws IOException { - delegate.close(); - } - - @Override - public void flush() throws IOException { - delegate.flush(); - } - - public void writeString(String text) throws IOException { - delegate.writeString(text); - } } diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapperCreator.java b/logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapperCreator.java deleted file mode 100644 index 420170025..000000000 --- a/logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapperCreator.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.zalando.logbook.json; - -import com.fasterxml.jackson.core.JsonFactory; - -import java.io.CharArrayWriter; -import java.io.IOException; - -public interface JsonGeneratorWrapperCreator { - JsonGeneratorWrapper create(JsonFactory factory, CharArrayWriter output) throws IOException; -} diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapper.java b/logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapper.java index 3d9c9765d..5f29c1fd3 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapper.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapper.java @@ -1,11 +1,18 @@ package org.zalando.logbook.json; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; -final class NumberAsStringJsonGeneratorWrapper extends JsonGeneratorWrapper { - @SuppressWarnings("deprecation") - public NumberAsStringJsonGeneratorWrapper(JsonGenerator delegate) { - super(delegate); - delegate.enable(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS); +import java.io.IOException; + +final class NumberAsStringJsonGeneratorWrapper implements JsonGeneratorWrapper { + + public void copyCurrentEvent(JsonGenerator delegate, JsonParser parser) throws IOException { + if (parser.getCurrentToken() == JsonToken.VALUE_NUMBER_FLOAT) { + delegate.writeString(parser.getValueAsString()); + } else { + delegate.copyCurrentEvent(parser); + } } } diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapperCreator.java b/logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapperCreator.java deleted file mode 100644 index c1ea11bf3..000000000 --- a/logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapperCreator.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.zalando.logbook.json; - -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.json.JsonWriteFeature; - -import java.io.CharArrayWriter; -import java.io.IOException; - - -public final class NumberAsStringJsonGeneratorWrapperCreator implements JsonGeneratorWrapperCreator { - public JsonGeneratorWrapper create(JsonFactory factory, CharArrayWriter output) throws IOException { - final JsonGenerator generator = factory.rebuild() - .enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS) - .build() - .createGenerator(output); - return new NumberAsStringJsonGeneratorWrapper(generator); - } -} diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java b/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java index 3b39c30d8..a20ecd853 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/ParsingJsonCompactor.java @@ -1,6 +1,7 @@ package org.zalando.logbook.json; import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import java.io.CharArrayWriter; @@ -10,15 +11,15 @@ final class ParsingJsonCompactor implements JsonCompactor { private final JsonFactory factory; - private final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator; + private final JsonGeneratorWrapper jsonGeneratorWrapper; - public ParsingJsonCompactor(final JsonFactory factory, final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator) { + public ParsingJsonCompactor(final JsonFactory factory, final JsonGeneratorWrapper jsonGeneratorWrapper) { this.factory = factory; - this.jsonGeneratorWrapperCreator = jsonGeneratorWrapperCreator; + this.jsonGeneratorWrapper = jsonGeneratorWrapper; } - public ParsingJsonCompactor(final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator) { - this(new JsonFactory(), jsonGeneratorWrapperCreator); + public ParsingJsonCompactor(final JsonGeneratorWrapper jsonGeneratorWrapper) { + this(new JsonFactory(), jsonGeneratorWrapper); } public ParsingJsonCompactor() { @@ -26,7 +27,7 @@ public ParsingJsonCompactor() { } public ParsingJsonCompactor(final JsonFactory factory) { - this(factory, new DefaultJsonGeneratorWrapperCreator()); + this(factory, new DefaultJsonGeneratorWrapper()); } @Override @@ -34,10 +35,11 @@ public String compact(final String json) throws IOException { try ( final CharArrayWriter output = new CharArrayWriter(json.length()); final JsonParser parser = factory.createParser(json); - final JsonGeneratorWrapper generator = jsonGeneratorWrapperCreator.create(factory, output)) { + final JsonGenerator generator = factory.createGenerator(output)) { + while (parser.nextToken() != null) { - generator.copyCurrentEvent(parser); + jsonGeneratorWrapper.copyCurrentEvent(generator, parser); } generator.flush(); diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapper.java b/logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapper.java index b88a9f5a0..c38fa4b1d 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapper.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapper.java @@ -5,14 +5,10 @@ import java.io.IOException; -final class PreciseFloatJsonGeneratorWrapper extends JsonGeneratorWrapper { - - public PreciseFloatJsonGeneratorWrapper(JsonGenerator delegate) { - super(delegate); - } +final class PreciseFloatJsonGeneratorWrapper implements JsonGeneratorWrapper { @Override - public void copyCurrentEvent(JsonParser parser) throws IOException { + public void copyCurrentEvent(JsonGenerator delegate, JsonParser parser) throws IOException { delegate.copyCurrentEventExact(parser); } } diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapperCreator.java b/logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapperCreator.java deleted file mode 100644 index 5f5e21fff..000000000 --- a/logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapperCreator.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.zalando.logbook.json; - -import com.fasterxml.jackson.core.JsonFactory; - -import java.io.CharArrayWriter; -import java.io.IOException; - -public final class PreciseFloatJsonGeneratorWrapperCreator implements JsonGeneratorWrapperCreator { - public JsonGeneratorWrapper create(JsonFactory factory, CharArrayWriter output) throws IOException { - return new PreciseFloatJsonGeneratorWrapper(factory.createGenerator(output)); - } -} diff --git a/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java b/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java index 74411c124..6ad66b570 100644 --- a/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java +++ b/logbook-json/src/main/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilter.java @@ -1,6 +1,7 @@ package org.zalando.logbook.json; import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; @@ -19,16 +20,16 @@ public final class PrettyPrintingJsonBodyFilter implements BodyFilter { private final JsonFactory factory; - private final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator; + private final JsonGeneratorWrapper jsonGeneratorWrapper; public PrettyPrintingJsonBodyFilter(final JsonFactory factory, - final JsonGeneratorWrapperCreator jsonGeneratorWrapperCreator) { + final JsonGeneratorWrapper jsonGeneratorWrapper) { this.factory = factory; - this.jsonGeneratorWrapperCreator = jsonGeneratorWrapperCreator; + this.jsonGeneratorWrapper = jsonGeneratorWrapper; } public PrettyPrintingJsonBodyFilter(final JsonFactory factory) { - this(factory, new DefaultJsonGeneratorWrapperCreator()); + this(factory, new DefaultJsonGeneratorWrapper()); } public PrettyPrintingJsonBodyFilter() { @@ -53,12 +54,12 @@ public String filter(@Nullable final String contentType, final String body) { try ( final CharArrayWriter output = new CharArrayWriter(body.length() * 2); // rough estimate of output size final JsonParser parser = factory.createParser(body); - final JsonGeneratorWrapper generator = jsonGeneratorWrapperCreator.create(factory, output)) { + final JsonGenerator generator = factory.createGenerator(output)) { generator.useDefaultPrettyPrinter(); while (parser.nextToken() != null) { - generator.copyCurrentEvent(parser); + jsonGeneratorWrapper.copyCurrentEvent(generator, parser); } generator.flush(); diff --git a/logbook-json/src/test/java/org/zalando/logbook/json/CompactingJsonBodyFilterTest.java b/logbook-json/src/test/java/org/zalando/logbook/json/CompactingJsonBodyFilterTest.java index 31213a27a..da48ff937 100644 --- a/logbook-json/src/test/java/org/zalando/logbook/json/CompactingJsonBodyFilterTest.java +++ b/logbook-json/src/test/java/org/zalando/logbook/json/CompactingJsonBodyFilterTest.java @@ -53,7 +53,7 @@ void shouldTransformValidJsonRequestWithCompatibleContentType() { @Test void shouldPreserveBigFloatOnCopy() { - final String filtered = new CompactingJsonBodyFilter(new PreciseFloatJsonGeneratorWrapperCreator()) + final String filtered = new CompactingJsonBodyFilter(new PreciseFloatJsonGeneratorWrapper()) .filter("application/custom+json", pretty); final String compactedWithPreciseFloat = "{\"root\":{\"child\":\"text\",\"float_child\":0.40000000000000002}}"; assertThat(filtered).isEqualTo(compactedWithPreciseFloat); @@ -61,7 +61,7 @@ void shouldPreserveBigFloatOnCopy() { @Test void shouldLogFloatAsString() { - final String filtered = new CompactingJsonBodyFilter(new NumberAsStringJsonGeneratorWrapperCreator()) + final String filtered = new CompactingJsonBodyFilter(new NumberAsStringJsonGeneratorWrapper()) .filter("application/custom+json", pretty); final String compactedWithFloatAsString = "{\"root\":{\"child\":\"text\",\"float_child\":\"0.40000000000000002\"}}"; assertThat(filtered).isEqualTo(compactedWithFloatAsString); diff --git a/logbook-json/src/test/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilterTest.java b/logbook-json/src/test/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilterTest.java index 16ca81239..ad7946555 100644 --- a/logbook-json/src/test/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilterTest.java +++ b/logbook-json/src/test/java/org/zalando/logbook/json/JacksonJsonFieldBodyFilterTest.java @@ -80,11 +80,20 @@ public void doesNotFilterNonJson() throws Exception { @Test public void shouldPreserveBigFloatOnCopy() throws Exception { final String string = getResource("/student.json").trim(); - final JacksonJsonFieldBodyFilter filter = new JacksonJsonFieldBodyFilter(Collections.emptyList(), "XXX", new JsonFactory(), new PreciseFloatJsonGeneratorWrapperCreator()); + final JacksonJsonFieldBodyFilter filter = new JacksonJsonFieldBodyFilter(Collections.emptyList(), "XXX", new JsonFactory(), new PreciseFloatJsonGeneratorWrapper()); final String filtered = filter.filter("application/json", string); assertThat(filtered).contains("\"debt\":123450.40000000000000002"); } + @Test + public void shouldLogFloatAsStringOnCopy() throws Exception { + final String string = getResource("/student.json").trim(); + final JacksonJsonFieldBodyFilter filter = new JacksonJsonFieldBodyFilter(Collections.singleton("balance"), "XXX", new JsonFactory(), new NumberAsStringJsonGeneratorWrapper()); + final String filtered = filter.filter("application/json", string); + assertThat(filtered).contains("\"balance\":\"XXX\""); + assertThat(filtered).contains("\"debt\":\"123450.40000000000000002\""); + } + private String getResource(final String path) throws IOException { final byte[] bytes = Files.readAllBytes(Paths.get("src/test/resources/" + path)); return new String(bytes, UTF_8); diff --git a/logbook-json/src/test/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilterTest.java b/logbook-json/src/test/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilterTest.java index 85edac112..186012243 100644 --- a/logbook-json/src/test/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilterTest.java +++ b/logbook-json/src/test/java/org/zalando/logbook/json/PrettyPrintingJsonBodyFilterTest.java @@ -81,7 +81,7 @@ void shouldConstructFromObjectMapper() { @Test void shouldPreserveBigFloatOnCopy() { - final String filtered = new PrettyPrintingJsonBodyFilter(new JsonFactory(), new PreciseFloatJsonGeneratorWrapperCreator()) + final String filtered = new PrettyPrintingJsonBodyFilter(new JsonFactory(), new PreciseFloatJsonGeneratorWrapper()) .filter("application/json", compacted); assertThat(filtered).isEqualTo(compactedWithPreciseFloat); } diff --git a/logbook-json/src/test/resources/student.json b/logbook-json/src/test/resources/student.json index 7fffcf7ec..cd9b26d78 100644 --- a/logbook-json/src/test/resources/student.json +++ b/logbook-json/src/test/resources/student.json @@ -21,5 +21,6 @@ "PE": 4.0 }, "nickname": null, - "debt": 123450.40000000000000002 + "debt": 123450.40000000000000002, + "balance": 0.40000000000000002 } From 46205015f0bb2f498e287f0c8e0a433d1c11bd8e Mon Sep 17 00:00:00 2001 From: Karen Asmarian Date: Mon, 20 Jan 2025 10:05:24 +0100 Subject: [PATCH 6/6] update README --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 906ef5185..16208eb1d 100644 --- a/README.md +++ b/README.md @@ -540,10 +540,12 @@ a JSON response body will **not** be escaped and represented as a string: > Filters for JSON bodies are using Jackson, which comes with a defect of dropping off precision from floating point > numbers (see [FasterXML/jackson-core/issues/984](https://github.com/FasterXML/jackson-core/issues/984)). > -> This can be changed by setting the `usePreciseFloats` flag to true in the filter respective filters. Using this flag -> may lead to a performance penalty as BigDecimal is usually used as the representation accessed from JsonParser. -> -> E.g. `new CompactingJsonBodyFilter(true)` will keep the precision of floating point numbers. +> This can be changed by passing different `JsonGeneratorWrapper` implementations to the filter respective filters. +> Available wrappers: +> * `DefaultJsonGeneratorWrapper` - default implementation, which doesn't alter Jackson's `JsonGenerator` behavior +> * `NumberAsStringJsonGeneratorWrapper` - writes floating point numbers as strings, and preserves their precision. +> * `PreciseFloatJsonGeneratorWrapper` - writes floating point with precision, may lead to a performance penalty as +> BigDecimal is usually used as the representation accessed from JsonParser. ##### Common Log Format