From a8189567dd0fb9620eb2f05e47ddcd7907b79e77 Mon Sep 17 00:00:00 2001 From: "robin.bygrave" Date: Fri, 19 Jun 2026 16:19:35 +1200 Subject: [PATCH] Replace EJson internals with avaje JsonMapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworks the DJsonService (the SpiJsonService SPI behind io.ebean.text.json.EJson) to use avaje JsonMapper instead of the bespoke EJsonReader/EJsonWriter, consolidating all read/write logic into a single JsonAdapter. Changes - New EbeanJsonAdapter — a JsonAdapter that materializes JSON into plain or modify-aware Map/List/Set, with two shared singletons (PLAIN, MODIFY_AWARE). Preserves existing EJson semantics: - Integral numbers → Long, decimals → BigDecimal - Write coverage for String/Integer/Long/Double/Float/BigDecimal/Boolean/Map/Collection with a toString() fallback for other types (UUID, enum, etc.) - Modify-aware loads share a single ModifyAwareFlag owner per root, reset to non-dirty once after the load completes - DJsonService now builds one JsonMapper + two JsonMapper.Type and routes parse/write through them. Null serialization is retained via a serializeNulls(true) writer; null/blank-input guards, token-honoring entry points, and parseSet (modify-aware asSet()) are preserved. - EJsonReader/EJsonWriter are now unused and should be deleted (couldn't remove them in this environment). Why Simplifies Ebean's JSON handling by reusing avaje-json-core's JsonMapper rather than maintaining a parallel reader/writer, while keeping behavior identical. --- .../io/ebeaninternal/json/DJsonService.java | 77 +++++-- .../io/ebeaninternal/json/EJsonReader.java | 204 ------------------ .../io/ebeaninternal/json/EJsonWriter.java | 149 ------------- .../ebeaninternal/json/EbeanJsonAdapter.java | 181 ++++++++++++++++ 4 files changed, 240 insertions(+), 371 deletions(-) delete mode 100644 ebean-core-json/src/main/java/io/ebeaninternal/json/EJsonReader.java delete mode 100644 ebean-core-json/src/main/java/io/ebeaninternal/json/EJsonWriter.java create mode 100644 ebean-core-json/src/main/java/io/ebeaninternal/json/EbeanJsonAdapter.java diff --git a/ebean-core-json/src/main/java/io/ebeaninternal/json/DJsonService.java b/ebean-core-json/src/main/java/io/ebeaninternal/json/DJsonService.java index cbec38cd5..ff75c1018 100644 --- a/ebean-core-json/src/main/java/io/ebeaninternal/json/DJsonService.java +++ b/ebean-core-json/src/main/java/io/ebeaninternal/json/DJsonService.java @@ -3,10 +3,13 @@ package io.ebeaninternal.json; import io.avaje.json.JsonReader; import io.avaje.json.JsonReader.Token; import io.avaje.json.JsonWriter; +import io.avaje.json.mapper.JsonMapper; +import io.avaje.json.stream.JsonStream; import io.ebean.service.SpiJsonService; import java.io.IOException; import java.io.Reader; +import java.io.StringWriter; import java.io.Writer; import java.util.Collection; import java.util.LinkedHashSet; @@ -16,98 +19,136 @@ import java.util.Set; /** * Utility that converts between JSON content and simple java Maps/Lists. + *

+ * Backed by avaje {@link JsonMapper} using {@link EbeanJsonAdapter} which + * preserves Ebean's modify-aware collection and number semantics. */ public final class DJsonService implements SpiJsonService { + private static final JsonStream JSON_STREAM = JsonStream.builder().build(); + private static final JsonMapper MAPPER = JsonMapper.builder().jsonStream(JSON_STREAM).build(); + + private static final JsonMapper.Type PLAIN = MAPPER.type(EbeanJsonAdapter.PLAIN); + private static final JsonMapper.Type MODIFY_AWARE = MAPPER.type(EbeanJsonAdapter.MODIFY_AWARE); + + private static JsonMapper.Type type(boolean modifyAware) { + return modifyAware ? MODIFY_AWARE : PLAIN; + } + + private static boolean blank(String content) { + return content == null || content.trim().isEmpty(); + } + + private static String readAll(Reader reader) throws IOException { + StringBuilder builder = new StringBuilder(); + char[] buffer = new char[2048]; + int len; + while ((len = reader.read(buffer)) != -1) { + builder.append(buffer, 0, len); + } + return builder.toString(); + } + @Override public String write(Object object) throws IOException { - return EJsonWriter.write(object); + StringWriter writer = new StringWriter(); + write(object, writer); + return writer.toString(); } @Override public void write(Object object, Writer writer) throws IOException { - EJsonWriter.write(object, writer); + JsonWriter jsonWriter = JSON_STREAM.writer(writer); + jsonWriter.serializeNulls(true); + PLAIN.toJson(object, jsonWriter); + jsonWriter.flush(); } @Override public void write(Object object, JsonWriter jsonWriter) throws IOException { - EJsonWriter.write(object, jsonWriter); + PLAIN.toJson(object, jsonWriter); } @Override public void writeCollection(Collection collection, JsonWriter jsonWriter) throws IOException { - EJsonWriter.writeCollection(collection, jsonWriter); + EbeanJsonAdapter.writeCollection(jsonWriter, collection); } @Override + @SuppressWarnings("unchecked") public Map parseObject(String json, boolean modifyAware) throws IOException { - return EJsonReader.parseObject(json, modifyAware); + return blank(json) ? null : (Map) type(modifyAware).fromJson(json); } @Override public Map parseObject(String json) throws IOException { - return EJsonReader.parseObject(json); + return parseObject(json, false); } @Override public Map parseObject(Reader reader, boolean modifyAware) throws IOException { - return EJsonReader.parseObject(reader, modifyAware); + return parseObject(readAll(reader), modifyAware); } @Override public Map parseObject(Reader reader) throws IOException { - return EJsonReader.parseObject(reader); + return parseObject(reader, false); } @Override + @SuppressWarnings("unchecked") public Map parseObject(JsonReader parser) throws IOException { - return EJsonReader.parseObject(parser); + return (Map) PLAIN.fromJson(parser); } @Override + @SuppressWarnings("unchecked") public Map parseObject(JsonReader parser, Token token) throws IOException { - return EJsonReader.parseObject(parser, token); + return (Map) EbeanJsonAdapter.read(parser, token, false); } @Override + @SuppressWarnings("unchecked") public List parseList(String json, boolean modifyAware) throws IOException { - return EJsonReader.parseList(json, modifyAware); + return blank(json) ? null : (List) type(modifyAware).fromJson(json); } @Override + @SuppressWarnings("unchecked") public List parseList(String json) throws IOException { - return EJsonReader.parseList(json); + return (List) parseList(json, false); } @Override public List parseList(Reader reader) throws IOException { - return EJsonReader.parseList(reader); + return parseList(readAll(reader)); } @Override + @SuppressWarnings("unchecked") public List parseList(JsonReader parser) throws IOException { - return EJsonReader.parseList(parser, false); + return (List) PLAIN.fromJson(parser); } @Override @SuppressWarnings("unchecked") public List parseList(JsonReader parser, Token currentToken) throws IOException { - return (List) EJsonReader.parse(parser, currentToken, false); + return (List) EbeanJsonAdapter.read(parser, currentToken, false); } @Override public Object parse(String json) throws IOException { - return EJsonReader.parse(json); + return blank(json) ? null : PLAIN.fromJson(json); } @Override public Object parse(Reader reader) throws IOException { - return EJsonReader.parse(reader); + return parse(readAll(reader)); } @Override public Object parse(JsonReader parser) throws IOException { - return EJsonReader.parse(parser); + return PLAIN.fromJson(parser); } @Override diff --git a/ebean-core-json/src/main/java/io/ebeaninternal/json/EJsonReader.java b/ebean-core-json/src/main/java/io/ebeaninternal/json/EJsonReader.java deleted file mode 100644 index 1d40f8f24..000000000 --- a/ebean-core-json/src/main/java/io/ebeaninternal/json/EJsonReader.java +++ /dev/null @@ -1,204 +0,0 @@ -package io.ebeaninternal.json; - -import io.avaje.json.JsonReader; -import io.avaje.json.JsonReader.Token; -import io.avaje.json.stream.JsonStream; -import io.ebean.ModifyAwareType; - -import java.io.IOException; -import java.io.Reader; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -final class EJsonReader { - - private EJsonReader() { - } - - private static JsonReader reader(String content) { - return JsonStream.builder().build().reader(content); - } - - @SuppressWarnings("unchecked") - static Map parseObject(String json, boolean modifyAware) throws IOException { - return (Map) parse(json, modifyAware); - } - - @SuppressWarnings("unchecked") - static Map parseObject(String json) throws IOException { - return (Map) parse(json); - } - - @SuppressWarnings("unchecked") - static Map parseObject(Reader reader) throws IOException { - return (Map) parse(reader); - } - - @SuppressWarnings("unchecked") - static Map parseObject(Reader reader, boolean modifyAware) throws IOException { - return (Map) parse(reader, modifyAware); - } - - @SuppressWarnings("unchecked") - static Map parseObject(JsonReader parser) throws IOException { - return (Map) parse(parser); - } - - @SuppressWarnings("unchecked") - static Map parseObject(JsonReader parser, Token token) throws IOException { - return (Map) parse(parser, token, false); - } - - @SuppressWarnings("unchecked") - static List parseList(String json, boolean modifyAware) throws IOException { - return (List) parse(json, modifyAware); - } - - @SuppressWarnings("unchecked") - static List parseList(String json) throws IOException { - return (List) parse(json); - } - - @SuppressWarnings("unchecked") - static List parseList(Reader reader) throws IOException { - return (List) parse(reader); - } - - @SuppressWarnings("unchecked") - static List parseList(JsonReader parser, boolean modifyAware) throws IOException { - return (List) parse(parser, null, modifyAware); - } - - static Object parse(String json) throws IOException { - return parseRawJson(json, null); - } - - static Object parse(String json, boolean modifyAware) throws IOException { - return parseRawJson(json, modifyAware ? new ModifyAwareFlag() : null); - } - - static Object parse(Reader reader) throws IOException { - return parseRawJson(readAll(reader), null); - } - - static Object parse(Reader reader, boolean modifyAware) throws IOException { - return parseRawJson(readAll(reader), modifyAware ? new ModifyAwareFlag() : null); - } - - static Object parse(JsonReader parser) throws IOException { - return parse(parser, null, false); - } - - static Object parse(JsonReader parser, boolean modifyAware) throws IOException { - return parse(parser, null, modifyAware); - } - - static Object parse(JsonReader parser, Token token, boolean modifyAware) throws IOException { - ModifyAwareType owner = modifyAware ? new ModifyAwareFlag() : null; - Token effectiveToken = token == null ? parser.currentToken() : token; - if (effectiveToken == null) { - return parseRawJson(parser.readRaw(), owner); - } - return parseValue(parser, effectiveToken, owner); - } - - private static Object parseValue(JsonReader parser, Token token, ModifyAwareType owner) throws IOException { - if (token == null) { - token = parser.currentToken(); - if (token == null) { - if (parser.isNullValue()) { - return null; - } - return parseRawJson(parser.readRaw(), owner); - } - } - if (token == Token.BEGIN_OBJECT) { - return parseObjectValue(parser, owner); - } - if (token == Token.BEGIN_ARRAY) { - return parseArrayValue(parser, owner); - } - if (token == Token.NUMBER) { - BigDecimal value = parser.readDecimal(); - return value.scale() <= 0 ? value.longValue() : value; - } - if (token == Token.STRING) { - return parser.readString(); - } - if (token == Token.BOOLEAN) { - return parser.readBoolean(); - } - if (token == Token.NULL) { - parser.isNullValue(); - return null; - } - return parseRawJson(parser.readRaw(), owner); - } - - private static Object parseRawJson(String json, ModifyAwareType owner) throws IOException { - if (json == null) { - return null; - } - String content = json.trim(); - if (content.isEmpty()) { - return null; - } - JsonReader parser = reader(content); - Token token = parser.currentToken(); - return parseValue(parser, token, owner); - } - - private static String readAll(Reader reader) throws IOException { - StringBuilder builder = new StringBuilder(); - char[] buffer = new char[2048]; - int len; - while ((len = reader.read(buffer)) != -1) { - builder.append(buffer, 0, len); - } - return builder.toString(); - } - - private static Map parseObjectValue(JsonReader parser, ModifyAwareType owner) throws IOException { - Map map = owner == null - ? new LinkedHashMap<>() - : new ModifyAwareMap<>(owner, new LinkedHashMap<>()); - - parser.beginObject(); - while (parser.hasNextField()) { - String fieldName = parser.nextField(); - map.put(fieldName, parseValue(parser, parser.currentToken(), owner)); - if (owner != null) { - owner.setMarkedDirty(false); - } - } - parser.endObject(); - if (owner != null) { - owner.setMarkedDirty(false); - } - return map; - } - - private static List parseArrayValue(JsonReader parser, ModifyAwareType owner) throws IOException { - List list = owner == null - ? new ArrayList<>() - : new ModifyAwareList<>(owner, new ArrayList<>()); - - parser.beginArray(); - while (parser.hasNextElement()) { - Token elementToken = parser.currentToken(); - Object elementValue = parseValue(parser, elementToken, owner); - list.add(elementValue); - if (owner != null) { - owner.setMarkedDirty(false); - } - } - parser.endArray(); - if (owner != null) { - owner.setMarkedDirty(false); - } - return list; - } -} diff --git a/ebean-core-json/src/main/java/io/ebeaninternal/json/EJsonWriter.java b/ebean-core-json/src/main/java/io/ebeaninternal/json/EJsonWriter.java deleted file mode 100644 index 46c7f4434..000000000 --- a/ebean-core-json/src/main/java/io/ebeaninternal/json/EJsonWriter.java +++ /dev/null @@ -1,149 +0,0 @@ -package io.ebeaninternal.json; - -import io.avaje.json.stream.JsonStream; - -import java.io.IOException; -import java.io.StringWriter; -import java.io.Writer; -import java.math.BigDecimal; -import java.util.Collection; -import java.util.Iterator; -import java.util.Map; - -/** - * Utility to write simple java Maps/Lists as JSON. - */ -final class EJsonWriter { - - private static final JsonStream JSON_STREAM = JsonStream.builder().build(); - - private EJsonWriter() { - } - - /** - * Convert object to Json content. - */ - static String write(Object object) throws IOException { - StringWriter writer = new StringWriter(); - write(object, writer); - return writer.toString(); - } - - /** - * Convert object to Json content. - */ - static io.avaje.json.JsonWriter write(Object object, Writer writer) throws IOException { - io.avaje.json.JsonWriter jsonWriter = JSON_STREAM.writer(writer); - jsonWriter.serializeNulls(true); - write(object, jsonWriter); - jsonWriter.flush(); - return jsonWriter; - } - - /** - * Convert object to Json content. - */ - static void write(Object object, io.avaje.json.JsonWriter jsonWriter) throws IOException { - if (object == null) { - jsonWriter.nullValue(); - return; - } - if (object instanceof String) { - jsonWriter.value((String) object); - return; - } - if (object instanceof Integer) { - jsonWriter.value((Integer) object); - return; - } - if (object instanceof Long) { - jsonWriter.value((Long) object); - return; - } - if (object instanceof Double) { - jsonWriter.value((Double) object); - return; - } - if (object instanceof Float) { - jsonWriter.value((Float) object); - return; - } - if (object instanceof BigDecimal) { - jsonWriter.value((BigDecimal) object); - return; - } - if (object instanceof Boolean) { - jsonWriter.value((Boolean) object); - return; - } - if (object instanceof Map) { - writeMap((Map) object, jsonWriter); - return; - } - if (object instanceof Collection) { - writeCollection((Collection) object, jsonWriter); - return; - } - - jsonWriter.value(object.toString()); - } - - /** - * Write map as Json content. - */ - private static void writeMap(Map map, io.avaje.json.JsonWriter jsonWriter) throws IOException { - jsonWriter.beginObject(); - for (Map.Entry entry : map.entrySet()) { - String fieldName = (String) entry.getKey(); - Object value = entry.getValue(); - jsonWriter.name(fieldName); - write(value, jsonWriter); - } - jsonWriter.endObject(); - } - - /** - * Write list as Json content. - */ - static void writeCollection(Collection list, io.avaje.json.JsonWriter jsonWriter) throws IOException { - jsonWriter.beginArray(); - for (Object element : list) { - write(element, jsonWriter); - } - jsonWriter.endArray(); - } - - /** - * Convert map to Json content. - */ - static String write(Map map) throws IOException { - StringWriter writer = new StringWriter(); - write(map, writer); - return writer.toString(); - } - - /** - * Convert map to Json content. - */ - static io.avaje.json.JsonWriter write(Map map, Writer writer) throws IOException { - io.avaje.json.JsonWriter jsonWriter = JSON_STREAM.writer(writer); - jsonWriter.serializeNulls(true); - write(map, jsonWriter); - jsonWriter.flush(); - return jsonWriter; - } - - /** - * Convert map to Json content. - */ - static void write(Map map, io.avaje.json.JsonWriter jsonWriter) throws IOException { - jsonWriter.beginObject(); - Iterator> it = map.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry entry = it.next(); - jsonWriter.name(entry.getKey()); - write(entry.getValue(), jsonWriter); - } - jsonWriter.endObject(); - } -} diff --git a/ebean-core-json/src/main/java/io/ebeaninternal/json/EbeanJsonAdapter.java b/ebean-core-json/src/main/java/io/ebeaninternal/json/EbeanJsonAdapter.java new file mode 100644 index 000000000..a4cf4d776 --- /dev/null +++ b/ebean-core-json/src/main/java/io/ebeaninternal/json/EbeanJsonAdapter.java @@ -0,0 +1,181 @@ +package io.ebeaninternal.json; + +import io.avaje.json.JsonAdapter; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonReader.Token; +import io.avaje.json.JsonWriter; +import io.avaje.json.stream.JsonStream; +import io.ebean.ModifyAwareType; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Ebean specific {@link JsonAdapter} that materializes JSON into plain Java + * Map/List/scalar values - optionally wrapped in modify-aware collections so + * that mutations after load are tracked as dirty. + *

+ * This consolidates the prior EJsonReader/EJsonWriter behavior into a single + * adapter that plugs into avaje {@code JsonMapper}. + */ +final class EbeanJsonAdapter implements JsonAdapter { + + static final EbeanJsonAdapter PLAIN = new EbeanJsonAdapter(false); + static final EbeanJsonAdapter MODIFY_AWARE = new EbeanJsonAdapter(true); + + private static final JsonStream JSON_STREAM = JsonStream.builder().build(); + + private final boolean modifyAware; + + private EbeanJsonAdapter(boolean modifyAware) { + this.modifyAware = modifyAware; + } + + @Override + public Object fromJson(JsonReader reader) { + return read(reader, null, modifyAware); + } + + @Override + public void toJson(JsonWriter writer, Object value) { + write(writer, value); + } + + /** + * Read a value honoring an explicitly supplied current token (or current token when null). + */ + static Object read(JsonReader parser, Token token, boolean modifyAware) { + ModifyAwareType owner = modifyAware ? new ModifyAwareFlag() : null; + Token effectiveToken = token == null ? parser.currentToken() : token; + Object value; + if (effectiveToken == null) { + value = parseRawJson(parser.readRaw(), owner); + } else { + value = parseValue(parser, effectiveToken, owner); + } + if (owner != null) { + owner.setMarkedDirty(false); + } + return value; + } + + private static Object parseValue(JsonReader parser, Token token, ModifyAwareType owner) { + if (token == null) { + token = parser.currentToken(); + if (token == null) { + if (parser.isNullValue()) { + return null; + } + return parseRawJson(parser.readRaw(), owner); + } + } + switch (token) { + case BEGIN_OBJECT: + return parseObjectValue(parser, owner); + case BEGIN_ARRAY: + return parseArrayValue(parser, owner); + case NUMBER: + BigDecimal value = parser.readDecimal(); + return value.scale() <= 0 ? value.longValue() : value; + case STRING: + return parser.readString(); + case BOOLEAN: + return parser.readBoolean(); + case NULL: + parser.isNullValue(); + return null; + default: + return parseRawJson(parser.readRaw(), owner); + } + } + + private static Object parseRawJson(String json, ModifyAwareType owner) { + if (json == null) { + return null; + } + String content = json.trim(); + if (content.isEmpty()) { + return null; + } + try (JsonReader parser = JSON_STREAM.reader(content)) { + return parseValue(parser, parser.currentToken(), owner); + } + } + + private static Map parseObjectValue(JsonReader parser, ModifyAwareType owner) { + Map map = owner == null + ? new LinkedHashMap<>() + : new ModifyAwareMap<>(owner, new LinkedHashMap<>()); + + parser.beginObject(); + while (parser.hasNextField()) { + String fieldName = parser.nextField(); + map.put(fieldName, parseValue(parser, parser.currentToken(), owner)); + } + parser.endObject(); + return map; + } + + private static List parseArrayValue(JsonReader parser, ModifyAwareType owner) { + List list = owner == null + ? new ArrayList<>() + : new ModifyAwareList<>(owner, new ArrayList<>()); + + parser.beginArray(); + while (parser.hasNextElement()) { + list.add(parseValue(parser, parser.currentToken(), owner)); + } + parser.endArray(); + return list; + } + + /** + * Write the value to an existing JsonWriter (used for the raw stream paths). + */ + static void write(JsonWriter jsonWriter, Object object) { + if (object == null) { + jsonWriter.nullValue(); + } else if (object instanceof String) { + jsonWriter.value((String) object); + } else if (object instanceof Integer) { + jsonWriter.value((Integer) object); + } else if (object instanceof Long) { + jsonWriter.value((Long) object); + } else if (object instanceof Double) { + jsonWriter.value((Double) object); + } else if (object instanceof Float) { + jsonWriter.value((Float) object); + } else if (object instanceof BigDecimal) { + jsonWriter.value((BigDecimal) object); + } else if (object instanceof Boolean) { + jsonWriter.value((Boolean) object); + } else if (object instanceof Map) { + writeMap(jsonWriter, (Map) object); + } else if (object instanceof Collection) { + writeCollection(jsonWriter, (Collection) object); + } else { + jsonWriter.value(object.toString()); + } + } + + private static void writeMap(JsonWriter jsonWriter, Map map) { + jsonWriter.beginObject(); + for (Map.Entry entry : map.entrySet()) { + jsonWriter.name((String) entry.getKey()); + write(jsonWriter, entry.getValue()); + } + jsonWriter.endObject(); + } + + static void writeCollection(JsonWriter jsonWriter, Collection collection) { + jsonWriter.beginArray(); + for (Object element : collection) { + write(jsonWriter, element); + } + jsonWriter.endArray(); + } +}