From 60db0b099921e3c83496cc988cfab128573de91f Mon Sep 17 00:00:00 2001 From: rbygrave Date: Tue, 24 Aug 2021 15:58:29 +1200 Subject: [PATCH] #2328 - Postgres JSONB dirty checking - trim json --- .../ebeaninternal/server/type/JsonTrim.java | 33 +++++++++++++++++++ .../type/ScalarTypeJsonObjectMapper.java | 12 ++++++- .../server/type/JsonTrimTest.java | 21 ++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/type/JsonTrim.java create mode 100644 ebean-core/src/test/java/io/ebeaninternal/server/type/JsonTrimTest.java diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/JsonTrim.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/JsonTrim.java new file mode 100644 index 000000000..97592fb34 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/JsonTrim.java @@ -0,0 +1,33 @@ +package io.ebeaninternal.server.type; + +/** + * Helper that removes whitespace from JSON. Used to normalise Postgres JSONB content. + */ +final class JsonTrim { + + /** + * Return JSON with whitespace trimmed. + */ + static String trim(String json) { + if (json == null) { + return null; + } + int len = json.length(); + StringBuilder builder = new StringBuilder(len); + boolean escaped = false; + boolean quoted = false; + for (int i = 0; i < len; i++) { + char c = json.charAt(i); + if (c == '\"') { + if (!escaped) quoted = !quoted; + else escaped = false; + } else if (quoted && c == '\\') { + escaped = true; + } + if (quoted || !Character.isWhitespace(c)) { + builder.append(c); + } + } + return builder.toString(); + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java index 5cda3f921..3a863df1e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java @@ -39,7 +39,11 @@ final class ScalarTypeJsonObjectMapper { return new GenericObject(jsonManager, field, dbType, docType); } // using the global default MutationDetection mode (defaults to HASH) - prop.setMutationDetection(jsonManager.mutationDetection()); + final MutationDetection defaultMode = jsonManager.mutationDetection(); + prop.setMutationDetection(defaultMode); + if (MutationDetection.NONE == defaultMode) { + return new NoMutationDetection(jsonManager, field, dbType, docType); + } return new GenericObject(jsonManager, field, dbType, docType); } @@ -68,8 +72,11 @@ final class ScalarTypeJsonObjectMapper { */ private static final class GenericObject extends Base { + private final boolean jsonb; + GenericObject(TypeJsonManager jsonManager, AnnotatedField field, int dbType, DocPropertyType docType) { super(Object.class, jsonManager, field, dbType, docType); + this.jsonb = "jsonb".equals(pgType); } @Override @@ -80,6 +87,9 @@ final class ScalarTypeJsonObjectMapper { @Override public Object read(DataReader reader) throws SQLException { String json = reader.getString(); + if (jsonb) { + json = JsonTrim.trim(json); + } // pushJson such that we MD5 and store on EntityBeanIntercept later reader.pushJson(json); if (json == null || json.isEmpty()) { diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/JsonTrimTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/JsonTrimTest.java new file mode 100644 index 000000000..622ceb3f7 --- /dev/null +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/JsonTrimTest.java @@ -0,0 +1,21 @@ +package io.ebeaninternal.server.type; + + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JsonTrimTest { + + @Test + public void trim_array() { + final String trimmed = JsonTrim.trim("[{\"name\": \"one\", \"along\": 1, \"timestamp\": 1629609021559}, {\"name\": \"two\", \"along\": 2, \"timestamp\": 1629609021559}]"); + assertThat(trimmed).isEqualTo("[{\"name\":\"one\",\"along\":1,\"timestamp\":1629609021559},{\"name\":\"two\",\"along\":2,\"timestamp\":1629609021559}]"); + } + + @Test + public void trim_object() { + final String trimmed = JsonTrim.trim("{\"name\": \"one\",\t \t \"along\": 1, \"timestamp\": 1629609021559}"); + assertThat(trimmed).isEqualTo("{\"name\":\"one\",\"along\":1,\"timestamp\":1629609021559}"); + } +}