From 313fdda8579dfd02e8d663250718b3822bdcf3c7 Mon Sep 17 00:00:00 2001 From: "robin.bygrave" Date: Thu, 18 Jun 2026 22:00:18 +1200 Subject: [PATCH 1/7] Refactor from Jackson Core to avaje-json-core --- docs/LIBRARY.md | 1 + docs/guides/AGENTS.md | 1 + docs/guides/README.md | 1 + ...ng-json-jackson-core-to-avaje-json-core.md | 91 +++++ ebean-api/pom.xml | 10 +- .../main/java/io/ebean/DatabaseBuilder.java | 16 +- .../java/io/ebean/config/ClassLoadConfig.java | 4 +- .../java/io/ebean/config/DatabaseConfig.java | 12 +- .../java/io/ebean/service/SpiJsonService.java | 28 +- .../main/java/io/ebean/text/json/EJson.java | 27 +- .../io/ebean/text/json/JsonBeanReader.java | 6 +- .../java/io/ebean/text/json/JsonContext.java | 57 ++- .../java/io/ebean/text/json/JsonWriter.java | 8 +- ebean-api/src/main/java/module-info.java | 2 +- ebean-core-json/pom.xml | 8 +- .../io/ebeaninternal/json/DJsonService.java | 99 +---- .../io/ebeaninternal/json/EJsonReader.java | 350 +++++------------- .../io/ebeaninternal/json/EJsonWriter.java | 271 ++++++-------- .../src/main/java/module-info.java | 3 +- ebean-core-type/pom.xml | 7 +- .../java/io/ebean/core/type/ScalarType.java | 45 ++- .../ebean/core/type/ScalarTypeBaseDate.java | 33 +- .../core/type/ScalarTypeBaseDateTime.java | 57 ++- .../core/type/ScalarTypeBaseVarchar.java | 15 +- .../src/main/java/module-info.java | 3 +- ebean-core/pom.xml | 8 - .../io/ebeaninternal/api/SpiJsonContext.java | 4 +- .../ebeaninternal/api/json/SpiJsonReader.java | 12 +- .../server/changelog/ChangeJsonBuilder.java | 59 +-- .../server/core/InternalConfiguration.java | 8 +- .../server/deploy/BeanChangeJson.java | 64 +--- .../server/deploy/BeanDescriptor.java | 15 +- .../BeanDescriptorElementEmbeddedMap.java | 22 +- .../deploy/BeanDescriptorElementScalar.java | 15 +- .../BeanDescriptorElementScalarMap.java | 22 +- .../server/deploy/BeanDescriptorJsonHelp.java | 87 ++--- .../server/deploy/BeanProperty.java | 9 +- .../server/deploy/BeanPropertyAssocMany.java | 115 ++++-- .../deploy/BeanPropertyAssocManyJsonHelp.java | 15 +- .../BeanPropertyAssocManyJsonTransient.java | 4 +- .../server/json/DJsonBeanReader.java | 4 +- .../server/json/DJsonContext.java | 214 ++++++----- .../server/json/DJsonScalar.java | 13 +- .../ebeaninternal/server/json/ReadJson.java | 40 +- .../ebeaninternal/server/json/WriteJson.java | 250 ++++--------- .../readaudit/DefaultReadAuditLogger.java | 71 ++-- .../server/type/ScalarTypeArrayList.java | 14 +- .../server/type/ScalarTypeArraySet.java | 14 +- .../server/type/ScalarTypeBigDecimal.java | 12 +- .../server/type/ScalarTypeBoolean.java | 39 +- .../server/type/ScalarTypeByte.java | 15 +- .../server/type/ScalarTypeBytesBase.java | 15 +- .../server/type/ScalarTypeBytesEncrypted.java | 15 +- .../server/type/ScalarTypeCharArray.java | 6 +- .../server/type/ScalarTypeDouble.java | 12 +- .../server/type/ScalarTypeDuration.java | 12 +- .../type/ScalarTypeEncryptedWrapper.java | 8 +- .../server/type/ScalarTypeEnumStandard.java | 21 +- .../server/type/ScalarTypeFile.java | 15 +- .../server/type/ScalarTypeFloat.java | 12 +- .../server/type/ScalarTypeInteger.java | 12 +- .../server/type/ScalarTypeJsonList.java | 10 +- .../server/type/ScalarTypeJsonMap.java | 10 +- .../server/type/ScalarTypeJsonSet.java | 10 +- .../server/type/ScalarTypeLocalDateTime.java | 19 +- .../server/type/ScalarTypeLocalTime.java | 12 +- .../server/type/ScalarTypeLong.java | 12 +- .../server/type/ScalarTypeMathBigInteger.java | 12 +- .../server/type/ScalarTypeMonthDay.java | 12 +- .../server/type/ScalarTypeNotFound.java | 8 +- .../server/type/ScalarTypePostgresHstore.java | 10 +- .../server/type/ScalarTypeShort.java | 12 +- .../server/type/ScalarTypeStringBase.java | 15 +- .../server/type/ScalarTypeTime.java | 12 +- .../server/type/ScalarTypeUUIDBase.java | 12 +- .../server/type/ScalarTypeWrapper.java | 8 +- .../server/type/ScalarTypeYear.java | 12 +- .../server/util/JsonContentHash.java | 150 ++++---- ebean-core/src/main/java/module-info.java | 2 +- .../ebeaninternal/server/type/JsonTester.java | 29 +- .../server/type/ScalarTypeArrayListTest.java | 15 +- .../server/type/ScalarTypeArraySetH2Test.java | 14 +- .../server/type/ScalarTypeBooleanTest.java | 37 +- .../type/ScalarTypeLocalDateTimeTest.java | 17 +- .../type/ScalarTypePostgresHstoreTest.java | 24 +- ebean-jackson-mapper/pom.xml | 7 + .../mapper/ScalarJsonJacksonMapper.java | 19 +- .../src/main/java/module-info.java | 1 + .../io/ebean/postgis/ScalarTypePgisBase.java | 8 +- .../io/ebean/pgvector/ScalarTypePGbase.java | 8 +- .../io/ebean/postgis/ScalarTypePgisBase.java | 8 +- .../postgis/latte/ScalarTypeGeoLatteBase.java | 8 +- .../src/main/java/io/ebean/test/DbJson.java | 8 +- .../server/text/json/DJsonScalarTest.java | 26 +- .../server/text/json/WriteJsonDirtyTest.java | 10 +- .../server/text/json/WriteJsonTest.java | 6 +- .../java/io/ebean/xtest/json/EJsonTests.java | 19 +- .../ebean/xtest/json/JsonBeanReaderTest.java | 14 +- .../io/ebean/xtest/json/JsonContextTest.java | 33 +- .../json/TestJsonBeanDescriptorParse.java | 7 +- .../resources/bean/example-list-contains.json | 2 - .../resources/bean/example-list-match.json | 29 +- pom.xml | 1 + 103 files changed, 1477 insertions(+), 1639 deletions(-) create mode 100644 docs/guides/migrating-json-jackson-core-to-avaje-json-core.md diff --git a/docs/LIBRARY.md b/docs/LIBRARY.md index 82e27c9859..9e8b86fb5f 100644 --- a/docs/LIBRARY.md +++ b/docs/LIBRARY.md @@ -183,6 +183,7 @@ database.save(customer); | Configure database and `Database` bean | [add-ebean-postgres-database-config.md](guides/add-ebean-postgres-database-config.md) | | Add PostgreSQL test container support | [add-ebean-postgres-test-container.md](guides/add-ebean-postgres-test-container.md) | | Generate DB migrations | [add-ebean-db-migration-generation.md](guides/add-ebean-db-migration-generation.md) | +| Migrate JSON APIs from Jackson core to avaje-json-core | [migrating-json-jackson-core-to-avaje-json-core.md](guides/migrating-json-jackson-core-to-avaje-json-core.md) | | Model entity beans correctly | [entity-bean-creation.md](guides/entity-bean-creation.md) | | Use Lombok safely with entities | [lombok-with-ebean-entity-beans.md](guides/lombok-with-ebean-entity-beans.md) | | Write type-safe query bean queries | [writing-ebean-query-beans.md](guides/writing-ebean-query-beans.md) | diff --git a/docs/guides/AGENTS.md b/docs/guides/AGENTS.md index 0fec50b33a..4109ab263f 100644 --- a/docs/guides/AGENTS.md +++ b/docs/guides/AGENTS.md @@ -12,6 +12,7 @@ Key guides (fetch and follow when performing the relevant task): - Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md - Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md - Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md +- Migrate JSON APIs from Jackson core to avaje-json-core: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-json-jackson-core-to-avaje-json-core.md - Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md - Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md - Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md diff --git a/docs/guides/README.md b/docs/guides/README.md index b85a87b7be..a9432d6c50 100644 --- a/docs/guides/README.md +++ b/docs/guides/README.md @@ -23,6 +23,7 @@ existing Maven project. Complete the steps in order. | Guide | Description | |-------|-------------| | [Migrate to `Database.builder()`](migrating-to-database-builder.md) | Replace legacy `new DatabaseConfig()` and `DatabaseFactory.create(...)` code with `Database.builder()` and `DatabaseBuilder.build()`. Includes common rewrites, fluent builder equivalents, and manual-review cases for semi-automated upgrades | +| [Migrate JSON APIs from Jackson core to avaje-json-core](migrating-json-jackson-core-to-avaje-json-core.md) | Cut over `JsonParser`/`JsonGenerator`/`JsonFactory` usage to `JsonReader`/`JsonWriter`/`JsonStream`, including `DatabaseBuilder`/`DatabaseConfig` JSON config changes and validation checklist | ## Observability diff --git a/docs/guides/migrating-json-jackson-core-to-avaje-json-core.md b/docs/guides/migrating-json-jackson-core-to-avaje-json-core.md new file mode 100644 index 0000000000..da7fbc218a --- /dev/null +++ b/docs/guides/migrating-json-jackson-core-to-avaje-json-core.md @@ -0,0 +1,91 @@ +# Guide: Migrate JSON APIs from Jackson core to avaje-json-core + +## Purpose + +This guide covers the one-step cutover in Ebean from Jackson core JSON APIs to +avaje-json-core APIs. + +Use this when upgrading code that references: + +- `com.fasterxml.jackson.core.JsonParser` +- `com.fasterxml.jackson.core.JsonGenerator` +- `com.fasterxml.jackson.core.JsonFactory` + +The replacement types are: + +- `io.avaje.json.JsonReader` +- `io.avaje.json.JsonWriter` +- `io.avaje.json.stream.JsonStream` + +--- + +## Breaking API changes + +| Previous API | New API | +|---|---| +| `JsonParser` | `JsonReader` | +| `JsonGenerator` | `JsonWriter` | +| `JsonFactory` | `JsonStream` | +| `DatabaseBuilder.jsonFactory(...)` | `DatabaseBuilder.jsonStream(...)` | +| `DatabaseConfig.getJsonFactory()/setJsonFactory(...)` | `DatabaseConfig.getJsonStream()/setJsonStream(...)` | + +--- + +## Typical migration rewrites + +### Parser and generator signatures + +```java +// before +void read(JsonParser parser) +void write(JsonGenerator generator) + +// after +void read(JsonReader parser) +void write(JsonWriter generator) +``` + +### Database configuration + +```java +// before +Database.builder().jsonFactory(factory) + +// after +Database.builder().jsonStream(stream) +``` + +### JSON utility calls + +`EJson` and `JsonContext` APIs now operate on `JsonReader` and `JsonWriter` types. +If your code was calling those APIs with Jackson core types, switch to avaje types. + +--- + +## Dependency and module notes + +- `ebean-core` no longer requires a direct `jackson-core` dependency for JSON + parsing/writing. +- `jackson-databind` remains optional for `ObjectMapper` compatibility paths. +- `ebean-jackson-mapper` remains the compatibility bridge module for mapper-based + integrations. + +--- + +## Behavior notes to verify during upgrade + +1. Parser token handling is now based on avaje `JsonReader.Token`. +2. Scalar JSON reads (for example booleans, date-time, array scalar types) should + be validated in your tests if you previously depended on Jackson token quirks. +3. If your integration uses transient assoc-many JSON mapping with ObjectMapper, + keep ObjectMapper wiring enabled. + +--- + +## Validation checklist + +1. Compile all modules that implement or consume `io.ebean.text.json` APIs. +2. Run module tests that cover JSON scalar conversion and bean JSON round-trips. +3. Confirm no remaining `com.fasterxml.jackson.core.*` imports in migrated code. +4. Keep `ObjectMapper` compatibility tests if your project depends on mapper paths. + diff --git a/ebean-api/pom.xml b/ebean-api/pom.xml index e7435fbdc4..5d7b43c81b 100644 --- a/ebean-api/pom.xml +++ b/ebean-api/pom.xml @@ -70,15 +70,13 @@ true - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - true + io.avaje + avaje-json-core + ${avaje-json-core.version} - + com.fasterxml.jackson.core jackson-databind diff --git a/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java b/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java index 0f1c6eeebb..51e6f9618b 100644 --- a/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java +++ b/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java @@ -1,6 +1,6 @@ package io.ebean; -import com.fasterxml.jackson.core.JsonFactory; +import io.avaje.json.stream.JsonStream; import io.ebean.annotation.*; import io.ebean.cache.ServerCachePlugin; import io.ebean.config.*; @@ -360,18 +360,18 @@ default DatabaseBuilder slowQueryListener(SlowQueryListener slowQueryListener) { DatabaseBuilder putServiceObject(Object configObject); /** - * Set the Jackson JsonFactory to use. + * Set the JsonStream to use. *

* If not set a default implementation will be used. */ - default DatabaseBuilder jsonFactory(JsonFactory jsonFactory) { - return setJsonFactory(jsonFactory); + default DatabaseBuilder jsonStream(JsonStream jsonStream) { + return setJsonStream(jsonStream); } /** - * @deprecated migrate to {@link #jsonFactory(JsonFactory)}. + * @deprecated migrate to {@link #jsonStream(JsonStream)}. */ - DatabaseBuilder setJsonFactory(JsonFactory jsonFactory); + DatabaseBuilder setJsonStream(JsonStream jsonStream); /** * Set the JSON format to use for DateTime types. @@ -2254,11 +2254,11 @@ interface Settings extends DatabaseBuilder { boolean isAutoLoadModuleInfo(); /** - * Return the Jackson JsonFactory to use. + * Return the JsonStream to use. *

* If not set a default implementation will be used. */ - JsonFactory getJsonFactory(); + JsonStream getJsonStream(); /** * Get the clock used for setting the timestamps (e.g. @UpdatedTimestamp) on objects. diff --git a/ebean-api/src/main/java/io/ebean/config/ClassLoadConfig.java b/ebean-api/src/main/java/io/ebean/config/ClassLoadConfig.java index 7fa749ca87..8a1bf02ab1 100644 --- a/ebean-api/src/main/java/io/ebean/config/ClassLoadConfig.java +++ b/ebean-api/src/main/java/io/ebean/config/ClassLoadConfig.java @@ -60,7 +60,8 @@ public boolean isJacksonAnnotationsPresent() { } public boolean isJacksonCorePresent() { - return isPresent("com.fasterxml.jackson.core.JsonParser"); + // Legacy method name retained for compatibility; now checks avaje JSON core. + return isPresent("io.avaje.json.JsonReader"); } /** @@ -158,4 +159,3 @@ ClassLoader getClassLoader() { } } } - diff --git a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java index 3c36245733..33dbf399f0 100644 --- a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java +++ b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java @@ -1,7 +1,7 @@ package io.ebean.config; -import com.fasterxml.jackson.core.JsonFactory; import io.avaje.config.Config; +import io.avaje.json.stream.JsonStream; import io.ebean.*; import io.ebean.annotation.MutationDetection; import io.ebean.annotation.PersistBatch; @@ -420,7 +420,7 @@ public class DatabaseConfig implements DatabaseBuilder.Settings { * The default PersistenceContextScope used if one is not explicitly set on a query. */ private PersistenceContextScope persistenceContextScope = PersistenceContextScope.TRANSACTION; - private JsonFactory jsonFactory; + private JsonStream jsonStream; private boolean localTimeWithNanos; private boolean durationWithNanos; private int maxCallStack = 5; @@ -631,13 +631,13 @@ private String serviceObjectKey(Class cls) { } @Override - public JsonFactory getJsonFactory() { - return jsonFactory; + public JsonStream getJsonStream() { + return jsonStream; } @Override - public DatabaseConfig setJsonFactory(JsonFactory jsonFactory) { - this.jsonFactory = jsonFactory; + public DatabaseConfig setJsonStream(JsonStream jsonStream) { + this.jsonStream = jsonStream; return this; } diff --git a/ebean-api/src/main/java/io/ebean/service/SpiJsonService.java b/ebean-api/src/main/java/io/ebean/service/SpiJsonService.java index a7fb1753d3..25eb68a952 100644 --- a/ebean-api/src/main/java/io/ebean/service/SpiJsonService.java +++ b/ebean-api/src/main/java/io/ebean/service/SpiJsonService.java @@ -1,8 +1,8 @@ package io.ebean.service; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonReader.Token; +import io.avaje.json.JsonWriter; import java.io.IOException; import java.io.Reader; @@ -32,12 +32,12 @@ public interface SpiJsonService extends BootstrapService { /** * Write the nested Map/List as json to the jsonGenerator. */ - void write(Object object, JsonGenerator jsonGenerator) throws IOException; + void write(Object object, JsonWriter jsonGenerator) throws IOException; /** * Write the collection as json array to the jsonGenerator. */ - void writeCollection(Collection collection, JsonGenerator jsonGenerator) throws IOException; + void writeCollection(Collection collection, JsonWriter jsonGenerator) throws IOException; /** * Parse the json and return as a Map additionally specifying if the returned map should @@ -61,17 +61,17 @@ public interface SpiJsonService extends BootstrapService { Map parseObject(Reader reader) throws IOException; /** - * Parse the json and return as a Map taking a JsonParser. + * Parse the json and return as a Map taking a JsonReader. */ - Map parseObject(JsonParser parser) throws IOException; + Map parseObject(JsonReader parser) throws IOException; /** - * Parse the json and return as a Map taking a JsonParser and a starting token. + * Parse the json and return as a Map taking a JsonReader and a starting token. *

* Used when the first token is checked to see if the value is null prior to calling this. *

*/ - Map parseObject(JsonParser parser, JsonToken token) throws IOException; + Map parseObject(JsonReader parser, Token token) throws IOException; /** * Parse the json and return as a modify aware List. @@ -89,14 +89,14 @@ public interface SpiJsonService extends BootstrapService { List parseList(Reader reader) throws IOException; /** - * Parse the json and return as a List taking a JsonParser. + * Parse the json and return as a List taking a JsonReader. */ - List parseList(JsonParser parser) throws IOException; + List parseList(JsonReader parser) throws IOException; /** * Parse the json returning as a List taking into account the current token. */ - List parseList(JsonParser parser, JsonToken currentToken) throws IOException; + List parseList(JsonReader parser, Token currentToken) throws IOException; /** * Parse the json and return as a List or Map. @@ -111,7 +111,7 @@ public interface SpiJsonService extends BootstrapService { /** * Parse the json and return as a List or Map. */ - Object parse(JsonParser parser) throws IOException; + Object parse(JsonReader parser) throws IOException; /** * Parse the json returning a Set that might be modify aware. @@ -121,5 +121,5 @@ public interface SpiJsonService extends BootstrapService { /** * Parse the json returning as a Set taking into account the current token. */ - Set parseSet(JsonParser parser, JsonToken currentToken) throws IOException; + Set parseSet(JsonReader parser, Token currentToken) throws IOException; } diff --git a/ebean-api/src/main/java/io/ebean/text/json/EJson.java b/ebean-api/src/main/java/io/ebean/text/json/EJson.java index e1aabd7012..61fe6a6a29 100644 --- a/ebean-api/src/main/java/io/ebean/text/json/EJson.java +++ b/ebean-api/src/main/java/io/ebean/text/json/EJson.java @@ -1,8 +1,7 @@ package io.ebean.text.json; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonReader.Token; import io.ebean.XBootstrapService; import io.ebean.service.SpiJsonService; @@ -38,14 +37,14 @@ public static void write(Object object, Writer writer) throws IOException { /** * Write the nested Map/List as json to the jsonGenerator. */ - public static void write(Object object, JsonGenerator jsonGenerator) throws IOException { + public static void write(Object object, io.avaje.json.JsonWriter jsonGenerator) throws IOException { plugin.write(object, jsonGenerator); } /** * Write the collection as json array to the jsonGenerator. */ - public static void writeCollection(Collection collection, JsonGenerator jsonGenerator) throws IOException { + public static void writeCollection(Collection collection, io.avaje.json.JsonWriter jsonGenerator) throws IOException { plugin.writeCollection(collection, jsonGenerator); } @@ -79,19 +78,19 @@ public static Map parseObject(Reader reader) throws IOException } /** - * Parse the json and return as a Map taking a JsonParser. + * Parse the json and return as a Map taking a JsonReader. */ - public static Map parseObject(JsonParser parser) throws IOException { + public static Map parseObject(JsonReader parser) throws IOException { return plugin.parseObject(parser); } /** - * Parse the json and return as a Map taking a JsonParser and a starting token. + * Parse the json and return as a Map taking a JsonReader and a starting token. *

* Used when the first token is checked to see if the value is null prior to calling this. *

*/ - public static Map parseObject(JsonParser parser, JsonToken token) throws IOException { + public static Map parseObject(JsonReader parser, Token token) throws IOException { return plugin.parseObject(parser, token); } @@ -117,16 +116,16 @@ public static List parseList(Reader reader) throws IOException { } /** - * Parse the json and return as a List taking a JsonParser. + * Parse the json and return as a List taking a JsonReader. */ - public static List parseList(JsonParser parser) throws IOException { + public static List parseList(JsonReader parser) throws IOException { return plugin.parseList(parser); } /** * Parse the json returning as a List taking into account the current token. */ - public static List parseList(JsonParser parser, JsonToken currentToken) throws IOException { + public static List parseList(JsonReader parser, Token currentToken) throws IOException { return plugin.parseList(parser, currentToken); } @@ -147,7 +146,7 @@ public static Object parse(Reader reader) throws IOException { /** * Parse the json and return as a List or Map. */ - public static Object parse(JsonParser parser) throws IOException { + public static Object parse(JsonReader parser) throws IOException { return plugin.parse(parser); } @@ -161,7 +160,7 @@ public static Set parseSet(String json, boolean modifyAware) throws IOExc /** * Parse the json returning as a Set taking into account the current token. */ - public static Set parseSet(JsonParser parser, JsonToken currentToken) throws IOException { + public static Set parseSet(JsonReader parser, Token currentToken) throws IOException { return plugin.parseSet(parser, currentToken); } } diff --git a/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java b/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java index 935b788600..ddc3c4bce0 100644 --- a/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java +++ b/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java @@ -1,6 +1,6 @@ package io.ebean.text.json; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; import io.ebean.bean.PersistenceContext; /** @@ -25,9 +25,9 @@ default T read() { } /** - * Create a new reader taking the context from the existing one but using a new JsonParser. + * Create a new reader taking the context from the existing one but using a new JsonReader. */ - JsonBeanReader forJson(JsonParser moreJson); + JsonBeanReader forJson(JsonReader moreJson); /** * Add a bean explicitly to the persistence context. diff --git a/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java b/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java index d2c0df822b..0c9925f5fc 100644 --- a/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java +++ b/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java @@ -1,7 +1,6 @@ package io.ebean.text.json; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; import io.ebean.FetchPath; import io.ebean.plugin.BeanType; @@ -49,14 +48,14 @@ public interface JsonContext { * * @throws JsonIOException When IOException occurs */ - T toBean(Class cls, JsonParser parser) throws JsonIOException; + T toBean(Class cls, JsonReader parser) throws JsonIOException; /** * Convert json parser input into a Bean of a specific type additionally using JsonReadOptions.. * * @throws JsonIOException When IOException occurs */ - T toBean(Class cls, JsonParser parser, JsonReadOptions options) throws JsonIOException; + T toBean(Class cls, JsonReader parser, JsonReadOptions options) throws JsonIOException; /** * Read json parser input into a given Bean.
@@ -65,19 +64,19 @@ public interface JsonContext { * * @throws JsonIOException When IOException occurs */ - void toBean(T target, JsonParser parser) throws JsonIOException; + void toBean(T target, JsonReader parser) throws JsonIOException; /** * Read json parser input into a given Bean additionally using JsonReadOptions.
- * See {@link #toBean(Class, JsonParser)} for details modified. + * See {@link #toBean(Class, JsonReader)} for details modified. * * @throws JsonIOException When IOException occurs */ - void toBean(T target, JsonParser parser, JsonReadOptions options) throws JsonIOException; + void toBean(T target, JsonReader parser, JsonReadOptions options) throws JsonIOException; /** * Read json reader input into a given Bean.
- * See {@link #toBean(Class, JsonParser)} for details + * See {@link #toBean(Class, JsonReader)} for details * * @throws JsonIOException When IOException occurs */ @@ -85,7 +84,7 @@ public interface JsonContext { /** * Read json reader input into a given Bean additionally using JsonReadOptions.
- * See {@link #toBean(Class, JsonParser)} for details modified. + * See {@link #toBean(Class, JsonReader)} for details modified. * * @throws JsonIOException When IOException occurs */ @@ -93,7 +92,7 @@ public interface JsonContext { /** * Read json string input into a given Bean.
- * See {@link #toBean(Class, JsonParser)} for details + * See {@link #toBean(Class, JsonReader)} for details * * @throws JsonIOException When IOException occurs */ @@ -101,7 +100,7 @@ public interface JsonContext { /** * Read json string input into a given Bean additionally using JsonReadOptions.
- * See {@link #toBean(Class, JsonParser)} for details + * See {@link #toBean(Class, JsonReader)} for details * * @throws JsonIOException When IOException occurs */ @@ -113,7 +112,7 @@ public interface JsonContext { * Note that JsonOption provides an option for setting a persistence context and also enabling further lazy loading. Further lazy * loading requires a persistence context so if that is set on then a persistence context is created if there is not one set. */ - JsonBeanReader createBeanReader(Class cls, JsonParser parser, JsonReadOptions options) throws JsonIOException; + JsonBeanReader createBeanReader(Class cls, JsonReader parser, JsonReadOptions options) throws JsonIOException; /** * Create and return a new bean reading for the bean type given the JSON options and source. @@ -122,7 +121,7 @@ public interface JsonContext { * further lazy loading. Further lazy loading requires a persistence context so if that is set * on then a persistence context is created if there is not one set. */ - JsonBeanReader createBeanReader(BeanType beanType, JsonParser parser, JsonReadOptions options) throws JsonIOException; + JsonBeanReader createBeanReader(BeanType beanType, JsonReader parser, JsonReadOptions options) throws JsonIOException; /** * Convert json string input into a list of beans of a specific type. @@ -157,14 +156,14 @@ public interface JsonContext { * * @throws JsonIOException When IOException occurs */ - List toList(Class cls, JsonParser json) throws JsonIOException; + List toList(Class cls, JsonReader json) throws JsonIOException; /** * Convert json parser input into a list of beans of a specific type additionally using JsonReadOptions. * * @throws JsonIOException When IOException occurs */ - List toList(Class cls, JsonParser json, JsonReadOptions options) throws JsonIOException; + List toList(Class cls, JsonReader json, JsonReadOptions options) throws JsonIOException; /** * Use the genericType to determine if this should be converted into a List or @@ -188,7 +187,7 @@ public interface JsonContext { * * @throws JsonIOException When IOException occurs */ - Object toObject(Type genericType, JsonParser jsonParser) throws JsonIOException; + Object toObject(Type genericType, JsonReader jsonParser) throws JsonIOException; /** * Return the bean or collection as JSON string. @@ -212,11 +211,11 @@ public interface JsonContext { void toJson(Object value, Writer writer) throws JsonIOException; /** - * Write the bean or collection to the JsonGenerator. + * Write the bean or collection to the JsonWriter. * * @throws JsonIOException When IOException occurs */ - void toJson(Object value, JsonGenerator generator) throws JsonIOException; + void toJson(Object value, io.avaje.json.JsonWriter generator) throws JsonIOException; /** * Return the bean or collection as JSON string using FetchPath. @@ -231,15 +230,15 @@ public interface JsonContext { void toJson(Object value, Writer writer, FetchPath fetchPath) throws JsonIOException; /** - * Write the bean or collection to the JsonGenerator using the FetchPath. + * Write the bean or collection to the JsonWriter using the FetchPath. */ - void toJson(Object value, JsonGenerator generator, FetchPath fetchPath) throws JsonIOException; + void toJson(Object value, io.avaje.json.JsonWriter generator, FetchPath fetchPath) throws JsonIOException; /** * Deprecated in favour of using PathProperties by itself. - * Write json to the JsonGenerator using the JsonWriteOptions. + * Write json to the JsonWriter using the JsonWriteOptions. */ - void toJson(Object value, JsonGenerator generator, JsonWriteOptions options) throws JsonIOException; + void toJson(Object value, io.avaje.json.JsonWriter generator, JsonWriteOptions options) throws JsonIOException; /** * Deprecated in favour of using PathProperties by itself. @@ -264,27 +263,27 @@ public interface JsonContext { boolean isSupportedType(Type genericType); /** - * Create and return a new JsonGenerator for the given writer. + * Create and return a new JsonWriter for the given writer. * * @throws JsonIOException When IOException occurs */ - JsonGenerator createGenerator(Writer writer) throws JsonIOException; + io.avaje.json.JsonWriter createGenerator(Writer writer) throws JsonIOException; /** - * Create and return a new JsonParser for the given reader. + * Create and return a new JsonReader for the given reader. * * @throws JsonIOException When IOException occurs */ - JsonParser createParser(Reader reader) throws JsonIOException; + JsonReader createParser(Reader reader) throws JsonIOException; /** - * Write a scalar types known to Ebean to Jackson. + * Write scalar types known to Ebean to JsonWriter. *

* Ebean has built in support for java8 and Joda types as well as the other * standard JDK types like URI, URL, UUID etc. This is a fast simple way to - * write any of those types to Jackson. + * write any of those types. *

*/ - void writeScalar(JsonGenerator generator, Object scalarValue) throws IOException; + void writeScalar(io.avaje.json.JsonWriter generator, Object scalarValue) throws IOException; } diff --git a/ebean-api/src/main/java/io/ebean/text/json/JsonWriter.java b/ebean-api/src/main/java/io/ebean/text/json/JsonWriter.java index baed9a9cb4..fd895e9a90 100644 --- a/ebean-api/src/main/java/io/ebean/text/json/JsonWriter.java +++ b/ebean-api/src/main/java/io/ebean/text/json/JsonWriter.java @@ -1,19 +1,17 @@ package io.ebean.text.json; -import com.fasterxml.jackson.core.JsonGenerator; - import java.io.InputStream; import java.math.BigDecimal; /** - * Wraps an underlying JsonGenerator taking into account null suppression and exposing isIncludeEmpty() etc. + * Wraps an underlying JsonWriter taking into account null suppression and exposing isIncludeEmpty() etc. */ public interface JsonWriter { /** - * Return the Jackson core JsonGenerator. + * Return the underlying JsonWriter. */ - JsonGenerator gen(); + io.avaje.json.JsonWriter gen(); /** * Return true if null values should be included in JSON output. diff --git a/ebean-api/src/main/java/module-info.java b/ebean-api/src/main/java/module-info.java index 1768da2780..abdc78ff84 100644 --- a/ebean-api/src/main/java/module-info.java +++ b/ebean-api/src/main/java/module-info.java @@ -8,6 +8,7 @@ requires transitive java.sql; requires transitive io.avaje.config; + requires transitive io.avaje.json; requires transitive org.jspecify; requires transitive jakarta.persistence.api; requires transitive io.ebean.annotation; @@ -16,7 +17,6 @@ requires static org.slf4j; requires static io.ebean.types; - requires static com.fasterxml.jackson.core; requires static com.fasterxml.jackson.databind; exports io.ebean; diff --git a/ebean-core-json/pom.xml b/ebean-core-json/pom.xml index 69732b20fe..6af5fb9d06 100644 --- a/ebean-core-json/pom.xml +++ b/ebean-core-json/pom.xml @@ -19,12 +19,10 @@ 16.11.0 - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - true + io.avaje + avaje-json-core + ${avaje-json-core.version} 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 f286bae4df..cbec38cd56 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 @@ -1,190 +1,129 @@ package io.ebeaninternal.json; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonReader.Token; +import io.avaje.json.JsonWriter; import io.ebean.service.SpiJsonService; import java.io.IOException; import java.io.Reader; import java.io.Writer; -import java.util.*; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; /** * Utility that converts between JSON content and simple java Maps/Lists. */ public final class DJsonService implements SpiJsonService { - /** - * Write the nested Map/List as json. - */ @Override public String write(Object object) throws IOException { return EJsonWriter.write(object); } - /** - * Write the nested Map/List as json to the writer. - */ @Override public void write(Object object, Writer writer) throws IOException { EJsonWriter.write(object, writer); } - /** - * Write the nested Map/List as json to the jsonGenerator. - */ @Override - public void write(Object object, JsonGenerator jsonGenerator) throws IOException { - EJsonWriter.write(object, jsonGenerator); + public void write(Object object, JsonWriter jsonWriter) throws IOException { + EJsonWriter.write(object, jsonWriter); } - /** - * Write the collection as json array to the jsonGenerator. - */ @Override - public void writeCollection(Collection collection, JsonGenerator jsonGenerator) throws IOException { - EJsonWriter.writeCollection(collection, jsonGenerator); + public void writeCollection(Collection collection, JsonWriter jsonWriter) throws IOException { + EJsonWriter.writeCollection(collection, jsonWriter); } - /** - * Parse the json and return as a Map additionally specifying if the returned map should be modify - * aware meaning that it can detect when it has been modified. - */ @Override public Map parseObject(String json, boolean modifyAware) throws IOException { return EJsonReader.parseObject(json, modifyAware); } - /** - * Parse the json and return as a Map. - */ @Override public Map parseObject(String json) throws IOException { return EJsonReader.parseObject(json); } - /** - * Parse the json and return as a Map taking a reader. - */ @Override public Map parseObject(Reader reader, boolean modifyAware) throws IOException { return EJsonReader.parseObject(reader, modifyAware); } - /** - * Parse the json and return as a Map taking a reader. - */ @Override public Map parseObject(Reader reader) throws IOException { return EJsonReader.parseObject(reader); } - /** - * Parse the json and return as a Map taking a JsonParser. - */ @Override - public Map parseObject(JsonParser parser) throws IOException { + public Map parseObject(JsonReader parser) throws IOException { return EJsonReader.parseObject(parser); } - /** - * Parse the json and return as a Map taking a JsonParser and a starting token. - * - *

Used when the first token is checked to see if the value is null prior to calling this. - */ @Override - public Map parseObject(JsonParser parser, JsonToken token) throws IOException { + public Map parseObject(JsonReader parser, Token token) throws IOException { return EJsonReader.parseObject(parser, token); } - /** - * Parse the json and return as a modify aware List. - */ @Override public List parseList(String json, boolean modifyAware) throws IOException { return EJsonReader.parseList(json, modifyAware); } - /** - * Parse the json and return as a List. - */ @Override public List parseList(String json) throws IOException { return EJsonReader.parseList(json); } - /** - * Parse the json and return as a List taking a Reader. - */ @Override public List parseList(Reader reader) throws IOException { return EJsonReader.parseList(reader); } - /** - * Parse the json and return as a List taking a JsonParser. - */ @Override - public List parseList(JsonParser parser) throws IOException { + public List parseList(JsonReader parser) throws IOException { return EJsonReader.parseList(parser, false); } - /** - * Parse the json returning as a List taking into account the current token. - */ @Override @SuppressWarnings("unchecked") - public List parseList(JsonParser parser, JsonToken currentToken) throws IOException { + public List parseList(JsonReader parser, Token currentToken) throws IOException { return (List) EJsonReader.parse(parser, currentToken, false); } - /** - * Parse the json and return as a List or Map. - */ @Override public Object parse(String json) throws IOException { return EJsonReader.parse(json); } - /** - * Parse the json and return as a List or Map. - */ @Override public Object parse(Reader reader) throws IOException { return EJsonReader.parse(reader); } - /** - * Parse the json and return as a List or Map. - */ @Override - public Object parse(JsonParser parser) throws IOException { + public Object parse(JsonReader parser) throws IOException { return EJsonReader.parse(parser); } - /** - * Parse the json returning a Set that might be modify aware. - */ @Override public Set parseSet(String json, boolean modifyAware) throws IOException { List list = parseList(json, modifyAware); if (list == null) { return null; } - if (modifyAware) { return ((ModifyAwareList) list).asSet(); - } else { - return new LinkedHashSet<>(list); } + return new LinkedHashSet<>(list); } - /** - * Parse the json returning as a Set taking into account the current token. - */ @Override - public Set parseSet(JsonParser parser, JsonToken currentToken) throws IOException { + public Set parseSet(JsonReader parser, Token currentToken) throws IOException { return new LinkedHashSet<>(parseList(parser, currentToken)); } } 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 index 7c808979f9..1d40f8f249 100644 --- a/ebean-core-json/src/main/java/io/ebeaninternal/json/EJsonReader.java +++ b/ebean-core-json/src/main/java/io/ebeaninternal/json/EJsonReader.java @@ -1,29 +1,25 @@ package io.ebeaninternal.json; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; +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.io.StringReader; -import java.util.*; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; final class EJsonReader { - static final JsonFactory json = new JsonFactory(); - private final JsonParser parser; - private final boolean modifyAware; - private final ModifyAwareFlag modifyAwareOwner; - private int depth; - private Stack stack; - private Context currentContext; + private EJsonReader() { + } - EJsonReader(JsonParser parser, boolean modifyAware) { - this.parser = parser; - this.modifyAware = modifyAware; - this.modifyAwareOwner = modifyAware ? new ModifyAwareFlag() : null; + private static JsonReader reader(String content) { + return JsonStream.builder().build().reader(content); } @SuppressWarnings("unchecked") @@ -47,12 +43,12 @@ static Map parseObject(Reader reader, boolean modifyAware) throw } @SuppressWarnings("unchecked") - static Map parseObject(JsonParser parser) throws IOException { + static Map parseObject(JsonReader parser) throws IOException { return (Map) parse(parser); } @SuppressWarnings("unchecked") - static Map parseObject(JsonParser parser, JsonToken token) throws IOException { + static Map parseObject(JsonReader parser, Token token) throws IOException { return (Map) parse(parser, token, false); } @@ -72,285 +68,137 @@ static List parseList(Reader reader) throws IOException { } @SuppressWarnings("unchecked") - static List parseList(JsonParser parser, boolean modifyAware) throws IOException { - return (List) parse(parser, modifyAware); + static List parseList(JsonReader parser, boolean modifyAware) throws IOException { + return (List) parse(parser, null, modifyAware); } static Object parse(String json) throws IOException { - if (json == null) { - return null; - } - return parse(new StringReader(json)); + return parseRawJson(json, null); } static Object parse(String json, boolean modifyAware) throws IOException { - if (json == null) { - return null; - } - return parse(new StringReader(json), modifyAware); + return parseRawJson(json, modifyAware ? new ModifyAwareFlag() : null); } static Object parse(Reader reader) throws IOException { - return parse(json.createParser(reader)); + return parseRawJson(readAll(reader), null); } static Object parse(Reader reader, boolean modifyAware) throws IOException { - return parse(json.createParser(reader), modifyAware); + return parseRawJson(readAll(reader), modifyAware ? new ModifyAwareFlag() : null); } - static Object parse(JsonParser parser) throws IOException { + static Object parse(JsonReader parser) throws IOException { return parse(parser, null, false); } - static Object parse(JsonParser parser, boolean modifyAware) throws IOException { + static Object parse(JsonReader parser, boolean modifyAware) throws IOException { return parse(parser, null, modifyAware); } - static Object parse(JsonParser parser, JsonToken token, boolean modifyAware) throws IOException { - return new EJsonReader(parser, modifyAware).parseJson(token); - } - - private void startArray() { - depth++; - stack.push(currentContext); - currentContext = modifyAware ? new ArrayContext(modifyAwareOwner) : new ArrayContext(); - } - - private void startObject() { - depth++; - stack.push(currentContext); - currentContext = modifyAware ? new ObjectContext(modifyAwareOwner) : new ObjectContext(); - } - - private void endArray() { - end(); - } - - private void endObject() { - end(); - } - - private void end() { - depth--; - if (!stack.isEmpty()) { - currentContext = stack.pop(currentContext); - } - if (modifyAwareOwner != null) { - modifyAwareOwner.setMarkedDirty(false); + 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 void setValue(Object value) { - currentContext.setValue(value); - } - - private void setValueNull() { - currentContext.setValueNull(); - } - - private Object parseJson(JsonToken token) throws IOException { - + private static Object parseValue(JsonReader parser, Token token, ModifyAwareType owner) throws IOException { if (token == null) { - token = parser.nextToken(); - // if it is a simple value just return it - switch (token) { - case VALUE_NULL: + token = parser.currentToken(); + if (token == null) { + if (parser.isNullValue()) { return null; - case VALUE_FALSE: - return Boolean.FALSE; - case VALUE_TRUE: - return Boolean.TRUE; - case VALUE_STRING: - return parser.getText(); - case VALUE_NUMBER_INT: - return parser.getLongValue(); - case VALUE_NUMBER_FLOAT: - return parser.getDecimalValue(); + } + return parseRawJson(parser.readRaw(), owner); } } - - // it is a object or array, process the first JsonToken - stack = new Stack(); - processJsonToken(token); - - // process the rest of the object or array - while (depth > 0) { - token = parser.nextToken(); - processJsonToken(token); + if (token == Token.BEGIN_OBJECT) { + return parseObjectValue(parser, owner); } - - return currentContext.getValue(); - } - - /** - * Process the JsonToken for objects and arrays. - */ - private void processJsonToken(JsonToken token) throws IOException { - switch (token) { - case START_ARRAY: - startArray(); - break; - - case START_OBJECT: - startObject(); - break; - - case FIELD_NAME: - currentContext.setKey(parser.getCurrentName()); - break; - - case VALUE_STRING: - setValue(parser.getValueAsString()); - break; - - case VALUE_NUMBER_INT: - setValue(parser.getLongValue()); - break; - - case VALUE_NUMBER_FLOAT: - setValue(parser.getDecimalValue()); - break; - - case VALUE_TRUE: - setValue(Boolean.TRUE); - break; - - case VALUE_FALSE: - setValue(Boolean.FALSE); - break; - - case VALUE_NULL: - setValueNull(); - break; - - case END_OBJECT: - endObject(); - break; - - case END_ARRAY: - endArray(); - break; - - default: - break; + if (token == Token.BEGIN_ARRAY) { + return parseArrayValue(parser, owner); } - } - - private static final class Stack { - - private Context head; - - private void push(Context context) { - if (context != null) { - context.next = head; - head = context; - } + if (token == Token.NUMBER) { + BigDecimal value = parser.readDecimal(); + return value.scale() <= 0 ? value.longValue() : value; } - - private Context pop(Context endingContext) { - if (head == null) { - throw new NoSuchElementException(); - } - Context temp = head; - head = head.next; - temp.popContext(endingContext); - return temp; + if (token == Token.STRING) { + return parser.readString(); } - - private boolean isEmpty() { - return head == null; + if (token == Token.BOOLEAN) { + return parser.readBoolean(); } - } - - private abstract static class Context { - Context next; - - abstract void popContext(Context temp); - - abstract Object getValue(); - - abstract void setValue(Object value); - - abstract void setKey(String key); - - abstract void setValueNull(); - } - - private static class ObjectContext extends Context { - - private final Map map; - - private String key; - - ObjectContext() { - map = new LinkedHashMap<>(); - } - - ObjectContext(ModifyAwareType owner) { - map = new ModifyAwareMap<>(owner, new LinkedHashMap<>()); - } - - @Override - public void popContext(Context temp) { - setValue(temp.getValue()); - } - - @Override - Object getValue() { - return map; - } - - @Override - void setValue(Object value) { - map.put(key, value); + if (token == Token.NULL) { + parser.isNullValue(); + return null; } + return parseRawJson(parser.readRaw(), owner); + } - @Override - void setKey(String key) { - this.key = key; + private static Object parseRawJson(String json, ModifyAwareType owner) throws IOException { + if (json == null) { + return null; } - - @Override - void setValueNull() { - map.put(key, 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 class ArrayContext extends Context { - - private final List values; - - ArrayContext() { - values = new ArrayList<>(); + 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(); + } - ArrayContext(ModifyAwareType owner) { - values = new ModifyAwareList<>(owner, new ArrayList<>()); - } + private static Map parseObjectValue(JsonReader parser, ModifyAwareType owner) throws IOException { + Map map = owner == null + ? new LinkedHashMap<>() + : new ModifyAwareMap<>(owner, new LinkedHashMap<>()); - @Override - public void popContext(Context temp) { - values.add(temp.getValue()); + parser.beginObject(); + while (parser.hasNextField()) { + String fieldName = parser.nextField(); + map.put(fieldName, parseValue(parser, parser.currentToken(), owner)); + if (owner != null) { + owner.setMarkedDirty(false); + } } - - @Override - Object getValue() { - return values; + parser.endObject(); + if (owner != null) { + owner.setMarkedDirty(false); } + return map; + } - @Override - void setValue(Object value) { - values.add(value); - } + private static List parseArrayValue(JsonReader parser, ModifyAwareType owner) throws IOException { + List list = owner == null + ? new ArrayList<>() + : new ModifyAwareList<>(owner, new ArrayList<>()); - @Override - void setValueNull() { - // ignore + parser.beginArray(); + while (parser.hasNextElement()) { + Token elementToken = parser.currentToken(); + Object elementValue = parseValue(parser, elementToken, owner); + list.add(elementValue); + if (owner != null) { + owner.setMarkedDirty(false); + } } - - @Override - void setKey(String key) { - // not expected + 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 index ff73de8f99..46c7f44349 100644 --- a/ebean-core-json/src/main/java/io/ebeaninternal/json/EJsonWriter.java +++ b/ebean-core-json/src/main/java/io/ebeaninternal/json/EJsonWriter.java @@ -1,212 +1,149 @@ package io.ebeaninternal.json; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; +import io.avaje.json.stream.JsonStream; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.math.BigDecimal; -import java.math.BigInteger; import java.util.Collection; -import java.util.Date; +import java.util.Iterator; import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; +/** + * Utility to write simple java Maps/Lists as JSON. + */ final class EJsonWriter { - /** - * Base jsonFactory implementation used when it is not passed in. - */ - static final JsonFactory jsonFactory = new JsonFactory(); - private final JsonGenerator jsonGenerator; + private static final JsonStream JSON_STREAM = JsonStream.builder().build(); - private EJsonWriter(JsonGenerator jsonGenerator) { - this.jsonGenerator = jsonGenerator; + private EJsonWriter() { } + /** + * Convert object to Json content. + */ static String write(Object object) throws IOException { - StringWriter writer = new StringWriter(200); - write(object, writer).close(); + StringWriter writer = new StringWriter(); + write(object, writer); return writer.toString(); } - static JsonGenerator write(Object object, Writer writer) throws IOException { - JsonGenerator generator = jsonFactory.createGenerator(writer); - write(object, generator); - generator.flush(); - return generator; - } - - static void write(Object object, JsonGenerator jsonGenerator) { - new EJsonWriter(jsonGenerator).writeJson(object); - } - - static void writeCollection(Collection collection, JsonGenerator jsonGenerator) throws IOException { - new EJsonWriter(jsonGenerator).writeCollection(null, collection); - } - - private void writeJson(Object object) { - writeJson(null, object); + /** + * 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; } - @SuppressWarnings("unchecked") - private void writeJson(String name, Object object) { - try { - if (object == null) { - writeNull(name); - - } else if (object instanceof Number) { - writeNumber(name, (Number) object); - - } else if (object instanceof String) { - writeString(name, (String) object); - - } else if (object instanceof Map) { - writeMap(name, (Map) object); - - } else if (object instanceof Collection) { - writeCollection(name, (Collection) object); - - } else if (object instanceof Boolean) { - writeBoolean(name, (Boolean) object); - - } else if (object instanceof Date) { - writeDate(name, (Date) object); - - } else if (object instanceof Map.Entry) { - Map.Entry entry = (Map.Entry) object; - writeJson(entry.getKey().toString(), entry.getValue()); - - } else { - writeString(name, object.toString()); - } - - } catch (IOException e) { - throw new RuntimeException(e); + /** + * Convert object to Json content. + */ + static void write(Object object, io.avaje.json.JsonWriter jsonWriter) throws IOException { + if (object == null) { + jsonWriter.nullValue(); + return; } - } - - private void writeBoolean(String name, Boolean object) throws IOException { - if (name == null) { - jsonGenerator.writeBoolean(object); - } else { - jsonGenerator.writeBooleanField(name, object); + if (object instanceof String) { + jsonWriter.value((String) object); + return; } - } - - private void writeDate(String name, Date object) throws IOException { - if (name == null) { - jsonGenerator.writeNumber(object.getTime()); - } else { - jsonGenerator.writeNumberField(name, object.getTime()); + if (object instanceof Integer) { + jsonWriter.value((Integer) object); + return; } - } - - private void writeNumber(String name, Number object) throws IOException { - if (object instanceof Long) { - writeLong(name, object); - - } else if (object instanceof Integer) { - writeInteger(name, object); - - } else if (object instanceof Double) { - writeDouble(name, object); - - } else if (object instanceof BigDecimal) { - writeBigDecimal(name, object); - - } else if (object instanceof BigInteger) { - writeBigInteger(name, object); - - } else { - writeGeneralNumber(name, object); + jsonWriter.value((Long) object); + return; } - } - - private void writeGeneralNumber(String name, Number object) throws IOException { - writeBigDecimal(name, new BigDecimal(object.toString())); - } - - private void writeBigDecimal(String name, Number object) throws IOException { - if (name == null) { - jsonGenerator.writeNumber((BigDecimal) object); - } else { - jsonGenerator.writeNumberField(name, (BigDecimal) object); + if (object instanceof Double) { + jsonWriter.value((Double) object); + return; } - } - - private void writeBigInteger(String name, Number object) throws IOException { - if (name == null) { - jsonGenerator.writeNumber((BigInteger) object); - } else { - jsonGenerator.writeNumberField(name, object.longValue()); + if (object instanceof Float) { + jsonWriter.value((Float) object); + return; } - } - - private void writeDouble(String name, Number object) throws IOException { - if (name == null) { - jsonGenerator.writeNumber((Double) object); - } else { - jsonGenerator.writeNumberField(name, (Double) object); + if (object instanceof BigDecimal) { + jsonWriter.value((BigDecimal) object); + return; } - } - - private void writeLong(String name, Number object) throws IOException { - if (name == null) { - jsonGenerator.writeNumber((Long) object); - } else { - jsonGenerator.writeNumberField(name, (Long) object); + if (object instanceof Boolean) { + jsonWriter.value((Boolean) object); + return; } - } - - private void writeInteger(String name, Number object) throws IOException { - if (name == null) { - jsonGenerator.writeNumber((Integer) object); - } else { - jsonGenerator.writeNumberField(name, (Integer) object); + if (object instanceof Map) { + writeMap((Map) object, jsonWriter); + return; + } + if (object instanceof Collection) { + writeCollection((Collection) object, jsonWriter); + return; } + + jsonWriter.value(object.toString()); } - private void writeNull(String name) throws IOException { - if (name == null) { - jsonGenerator.writeNull(); - } else { - jsonGenerator.writeNullField(name); + /** + * 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(); } - private void writeString(String name, String object) throws IOException { - if (name == null) { - jsonGenerator.writeString(object); - } else { - jsonGenerator.writeStringField(name, object); + /** + * 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(); } - private void writeCollection(String name, Collection collection) throws IOException { - if (name != null) { - jsonGenerator.writeFieldName(name); - } - jsonGenerator.writeStartArray(); - for (Object object : collection) { - writeJson(null, object); - } - jsonGenerator.writeEndArray(); + /** + * Convert map to Json content. + */ + static String write(Map map) throws IOException { + StringWriter writer = new StringWriter(); + write(map, writer); + return writer.toString(); } - private void writeMap(String name, Map map) throws IOException { + /** + * 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; + } - if (name != null) { - jsonGenerator.writeFieldName(name); - } - jsonGenerator.writeStartObject(); - Set> entrySet = map.entrySet(); - for (Entry entry : entrySet) { - writeJson(entry.getKey().toString(), entry.getValue()); + /** + * 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); } - jsonGenerator.writeEndObject(); + jsonWriter.endObject(); } } diff --git a/ebean-core-json/src/main/java/module-info.java b/ebean-core-json/src/main/java/module-info.java index b7561ed62c..0845699bdf 100644 --- a/ebean-core-json/src/main/java/module-info.java +++ b/ebean-core-json/src/main/java/module-info.java @@ -1,8 +1,7 @@ module io.ebean.core.json { requires io.ebean.api; - - requires transitive com.fasterxml.jackson.core; + requires transitive io.avaje.json; exports io.ebeaninternal.json to io.ebean.test, io.ebean.core; provides io.ebean.service.BootstrapService with io.ebeaninternal.json.DJsonService; diff --git a/ebean-core-type/pom.xml b/ebean-core-type/pom.xml index 2adbb33dfc..0cebee48b5 100644 --- a/ebean-core-type/pom.xml +++ b/ebean-core-type/pom.xml @@ -20,10 +20,9 @@ - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - true + io.avaje + avaje-json-core + ${avaje-json-core.version} diff --git a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java index 9047befb09..31f49b7ad8 100644 --- a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java +++ b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java @@ -1,7 +1,7 @@ package io.ebean.core.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.text.StringFormatter; import io.ebean.text.StringParser; @@ -177,13 +177,44 @@ default long asVersion(T value) { void writeData(DataOutput dataOutput, T value) throws IOException; /** - * Read the value from JsonParser. + * Read the value from JsonReader. */ - T jsonRead(JsonParser parser) throws IOException; + default T jsonRead(JsonReader parser) throws IOException { + JsonReader.Token token = parser.currentToken(); + if (token == JsonReader.Token.NULL) { + parser.isNullValue(); + return null; + } + if (token == JsonReader.Token.STRING) { + return parse(parser.readString()); + } + return parse(parser.readRaw()); + } /** - * Write the value to the JsonGenerator. - */ - void jsonWrite(JsonGenerator writer, T value) throws IOException; + * Write the value to the JsonWriter. + */ + default void jsonWrite(JsonWriter writer, T value) throws IOException { + if (value == null) { + writer.nullValue(); + return; + } + String formatted = formatValue(value); + if (formatted == null) { + writer.nullValue(); + return; + } + DocPropertyType docType = docType(); + if (docType == DocPropertyType.OBJECT || docType == DocPropertyType.LIST || docType == DocPropertyType.ROOT || likelyRawJson(formatted)) { + writer.rawValue(formatted); + } else { + writer.value(formatted); + } + } + + private static boolean likelyRawJson(String formatted) { + String trimmed = formatted.trim(); + return !trimmed.isEmpty() && (trimmed.charAt(0) == '{' || trimmed.charAt(0) == '['); + } } diff --git a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarTypeBaseDate.java b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarTypeBaseDate.java index 5d76cfade3..66550a4ed3 100644 --- a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarTypeBaseDate.java +++ b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarTypeBaseDate.java @@ -1,8 +1,8 @@ package io.ebean.core.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonReader.Token; +import io.avaje.json.JsonWriter; import io.ebean.config.JsonConfig; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; @@ -83,20 +83,31 @@ public T convertFromMillis(long systemTimeMillis) { } @Override - public T jsonRead(JsonParser parser) throws IOException { - if (JsonToken.VALUE_NUMBER_INT == parser.getCurrentToken()) { - return convertFromMillis(parser.getLongValue()); - } else { - return convertFromDate(Date.valueOf(parser.getText())); + public T jsonRead(JsonReader parser) throws IOException { + Token token = parser.currentToken(); + if (Token.NUMBER == token) { + return convertFromMillis(parser.readLong()); + } + if (Token.STRING == token) { + return convertFromDate(Date.valueOf(parser.readString())); + } + + String raw = parser.readRaw(); + if (raw == null || "null".equals(raw)) { + return null; + } + if (raw.length() > 1 && raw.charAt(0) == '"' && raw.charAt(raw.length() - 1) == '"') { + return convertFromDate(Date.valueOf(raw.substring(1, raw.length() - 1))); } + return convertFromMillis(Long.parseLong(raw)); } @Override - public void jsonWrite(JsonGenerator writer, T value) throws IOException { + public void jsonWrite(JsonWriter writer, T value) throws IOException { if (mode == JsonConfig.Date.ISO8601) { - writer.writeString(toIsoFormat(value)); + writer.value(toIsoFormat(value)); } else { - writer.writeNumber(convertToMillis(value)); + writer.value(convertToMillis(value)); } } diff --git a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarTypeBaseDateTime.java b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarTypeBaseDateTime.java index 0ee2cb82f4..95e7a9a89f 100644 --- a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarTypeBaseDateTime.java +++ b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarTypeBaseDateTime.java @@ -1,7 +1,8 @@ package io.ebean.core.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonReader.Token; +import io.avaje.json.JsonWriter; import io.ebean.config.JsonConfig; import java.io.DataInput; @@ -99,35 +100,53 @@ protected String toJsonNanos(long epochSecs, int nanos) { } @Override - public T jsonRead(JsonParser parser) throws IOException { - switch (parser.getCurrentToken()) { - case VALUE_NUMBER_INT: { - return convertFromMillis(parser.getLongValue()); - } - case VALUE_NUMBER_FLOAT: { - BigDecimal value = parser.getDecimalValue(); - Timestamp timestamp = ScalarTypeUtils.toTimestamp(value); - return convertFromTimestamp(timestamp); - } - default: { - return fromJsonISO8601(parser.getText()); - } + public T jsonRead(JsonReader parser) throws IOException { + Token token = parser.currentToken(); + if (token == Token.NUMBER) { + return readNumber(parser.readDecimal()); + } + if (token == Token.STRING) { + return fromStringValue(parser.readString()); + } + + String raw = parser.readRaw(); + if (raw == null || "null".equals(raw)) { + return null; + } + if (raw.length() > 1 && raw.charAt(0) == '"' && raw.charAt(raw.length() - 1) == '"') { + return fromStringValue(raw.substring(1, raw.length() - 1)); + } + return readNumber(new BigDecimal(raw)); + } + + private T fromStringValue(String value) { + if (value.indexOf('-') == -1 && Character.isDigit(value.charAt(0))) { + return readNumber(new BigDecimal(value)); + } + return fromJsonISO8601(value); + } + + private T readNumber(BigDecimal value) { + if (value.scale() <= 0) { + return convertFromMillis(value.longValue()); } + Timestamp timestamp = ScalarTypeUtils.toTimestamp(value); + return convertFromTimestamp(timestamp); } @Override - public void jsonWrite(JsonGenerator writer, T value) throws IOException { + public void jsonWrite(JsonWriter writer, T value) throws IOException { switch (mode) { case ISO8601: { - writer.writeString(toJsonISO8601(value)); + writer.value(toJsonISO8601(value)); break; } case NANOS: { - writer.writeNumber(toJsonNanos(value)); + writer.value(toJsonNanos(value)); break; } default: { - writer.writeNumber(convertToMillis(value)); + writer.value(convertToMillis(value)); } } } diff --git a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarTypeBaseVarchar.java b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarTypeBaseVarchar.java index 99c83561b0..f0646ee134 100644 --- a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarTypeBaseVarchar.java +++ b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarTypeBaseVarchar.java @@ -1,7 +1,7 @@ package io.ebean.core.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import java.io.DataInput; import java.io.DataOutput; @@ -104,13 +104,16 @@ public void writeData(DataOutput dataOutput, T value) throws IOException { } @Override - public T jsonRead(JsonParser parser) throws IOException { - return parse(parser.getValueAsString()); + public T jsonRead(JsonReader parser) throws IOException { + if (parser.isNullValue()) { + return null; + } + return parse(parser.readString()); } @Override - public void jsonWrite(JsonGenerator writer, T value) throws IOException { - writer.writeString(format(value)); + public void jsonWrite(JsonWriter writer, T value) throws IOException { + writer.value(format(value)); } @Override diff --git a/ebean-core-type/src/main/java/module-info.java b/ebean-core-type/src/main/java/module-info.java index 88be6d462d..9c48ef2951 100644 --- a/ebean-core-type/src/main/java/module-info.java +++ b/ebean-core-type/src/main/java/module-info.java @@ -4,8 +4,7 @@ requires transitive java.sql; requires transitive io.ebean.api; + requires transitive io.avaje.json; requires static org.postgresql.jdbc; - requires static com.fasterxml.jackson.core; - } diff --git a/ebean-core/pom.xml b/ebean-core/pom.xml index d993d56a89..33094da198 100644 --- a/ebean-core/pom.xml +++ b/ebean-core/pom.xml @@ -138,14 +138,6 @@ - - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - true - - com.fasterxml.jackson.core jackson-databind diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiJsonContext.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiJsonContext.java index 2700ed60e4..a868c17b99 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiJsonContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiJsonContext.java @@ -1,6 +1,6 @@ package io.ebeaninternal.api; -import com.fasterxml.jackson.core.JsonGenerator; +import io.avaje.json.JsonWriter; import io.ebean.plugin.BeanType; import io.ebean.text.json.JsonContext; import io.ebean.text.json.JsonWriteOptions; @@ -17,7 +17,7 @@ public interface SpiJsonContext extends JsonContext { /** * Create a Json Writer for writing beans as JSON. */ - SpiJsonWriter createJsonWriter(JsonGenerator gen, JsonWriteOptions options); + SpiJsonWriter createJsonWriter(JsonWriter gen, JsonWriteOptions options); /** * Create a Json Writer for writing beans as JSON supplying a writer. diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java b/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java index 613b105cc6..125c933d9b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java @@ -1,8 +1,8 @@ package io.ebeaninternal.api.json; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.ObjectMapper; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonReader.Token; import io.ebean.bean.EntityBean; import io.ebean.bean.PersistenceContext; import io.ebeaninternal.server.deploy.BeanDescriptor; @@ -14,7 +14,9 @@ public interface SpiJsonReader { PersistenceContext persistenceContext(); - SpiJsonReader forJson(JsonParser moreJson); + SpiJsonReader forJson(JsonReader moreJson); + + SpiJsonReader forJson(String moreJson); void persistenceContextPut(Object beanId, T currentBean); @@ -22,9 +24,9 @@ public interface SpiJsonReader { ObjectMapper mapper(); - JsonParser parser(); + JsonReader parser(); - JsonToken nextToken() throws IOException; + Token nextToken() throws IOException; void pushPath(String path); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/changelog/ChangeJsonBuilder.java b/ebean-core/src/main/java/io/ebeaninternal/server/changelog/ChangeJsonBuilder.java index 5d71023b8b..dae1a11385 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/changelog/ChangeJsonBuilder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/changelog/ChangeJsonBuilder.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.changelog; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; +import io.avaje.json.JsonWriter; +import io.avaje.json.stream.JsonStream; import io.ebean.event.changelog.BeanChange; import io.ebean.event.changelog.ChangeSet; import io.ebean.event.changelog.ChangeType; @@ -15,13 +15,13 @@ */ final class ChangeJsonBuilder { - private final JsonFactory jsonFactory = new JsonFactory(); + private final JsonStream jsonStream = JsonStream.builder().build(); /** * Write the bean change as JSON. */ void writeBeanJson(Writer writer, BeanChange bean, ChangeSet changeSet) throws IOException { - try (JsonGenerator generator = jsonFactory.createGenerator(writer)) { + try (JsonWriter generator = jsonStream.writer(writer)) { writeBeanChange(generator, bean, changeSet); generator.flush(); } @@ -30,58 +30,67 @@ void writeBeanJson(Writer writer, BeanChange bean, ChangeSet changeSet) throws I /** * Write the bean change as JSON document containing the transaction header details. */ - private void writeBeanChange(JsonGenerator gen, BeanChange bean, ChangeSet changeSet) throws IOException { - gen.writeStartObject(); - gen.writeNumberField("ts", bean.getEventTime()); - gen.writeStringField("change", bean.getEvent().getCode()); - gen.writeStringField("type", bean.getType()); - gen.writeStringField("id", bean.getId().toString()); + private void writeBeanChange(JsonWriter gen, BeanChange bean, ChangeSet changeSet) { + gen.beginObject(); + gen.name("ts"); + gen.value(bean.getEventTime()); + gen.name("change"); + gen.value(bean.getEvent().getCode()); + gen.name("type"); + gen.value(bean.getType()); + gen.name("id"); + gen.value(bean.getId().toString()); if (bean.getTenantId() != null) { - gen.writeStringField("tenantId", bean.getTenantId().toString()); + gen.name("tenantId"); + gen.value(bean.getTenantId().toString()); } writeBeanTransactionDetails(gen, changeSet); writeBeanValues(gen, bean); - gen.writeEndObject(); + gen.endObject(); } /** * Denormalise by writing the transaction header details. */ - private void writeBeanTransactionDetails(JsonGenerator gen, ChangeSet changeSet) throws IOException { + private void writeBeanTransactionDetails(JsonWriter gen, ChangeSet changeSet) { String source = changeSet.getSource(); if (source != null) { - gen.writeStringField("source", source); + gen.name("source"); + gen.value(source); } String userId = changeSet.getUserId(); if (userId != null) { - gen.writeStringField("userId", userId); + gen.name("userId"); + gen.value(userId); } String userIpAddress = changeSet.getUserIpAddress(); if (userIpAddress != null) { - gen.writeStringField("userIpAddress", userIpAddress); + gen.name("userIpAddress"); + gen.value(userIpAddress); } Map userContext = changeSet.getUserContext(); if (userContext != null && !userContext.isEmpty()) { - gen.writeObjectFieldStart("userContext"); + gen.name("userContext"); + gen.beginObject(); for (Map.Entry entry : userContext.entrySet()) { - gen.writeStringField(entry.getKey(), entry.getValue()); + gen.name(entry.getKey()); + gen.value(entry.getValue()); } - gen.writeEndObject(); + gen.endObject(); } } /** * For insert and update write the new/old values. */ - private void writeBeanValues(JsonGenerator gen, BeanChange bean) throws IOException { + private void writeBeanValues(JsonWriter gen, BeanChange bean) { if (bean.getEvent() != ChangeType.DELETE) { - gen.writeFieldName("data"); - gen.writeRaw(":"); - gen.writeRaw(bean.getData()); + gen.name("data"); + gen.rawValue(bean.getData()); String oldData = bean.getOldData(); if (oldData != null) { - gen.writeRaw(",\"oldData\":"); - gen.writeRaw(oldData); + gen.name("oldData"); + gen.rawValue(oldData); } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java index e90716ccd9..158080b285 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java @@ -1,6 +1,6 @@ package io.ebeaninternal.server.core; -import com.fasterxml.jackson.core.JsonFactory; +import io.avaje.json.stream.JsonStream; import io.ebean.DatabaseBuilder; import io.ebean.ExpressionFactory; import io.ebean.annotation.Platform; @@ -89,7 +89,7 @@ public final class InternalConfiguration { private final boolean jacksonCorePresent; private final ExpressionFactory expressionFactory; private final SpiBackgroundExecutor backgroundExecutor; - private final JsonFactory jsonFactory; + private final JsonStream jsonStream; private final DocStoreFactory docStoreFactory; private final List plugins = new ArrayList<>(); private final MultiValueBind multiValueBind; @@ -108,7 +108,7 @@ public final class InternalConfiguration { this.tableModState = new TableModState(); this.logManager = initLogManager(); this.docStoreFactory = initDocStoreFactory(service(DocStoreFactory.class)); - this.jsonFactory = config.getJsonFactory(); + this.jsonStream = config.getJsonStream(); this.clusterManager = clusterManager; this.backgroundExecutor = backgroundExecutor; this.bootupClasses = bootupClasses; @@ -292,7 +292,7 @@ private MultiValueBind createMultiValueBind(Platform platform) { } SpiJsonContext createJsonContext(SpiEbeanServer server) { - return jacksonCorePresent ? new DJsonContext(server, jsonFactory, typeManager) : null; + return jacksonCorePresent ? new DJsonContext(server, jsonStream, typeManager) : null; } AutoTuneService createAutoTuneService(SpiEbeanServer server) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanChangeJson.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanChangeJson.java index 66b3faa45a..9fc447c767 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanChangeJson.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanChangeJson.java @@ -2,8 +2,11 @@ import io.ebean.PersistenceIOException; import io.ebean.bean.BeanDiffVisitor; +import io.ebean.config.JsonConfig; import io.ebeaninternal.api.json.SpiJsonWriter; +import io.ebeaninternal.server.json.WriteJson; import io.ebeaninternal.server.util.ArrayStack; +import io.avaje.json.stream.JsonStream; import java.io.IOException; import java.io.StringWriter; @@ -13,26 +16,18 @@ */ final class BeanChangeJson implements BeanDiffVisitor { - private final StringWriter newData; - private final StringWriter oldData; - private final SpiJsonWriter newJson; - private final SpiJsonWriter oldJson; + private final StringWriter data; + private final SpiJsonWriter json; + private final boolean writeNew; private final ArrayStack> stack = new ArrayStack<>(); private BeanDescriptor descriptor; - BeanChangeJson(BeanDescriptor descriptor, boolean statelessUpdate) { + BeanChangeJson(BeanDescriptor descriptor, boolean writeNew) { this.descriptor = descriptor; - this.newData = new StringWriter(200); - this.newJson = descriptor.createJsonWriter(newData); - newJson.writeStartObject(); - if (statelessUpdate) { - this.oldJson = null; - this.oldData = null; - } else { - this.oldData = new StringWriter(200); - this.oldJson = descriptor.createJsonWriter(oldData); - oldJson.writeStartObject(); - } + this.writeNew = writeNew; + this.data = new StringWriter(200); + this.json = new WriteJson(JsonStream.builder().build().writer(data), JsonConfig.Include.ALL); + json.writeStartObject(); } @Override @@ -40,10 +35,7 @@ public void visit(int position, Object newVal, Object oldVal) { try { BeanProperty prop = descriptor.propertiesIndex[position]; if (prop.isDbUpdatable()) { - prop.jsonWriteValue(newJson, newVal); - if (oldJson != null) { - prop.jsonWriteValue(oldJson, oldVal); - } + prop.jsonWriteValue(json, writeNew ? newVal : oldVal); } } catch (IOException e) { throw new PersistenceIOException(e); @@ -55,18 +47,12 @@ public void visitPush(int position) { stack.push(descriptor); BeanPropertyAssocOne embedded = (BeanPropertyAssocOne)descriptor.propertiesIndex[position]; descriptor = embedded.targetDescriptor(); - newJson.writeStartObject(embedded.name()); - if (oldJson != null) { - oldJson.writeStartObject(embedded.name()); - } + json.writeStartObject(embedded.name()); } @Override public void visitPop() { - newJson.writeEndObject(); - if (oldJson != null) { - oldJson.writeEndObject(); - } + json.writeEndObject(); descriptor = stack.pop(); } @@ -75,28 +61,14 @@ public void visitPop() { */ void flush() { try { - newJson.writeEndObject(); - newJson.flush(); - if (oldJson != null) { - oldJson.writeEndObject(); - oldJson.flush(); - } + json.writeEndObject(); + json.flush(); } catch (IOException e) { throw new PersistenceIOException(e); } } - /** - * Return the new values JSON. - */ - String newJson() { - return newData.toString(); - } - - /** - * Return the old values JSON. - */ - String oldJson() { - return oldData == null ? null : oldData.toString(); + String json() { + return data.toString(); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 5dfdd705bf..16da6b3078 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -792,10 +792,17 @@ private BeanChange deleteBeanChange(PersistRequestBean request) { */ private BeanChange updateBeanChange(PersistRequestBean request) { try { - BeanChangeJson changeJson = new BeanChangeJson(this, request.isStatelessUpdate()); - request.intercept().addDirtyPropertyValues(changeJson); - changeJson.flush(); - return beanChange(ChangeType.UPDATE, request.beanId(), changeJson.newJson(), changeJson.oldJson()); + BeanChangeJson newValues = new BeanChangeJson(this, true); + request.intercept().addDirtyPropertyValues(newValues); + newValues.flush(); + String oldData = null; + if (!request.isStatelessUpdate()) { + BeanChangeJson oldValues = new BeanChangeJson(this, false); + request.intercept().addDirtyPropertyValues(oldValues); + oldValues.flush(); + oldData = oldValues.json(); + } + return beanChange(ChangeType.UPDATE, request.beanId(), newValues.json(), oldData); } catch (RuntimeException e) { log.log(ERROR, "Failed to write ChangeLog entry for update", e); return null; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java index ecb283e5c2..04dbaec5e7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java @@ -1,6 +1,6 @@ package io.ebeaninternal.server.deploy; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; import io.ebean.bean.EntityBean; import io.ebean.core.type.ScalarType; import io.ebeaninternal.api.json.SpiJsonReader; @@ -49,25 +49,25 @@ public void jsonWriteMapEntry(SpiJsonWriter ctx, Map.Entry entry) throws I @Override public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) throws IOException { - JsonParser parser = readJson.parser(); + JsonReader parser = readJson.parser(); ElementCollector add = elementHelp.createCollector(); - do { - String fieldName = parser.nextFieldName(); - if (fieldName == null) { - break; - } + parser.beginObject(); + while (parser.hasNextField()) { + String fieldName = parser.nextField(); if (stringKey) { - parser.nextToken(); Object val = readJsonElement(readJson, null, null); // CHECKME: Update existing map entry here? add.addKeyValue(fieldName, val); } else { - parser.nextFieldName(); + parser.beginObject(); + parser.nextField(); Object key = scalarTypeKey.jsonRead(parser); - parser.nextFieldName(); + parser.nextField(); Object val = readJsonElement(readJson, null, null); // CHECKME: Update existing map entry here? + parser.endObject(); add.addKeyValue(key, val); } - } while (true); + } + parser.endObject(); return add.collection(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementScalar.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementScalar.java index 0242c993f2..d50b9c234f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementScalar.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementScalar.java @@ -1,7 +1,6 @@ package io.ebeaninternal.server.deploy; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; +import io.avaje.json.JsonReader; import io.ebean.PersistenceIOException; import io.ebean.SqlUpdate; import io.ebean.bean.EntityBean; @@ -40,15 +39,13 @@ public void jsonWriteElement(SpiJsonWriter ctx, Object element) { @Override public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) throws IOException { - JsonParser parser = readJson.parser(); + JsonReader parser = readJson.parser(); ElementCollector add = elementHelp.createCollector(); - do { - JsonToken token = parser.nextToken(); - if (JsonToken.VALUE_NULL == token || JsonToken.END_ARRAY == token) { - break; - } + parser.beginArray(); + while (parser.hasNextElement()) { add.addElement(scalarType.jsonRead(parser)); - } while (true); + } + parser.endArray(); return add.collection(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementScalarMap.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementScalarMap.java index d5b8d91a9a..4de87ebdb0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementScalarMap.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementScalarMap.java @@ -1,6 +1,6 @@ package io.ebeaninternal.server.deploy; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; import io.ebean.bean.EntityBean; import io.ebean.core.type.ScalarType; import io.ebeaninternal.api.json.SpiJsonReader; @@ -49,25 +49,25 @@ public void jsonWriteMapEntry(SpiJsonWriter ctx, Map.Entry entry) throws I @Override public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) throws IOException { - JsonParser parser = readJson.parser(); + JsonReader parser = readJson.parser(); ElementCollector add = elementHelp.createCollector(); - do { - String fieldName = parser.nextFieldName(); - if (fieldName == null) { - break; - } + parser.beginObject(); + while (parser.hasNextField()) { + String fieldName = parser.nextField(); if (stringKey) { - parser.nextToken(); Object val = scalarTypeVal.jsonRead(parser); add.addKeyValue(fieldName, val); } else { - parser.nextFieldName(); + parser.beginObject(); + parser.nextField(); Object key = scalarTypeKey.jsonRead(parser); - parser.nextFieldName(); + parser.nextField(); Object val = scalarTypeVal.jsonRead(parser); + parser.endObject(); add.addKeyValue(key, val); } - } while (true); + } + parser.endObject(); return add.collection(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java index 720ef92caa..46587cc898 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java @@ -1,10 +1,7 @@ package io.ebeaninternal.server.deploy; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonReader.Token; import io.ebean.bean.EntityBean; import io.ebean.text.json.EJson; import io.ebeaninternal.api.json.SpiJsonReader; @@ -34,7 +31,8 @@ void jsonWrite(SpiJsonWriter writeJson, EntityBean bean, String key) throws IOEx InheritInfo localInheritInfo = inheritInfo.readType(bean.getClass()); String discValue = localInheritInfo.getDiscriminatorStringValue(); String discColumn = localInheritInfo.getDiscriminatorColumn(); - writeJson.gen().writeStringField(discColumn, discValue); + writeJson.gen().name(discColumn); + writeJson.gen().value(discValue); localInheritInfo.desc().jsonWriteProperties(writeJson, bean); } writeJson.writeEndObject(); @@ -66,44 +64,38 @@ void jsonWriteDirtyProperties(SpiJsonWriter writeJson, EntityBean bean, boolean[ @SuppressWarnings("unchecked") T jsonRead(SpiJsonReader jsonRead, String path, boolean withInheritance, T target) throws IOException { - JsonParser parser = jsonRead.parser(); - //noinspection StatementWithEmptyBody - if (parser.getCurrentToken() == JsonToken.START_OBJECT) { - // start object token read by Jackson already - } else { - // check for null or start object - JsonToken token = parser.nextToken(); - if (JsonToken.VALUE_NULL == token || JsonToken.END_ARRAY == token) { - return null; - } - if (JsonToken.START_OBJECT != token) { - throw new JsonParseException(parser, "Unexpected token " + token + " - expecting start_object", parser.getCurrentLocation()); - } + JsonReader parser = jsonRead.parser(); + if (parser.isNullValue()) { + return null; + } + Token token = parser.currentToken(); + if (token != Token.BEGIN_OBJECT) { + throw new IllegalStateException("Unexpected token " + token + " - expecting BEGIN_OBJECT at: " + parser.location()); } if (desc.inheritInfo == null || !withInheritance) { return jsonReadObject(jsonRead, path, target); } - ObjectNode node = jsonRead.mapper().readTree(parser); - if (node.isNull()) { + Map node = EJson.parseObject(parser); + if (node == null) { return null; } - JsonParser newParser = node.traverse(); - SpiJsonReader newReader = jsonRead.forJson(newParser); // check for the discriminator value to determine the correct sub type String discColumn = inheritInfo.getRoot().getDiscriminatorColumn(); - JsonNode discNode = node.get(discColumn); - if (discNode == null || discNode.isNull()) { + Object discValue = node.get(discColumn); + String rawObject = EJson.write(node); + SpiJsonReader newReader = jsonRead.forJson(rawObject); + if (discValue == null) { if (!desc.isAbstractType()) { return desc.jsonReadObject(newReader, path, target); } String msg = "Error reading inheritance discriminator - expected [" + discColumn + "] but no json key?"; - throw new JsonParseException(newParser, msg, parser.getCurrentLocation()); + throw new IllegalStateException(msg); } - BeanDescriptor inheritDesc = (BeanDescriptor) inheritInfo.readType(discNode.asText()).desc(); + BeanDescriptor inheritDesc = (BeanDescriptor) inheritInfo.readType(String.valueOf(discValue)).desc(); return inheritDesc.jsonReadObject(newReader, path, target); } @@ -124,35 +116,30 @@ private T jsonReadProperties(SpiJsonReader readJson, EntityBean bean, String pat if (path != null) { readJson.pushPath(path); } + JsonReader parser = readJson.parser(); + parser.beginObject(); + // unmapped properties, send to JsonReadBeanVisitor later Map unmappedProperties = null; - do { - JsonParser parser = readJson.parser(); - JsonToken event = parser.nextToken(); - if (JsonToken.FIELD_NAME == event) { - String key = parser.getCurrentName(); - BeanProperty p = desc.beanProperty(key); - if (p != null) { - if (p.isVersion() && readJson.update() ) { - // skip version prop during update - p.jsonRead(readJson); - } else { - p.jsonRead(readJson, bean); - } + while (parser.hasNextField()) { + String key = parser.nextField(); + BeanProperty p = desc.beanProperty(key); + if (p != null) { + if (p.isVersion() && readJson.update()) { + // skip version prop during update + p.jsonRead(readJson); } else { - // read an unmapped property - if (unmappedProperties == null) { - unmappedProperties = new LinkedHashMap<>(); - } - unmappedProperties.put(key, EJson.parse(parser)); + p.jsonRead(readJson, bean); } - } else if (JsonToken.END_OBJECT == event) { - break; } else { - throw new RuntimeException("Unexpected token " + event + " - expecting key or end_object at: " + parser.getCurrentLocation()); + // read an unmapped property + if (unmappedProperties == null) { + unmappedProperties = new LinkedHashMap<>(); + } + unmappedProperties.put(key, EJson.parse(parser)); } - - } while (true); + } + parser.endObject(); if (unmappedProperties != null) { desc.setUnmappedJson(bean, unmappedProperties); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index 2c6443b77f..9e95796378 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -1,6 +1,7 @@ package io.ebeaninternal.server.deploy; -import com.fasterxml.jackson.core.JsonToken; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonReader.Token; import io.ebean.DataIntegrityException; import io.ebean.ModifyAwareType; import io.ebean.ValuePair; @@ -1437,14 +1438,14 @@ public void jsonRead(SpiJsonReader ctx, EntityBean bean) throws IOException { } public Object jsonRead(SpiJsonReader ctx) throws IOException { - JsonToken event = ctx.nextToken(); - if (JsonToken.VALUE_NULL == event) { + JsonReader parser = ctx.parser(); + if (parser.isNullValue()) { return null; } else { // expect to read non-null json value Object objValue; if (scalarType != null) { - objValue = scalarType.jsonRead(ctx.parser()); + objValue = scalarType.jsonRead(parser); } else { try { objValue = ctx.readValueUsingObjectMapper(propertyType); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index 2374b24ecb..6c82b9dc38 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -1,15 +1,14 @@ package io.ebeaninternal.server.deploy; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonReader.Token; import io.ebean.SqlUpdate; import io.ebean.Transaction; import io.ebean.bean.BeanCollection; import io.ebean.bean.BeanCollection.ModifyListenMode; import io.ebean.bean.BeanCollectionAdd; import io.ebean.bean.EntityBean; +import io.ebean.bean.EntityBeanIntercept; import io.ebean.bean.PersistenceContext; import io.ebean.plugin.PropertyAssocMany; import io.ebean.text.PathProperties; @@ -1043,9 +1042,8 @@ public String jsonWriteCollection(Object value) throws IOException { */ private Object jsonReadCollection(String json) throws IOException { SpiJsonReader ctx = descriptor.createJsonReader(json); - JsonParser parser = ctx.parser(); - JsonToken event = parser.nextToken(); - if (JsonToken.VALUE_NULL == event) { + JsonReader parser = ctx.parser(); + if (parser.isNullValue()) { return null; } return jsonReadCollection(ctx, null, null); @@ -1068,20 +1066,18 @@ public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean, BeanCollection collection = createEmpty(parentBean); BeanCollectionAdd add = beanCollectionAdd(collection); Map existingBeans = extractBeans(targets); - do { - + JsonReader parser = readJson.parser(); + parser.beginArray(); + while (parser.hasNextElement()) { EntityBean detailBean = getDetailBean(readJson, existingBeans); - if (detailBean == null) { - // read the entire array - break; - } - add.addEntityBean(detailBean); - - if (parentBean != null && childMasterProperty != null) { - // bind detail bean back to master via mappedBy property - childMasterProperty.setValue(detailBean, parentBean); + if (detailBean != null) { + add.addEntityBean(detailBean); + if (parentBean != null && childMasterProperty != null) { + // bind detail bean back to master via mappedBy property + childMasterProperty.setValue(detailBean, parentBean); + } } - } while (true); + } return collection; } @@ -1092,18 +1088,79 @@ private EntityBean getDetailBean(SpiJsonReader readJson, Map targets) BeanProperty idProperty = targetDescriptor.idProperty(); if (targets == null || idProperty == null) { return (EntityBean) targetDescriptor.jsonRead(readJson, name, null); - } else { - JsonToken token = readJson.parser().nextToken(); - if (JsonToken.VALUE_NULL == token || JsonToken.END_ARRAY == token) { - return null; + } + /// ROB Checkme + JsonReader parser = readJson.parser(); + if (parser.isNullValue()) { + return null; + } + EntityBean incomingBean = (EntityBean) targetDescriptor.jsonRead(readJson, name, null); + Object id = idProperty.getValue(incomingBean); + EntityBean existingBean = (EntityBean) targets.get(id); + if (existingBean == null) { + return incomingBean; + } + mergeLoadedBean(targetDescriptor, incomingBean, existingBean); + return existingBean; + } + + private void mergeLoadedBean(BeanDescriptor descriptor, EntityBean incomingBean, EntityBean existingBean) { + EntityBeanIntercept incomingIntercept = incomingBean._ebean_getIntercept(); + for (BeanProperty property : descriptor.propertiesBaseScalar()) { + if (incomingIntercept.isLoadedProperty(property.propertyIndex())) { + property.setValueIntercept(existingBean, property.getValue(incomingBean)); + } + } + for (BeanPropertyAssocMany property : descriptor.propertiesMany()) { + if (incomingIntercept.isLoadedProperty(property.propertyIndex())) { + Object mergedValue = mergeLoadedMany(property, property.getValue(incomingBean), property.getValue(existingBean)); + property.setValueIntercept(existingBean, mergedValue); + } + } + } + + private Object mergeLoadedMany(BeanPropertyAssocMany property, Object incomingValue, Object existingValue) { + if (!(incomingValue instanceof Collection)) { + return incomingValue; + } + if (!(existingValue instanceof Collection)) { + return incomingValue; + } + Collection incomingCollection = (Collection) incomingValue; + Collection existingCollection = (Collection) existingValue; + BeanProperty idProperty = property.targetDescriptor.idProperty(); + if (idProperty == null) { + return incomingValue; + } + + Map existingById = new LinkedHashMap<>(); + for (Object existingElement : existingCollection) { + if (existingElement instanceof EntityBean) { + EntityBean existingBean = (EntityBean) existingElement; + Object id = idProperty.getValue(existingBean); + if (id != null) { + existingById.put(id, existingBean); + } + } + } + + BeanCollection mergedCollection = property.createEmpty(null); + BeanCollectionAdd add = property.beanCollectionAdd(mergedCollection); + for (Object incomingElement : incomingCollection) { + if (!(incomingElement instanceof EntityBean)) { + continue; + } + EntityBean incomingBean = (EntityBean) incomingElement; + Object id = idProperty.getValue(incomingBean); + EntityBean existingBean = id == null ? null : existingById.get(id); + if (existingBean == null) { + add.addEntityBean(incomingBean); + } else { + mergeLoadedBean(property.targetDescriptor, incomingBean, existingBean); + add.addEntityBean(existingBean); } - // extract the id. We have to buffer the JSON; - ObjectNode node = readJson.mapper().readTree(readJson.parser()); - SpiJsonReader jsonReader = readJson.forJson(node.traverse()); - JsonNode idNode = node.get(idProperty.name()); - Object id = idNode == null ? null : idProperty.jsonRead(readJson.forJson(idNode.traverse())); - return (EntityBean) targetDescriptor.jsonRead(jsonReader, name, targets.get(id)); } + return mergedCollection; } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonHelp.java index 4bc7307fe0..d22b18f8ce 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonHelp.java @@ -1,8 +1,7 @@ package io.ebeaninternal.server.deploy; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonReader.Token; import io.ebean.bean.EntityBean; import io.ebeaninternal.api.json.SpiJsonReader; @@ -38,17 +37,17 @@ public void jsonRead(SpiJsonReader readJson, EntityBean parentBean) throws IOExc if (!this.many.jsonDeserialize) { return; } - JsonParser parser = readJson.parser(); - JsonToken event = parser.nextToken(); - if (JsonToken.VALUE_NULL == event) { + JsonReader parser = readJson.parser(); + if (parser.isNullValue()) { return; } if (many.isTransient()) { jsonReadTransientUsingObjectMapper(readJson, parentBean); return; } - if (JsonToken.START_ARRAY != event && JsonToken.START_OBJECT != event) { - throw new JsonParseException(parser, "Unexpected token " + event + " - expecting start array or object"); + Token event = parser.currentToken(); + if (Token.BEGIN_ARRAY != event && Token.BEGIN_OBJECT != event) { + throw new IllegalStateException("Unexpected token " + event + " - expecting start array or object"); } if (readJson.update()) { many.setValueIntercept(parentBean, many.jsonReadCollection(readJson, parentBean, many.getValue(parentBean))); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonTransient.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonTransient.java index ae82bd7c65..92d58688c6 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonTransient.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonTransient.java @@ -29,11 +29,11 @@ void jsonReadUsingObjectMapper(BeanPropertyAssocMany many, SpiJsonReader read TypeFactory typeFactory = mapper.getTypeFactory(); JavaType target = typeFactory.constructType(many.targetType()); MapType jacksonType = typeFactory.constructMapType(LinkedHashMap.class, TypeFactory.unknownType(), target); - value = mapper.readValue(readJson.parser(), jacksonType); + value = mapper.readValue(readJson.parser().readRaw(), jacksonType); } else { // read list or set using Jackson object mapper CollectionType jacksonType = mapper.getTypeFactory().constructCollectionType(manyType.getCollectionType(), many.targetType()); - value = mapper.readValue(readJson.parser(), jacksonType); + value = mapper.readValue(readJson.parser().readRaw(), jacksonType); } many.setValue(parentBean, value); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonBeanReader.java b/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonBeanReader.java index f1b92a78bb..08add2b91f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonBeanReader.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonBeanReader.java @@ -1,6 +1,6 @@ package io.ebeaninternal.server.json; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; import io.ebean.PersistenceIOException; import io.ebean.bean.PersistenceContext; import io.ebean.text.json.JsonBeanReader; @@ -45,7 +45,7 @@ public T read(T target) { } @Override - public JsonBeanReader forJson(JsonParser moreJson) { + public JsonBeanReader forJson(JsonReader moreJson) { return new DJsonBeanReader<>(desc, readJson.forJson(moreJson)); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonContext.java index 39b80344b9..6e90175407 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonContext.java @@ -1,12 +1,9 @@ package io.ebeaninternal.server.json; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.core.PrettyPrinter; -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +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.FetchPath; import io.ebean.bean.EntityBean; import io.ebean.config.JsonConfig; @@ -28,7 +25,6 @@ import java.io.IOException; import java.io.Reader; -import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Type; @@ -45,30 +41,22 @@ */ public final class DJsonContext implements SpiJsonContext { - private static final PrettyPrinter PRETTY_PRINTER = new Pretty(); - private final SpiEbeanServer server; - private final JsonFactory jsonFactory; + private final JsonStream jsonStream; private final Object defaultObjectMapper; private final JsonConfig.Include defaultInclude; private final DJsonScalar jsonScalar; - private static class Pretty extends DefaultPrettyPrinter { - Pretty() { - _objectFieldValueSeparatorWithSpaces = ": "; - } - } - - public DJsonContext(SpiEbeanServer server, JsonFactory jsonFactory, TypeManager typeManager) { + public DJsonContext(SpiEbeanServer server, JsonStream jsonStream, TypeManager typeManager) { this.server = server; - this.jsonFactory = (jsonFactory != null) ? jsonFactory : new JsonFactory(); + this.jsonStream = jsonStream; this.defaultObjectMapper = this.server.config().getObjectMapper(); this.defaultInclude = this.server.config().getJsonInclude(); this.jsonScalar = new DJsonScalar(typeManager); } @Override - public void writeScalar(JsonGenerator generator, Object scalarValue) throws IOException { + public void writeScalar(JsonWriter generator, Object scalarValue) throws IOException { jsonScalar.write(generator, scalarValue); } @@ -78,31 +66,34 @@ public boolean isSupportedType(Type genericType) { } @Override - public JsonGenerator createGenerator(Writer writer) throws JsonIOException { - try { - return jsonFactory.createGenerator(writer); - } catch (IOException e) { - throw new JsonIOException(e); - } + public JsonWriter createGenerator(Writer writer) throws JsonIOException { + JsonWriter jsonWriter = stream().writer(writer); + jsonWriter.serializeNulls(defaultInclude == JsonConfig.Include.ALL); + jsonWriter.serializeEmpty(defaultInclude != JsonConfig.Include.NON_EMPTY); + return jsonWriter; } @Override - public JsonParser createParser(Reader reader) throws JsonIOException { + public JsonReader createParser(Reader reader) throws JsonIOException { try { - return jsonFactory.createParser(reader); + return createParser(readAll(reader)); } catch (IOException e) { throw new JsonIOException(e); } } + private JsonStream stream() { + return (jsonStream != null) ? jsonStream : JsonStream.builder().build(); + } + @Override public T toBean(Class cls, String json) throws JsonIOException { - return toBean(cls, new StringReader(json)); + return toBean(cls, createParser(json)); } @Override public T toBean(Class cls, String json, JsonReadOptions options) throws JsonIOException { - return toBean(cls, new StringReader(json), options); + return toBean(cls, createParser(json), options); } @Override @@ -116,15 +107,15 @@ public T toBean(Class cls, Reader jsonReader, JsonReadOptions options) th } @Override - public T toBean(Class cls, JsonParser parser) throws JsonIOException { + public T toBean(Class cls, JsonReader parser) throws JsonIOException { return toBean(cls, parser, null); } @Override - public T toBean(Class cls, JsonParser parser, JsonReadOptions options) throws JsonIOException { + public T toBean(Class cls, JsonReader parser, JsonReadOptions options) throws JsonIOException { BeanDescriptor desc = getDescriptor(cls); try { - return desc.jsonRead(new ReadJson(desc, parser, options, determineObjectMapper(options), false), null, null); + return desc.jsonRead(new ReadJson(desc, parser, options, determineObjectMapper(options), false, stream()), null, null); } catch (IOException e) { throw new JsonIOException(e); } @@ -132,12 +123,12 @@ public T toBean(Class cls, JsonParser parser, JsonReadOptions options) th @Override public void toBean(T target, String json) throws JsonIOException { - toBean(target, new StringReader(json)); + toBean(target, createParser(json)); } @Override public void toBean(T target, String json, JsonReadOptions options) throws JsonIOException { - toBean(target, new StringReader(json), options); + toBean(target, createParser(json), options); } @Override @@ -151,42 +142,42 @@ public void toBean(T target, Reader jsonReader, JsonReadOptions options) thr } @Override - public void toBean(T target, JsonParser parser) throws JsonIOException { + public void toBean(T target, JsonReader parser) throws JsonIOException { toBean(target, parser, null); } @SuppressWarnings("unchecked") @Override - public void toBean(T target, JsonParser parser, JsonReadOptions options) throws JsonIOException { + public void toBean(T target, JsonReader parser, JsonReadOptions options) throws JsonIOException { BeanDescriptor desc = (BeanDescriptor) getDescriptor(target.getClass()); try { - desc.jsonRead(new ReadJson(desc, parser, options, determineObjectMapper(options), target != null), null, target); + desc.jsonRead(new ReadJson(desc, parser, options, determineObjectMapper(options), target != null, stream()), null, target); } catch (IOException e) { throw new JsonIOException(e); } } @Override - public DJsonBeanReader createBeanReader(Class cls, JsonParser parser, JsonReadOptions options) throws JsonIOException { + public DJsonBeanReader createBeanReader(Class cls, JsonReader parser, JsonReadOptions options) throws JsonIOException { BeanDescriptor desc = getDescriptor(cls); - return new DJsonBeanReader<>(desc, new ReadJson(desc, parser, options, determineObjectMapper(options), false)); + return new DJsonBeanReader<>(desc, new ReadJson(desc, parser, options, determineObjectMapper(options), false, stream())); } @Override - public DJsonBeanReader createBeanReader(BeanType beanType, JsonParser parser, JsonReadOptions options) throws JsonIOException { + public DJsonBeanReader createBeanReader(BeanType beanType, JsonReader parser, JsonReadOptions options) throws JsonIOException { BeanDescriptor desc = (BeanDescriptor) beanType; - SpiJsonReader readJson = new ReadJson(desc, parser, options, determineObjectMapper(options), false); + SpiJsonReader readJson = new ReadJson(desc, parser, options, determineObjectMapper(options), false, stream()); return new DJsonBeanReader<>(desc, readJson); } @Override public List toList(Class cls, String json) throws JsonIOException { - return toList(cls, new StringReader(json)); + return toList(cls, createParser(json)); } @Override public List toList(Class cls, String json, JsonReadOptions options) throws JsonIOException { - return toList(cls, new StringReader(json), options); + return toList(cls, createParser(json), options); } @Override @@ -200,35 +191,30 @@ public List toList(Class cls, Reader jsonReader, JsonReadOptions optio } @Override - public List toList(Class cls, JsonParser src) throws JsonIOException { + public List toList(Class cls, JsonReader src) throws JsonIOException { return toList(cls, src, null); } @Override - public List toList(Class cls, JsonParser src, JsonReadOptions options) throws JsonIOException { + public List toList(Class cls, JsonReader src, JsonReadOptions options) throws JsonIOException { BeanDescriptor desc = getDescriptor(cls); - SpiJsonReader readJson = new ReadJson(desc, src, options, determineObjectMapper(options), false); + SpiJsonReader readJson = new ReadJson(desc, src, options, determineObjectMapper(options), false, stream()); try { - - JsonToken currentToken = src.getCurrentToken(); - if (currentToken != JsonToken.START_ARRAY) { - JsonToken event = src.nextToken(); - if (event != JsonToken.START_ARRAY) { - throw new JsonParseException(src, "Expecting start_array event but got " + event); - } + if (src.isNullValue()) { + return null; + } + Token currentToken = src.currentToken(); + if (currentToken != Token.BEGIN_ARRAY) { + throw new JsonIOException("Expecting BEGIN_ARRAY but got " + currentToken); } - List list = new ArrayList<>(); - do { - // CHECKME: Should we update the list + src.beginArray(); + while (src.hasNextElement()) { T bean = desc.jsonRead(readJson, null, null); - if (bean == null) { - break; - } else { + if (bean != null) { list.add(bean); } - } while (true); - + } return list; } catch (IOException e) { throw new JsonIOException(e); @@ -237,7 +223,7 @@ public List toList(Class cls, JsonParser src, JsonReadOptions options) @Override public Object toObject(Type genericType, String json) throws JsonIOException { - return toObject(genericType, createParser(new StringReader(json))); + return toObject(genericType, createParser(json)); } @Override @@ -245,8 +231,22 @@ public Object toObject(Type genericType, Reader json) throws JsonIOException { return toObject(genericType, createParser(json)); } + private JsonReader createParser(String json) { + return stream().reader(json); + } + + private 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 Object toObject(Type genericType, JsonParser jsonParser) throws JsonIOException { + public Object toObject(Type genericType, JsonReader jsonParser) throws JsonIOException { TypeInfo info = ParamTypeHelper.getTypeInfo(genericType); ManyType manyType = info.getManyType(); switch (manyType) { @@ -262,19 +262,19 @@ public Object toObject(Type genericType, JsonParser jsonParser) throws JsonIOExc } @Override - public void toJson(Object value, JsonGenerator generator) throws JsonIOException { + public void toJson(Object value, JsonWriter generator) throws JsonIOException { // generator passed in so don't close it toJsonNoClose(value, generator, null); } @Override - public void toJson(Object value, JsonGenerator generator, FetchPath fetchPath) throws JsonIOException { + public void toJson(Object value, JsonWriter generator, FetchPath fetchPath) throws JsonIOException { // generator passed in so don't close it toJsonNoClose(value, generator, JsonWriteOptions.pathProperties(fetchPath)); } @Override - public void toJson(Object o, JsonGenerator generator, JsonWriteOptions options) throws JsonIOException { + public void toJson(Object o, JsonWriter generator, JsonWriteOptions options) throws JsonIOException { // generator passed in so don't close it toJsonNoClose(o, generator, options); } @@ -303,9 +303,9 @@ public void toJson(Object o, Writer writer, JsonWriteOptions options) throws Jso } /** - * Write to the JsonGenerator and close when complete. + * Write to the JsonWriter and close when complete. */ - private void toJsonWithClose(Object o, JsonGenerator generator, JsonWriteOptions options) throws JsonIOException { + private void toJsonWithClose(Object o, JsonWriter generator, JsonWriteOptions options) throws JsonIOException { try { toJsonInternal(o, generator, options); generator.close(); @@ -315,9 +315,9 @@ private void toJsonWithClose(Object o, JsonGenerator generator, JsonWriteOptions } /** - * Write to the JsonGenerator and without closing it (as it was created externally). + * Write to the JsonWriter and without closing it (as it was created externally). */ - private void toJsonNoClose(Object o, JsonGenerator generator, JsonWriteOptions options) throws JsonIOException { + private void toJsonNoClose(Object o, JsonWriter generator, JsonWriteOptions options) throws JsonIOException { try { toJsonInternal(o, generator, options); } catch (IOException e) { @@ -342,9 +342,9 @@ public String toJson(Object o, JsonWriteOptions options) throws JsonIOException private String toJsonString(Object value, JsonWriteOptions options, boolean pretty) throws JsonIOException { StringWriter writer = new StringWriter(500); - try (JsonGenerator gen = createGenerator(writer)) { + try (JsonWriter gen = createGenerator(writer)) { if (pretty) { - gen.setPrettyPrinter(PRETTY_PRINTER); + gen.pretty(true); } toJsonInternal(value, gen, options); } catch (IOException e) { @@ -354,29 +354,23 @@ private String toJsonString(Object value, JsonWriteOptions options, boolean pret } @SuppressWarnings("unchecked") - private void toJsonInternal(Object value, JsonGenerator gen, JsonWriteOptions options) throws IOException { + private void toJsonInternal(Object value, JsonWriter gen, JsonWriteOptions options) throws IOException { if (value == null) { - gen.writeNull(); + gen.nullValue(); } else if (value instanceof Number) { - gen.writeNumber(((Number) value).doubleValue()); + gen.jsonValue(value); } else if (value instanceof Boolean) { - gen.writeBoolean((Boolean) value); + gen.value((Boolean) value); } else if (value instanceof String) { - gen.writeString((String) value); - - // } else if (o instanceof JsonElement) { - + gen.value((String) value); } else if (value instanceof Map) { toJsonFromMap((Map) value, gen, options); - } else if (value instanceof Collection) { toJsonFromCollection((Collection) value, null, gen, options); - } else if (value instanceof EntityBean) { BeanDescriptor d = getDescriptor(value.getClass()); WriteJson writeJson = createWriteJson(gen, options); d.jsonWrite(writeJson, (EntityBean) value, null); - } else { jsonScalar.write(gen, value); } @@ -385,8 +379,8 @@ private void toJsonInternal(Object value, JsonGenerator gen, JsonWriteOptions op @Override public SpiJsonReader createJsonRead(BeanType beanType, String json) { BeanDescriptor desc = (BeanDescriptor) beanType; - JsonParser parser = createParser(new StringReader(json)); - return new ReadJson(desc, parser, null, defaultObjectMapper, false); + JsonReader parser = createParser(json); + return new ReadJson(desc, parser, null, defaultObjectMapper, false, stream()); } @Override @@ -395,11 +389,11 @@ public SpiJsonWriter createJsonWriter(Writer writer) { } @Override - public SpiJsonWriter createJsonWriter(JsonGenerator gen, JsonWriteOptions options) { + public SpiJsonWriter createJsonWriter(JsonWriter gen, JsonWriteOptions options) { return createWriteJson(gen, options); } - private WriteJson createWriteJson(JsonGenerator gen, JsonWriteOptions options) { + private WriteJson createWriteJson(JsonWriter gen, JsonWriteOptions options) { FetchPath pathProps = (options == null) ? null : options.getPathProperties(); Map> visitors = (options == null) ? null : options.getVisitorMap(); return new WriteJson(server, @@ -411,46 +405,50 @@ private WriteJson createWriteJson(JsonGenerator gen, JsonWriteOptions options) { options == null || options.isIncludeLoadedImplicit()); } - private void toJsonFromCollection(Collection collection, String key, JsonGenerator gen, JsonWriteOptions options) throws IOException { + private void toJsonFromCollection(Collection collection, String key, JsonWriter gen, JsonWriteOptions options) throws IOException { if (key != null) { - gen.writeFieldName(key); + gen.name(key); } - gen.writeStartArray(); + gen.beginArray(); WriteJson writeJson = createWriteJson(gen, options); for (T bean : collection) { - BeanDescriptor d = getDescriptor(bean.getClass()); - d.jsonWrite(writeJson, (EntityBean) bean, null); + if (bean == null) { + gen.nullValue(); + } else if (bean instanceof EntityBean) { + BeanDescriptor d = getDescriptor(bean.getClass()); + d.jsonWrite(writeJson, (EntityBean) bean, null); + } else { + EJson.write(bean, gen); + } } - gen.writeEndArray(); + gen.endArray(); } - private void toJsonFromMap(Map map, JsonGenerator gen, JsonWriteOptions options) throws IOException { + private void toJsonFromMap(Map map, JsonWriter gen, JsonWriteOptions options) throws IOException { Set> entrySet = map.entrySet(); Iterator> it = entrySet.iterator(); WriteJson writeJson = createWriteJson(gen, options); - gen.writeStartObject(); + gen.beginObject(); while (it.hasNext()) { Entry entry = it.next(); String key = entry.getKey().toString(); Object value = entry.getValue(); if (value == null) { - gen.writeNullField(key); + gen.name(key); + gen.nullValue(); + } else if (value instanceof Collection) { + toJsonFromCollection((Collection) value, key, gen, options); + } else if (value instanceof EntityBean) { + BeanDescriptor d = getDescriptor(value.getClass()); + d.jsonWrite(writeJson, (EntityBean) value, key); } else { - if (value instanceof Collection) { - toJsonFromCollection((Collection) value, key, gen, options); - - } else if (value instanceof EntityBean) { - BeanDescriptor d = getDescriptor(value.getClass()); - d.jsonWrite(writeJson, (EntityBean) value, key); - - } else { - EJson.write(entry, gen); - } + gen.name(key); + EJson.write(value, gen); } } - gen.writeEndObject(); + gen.endObject(); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonScalar.java b/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonScalar.java index cd3420773c..d18e96a34f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonScalar.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonScalar.java @@ -1,6 +1,6 @@ package io.ebeaninternal.server.json; -import com.fasterxml.jackson.core.JsonGenerator; +import io.avaje.json.JsonWriter; import io.ebean.core.type.ScalarType; import io.ebeaninternal.server.type.TypeManager; @@ -19,21 +19,18 @@ public DJsonScalar(TypeManager typeManager) { } @SuppressWarnings({ "unchecked", "rawtypes" }) - public void write(JsonGenerator gen, Object value) throws IOException { + public void write(JsonWriter gen, Object value) throws IOException { if (value instanceof String) { - gen.writeString((String) value); + gen.value((String) value); } else if (value instanceof List) { // expected for @DbArray values List list = (List)value; - gen.writeRaw('['); + gen.beginArray(); for (int i = 0; i < list.size(); i++) { - if (i > 0) { - gen.writeRaw(','); - } write(gen, list.get(i)); } - gen.writeRaw(']'); + gen.endArray(); } else { ScalarType scalarType = typeManager.type(value.getClass()); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/json/ReadJson.java b/ebean-core/src/main/java/io/ebeaninternal/server/json/ReadJson.java index ab9100c9f6..181810c352 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/json/ReadJson.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/json/ReadJson.java @@ -1,8 +1,10 @@ package io.ebeaninternal.server.json; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.ObjectMapper; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonReader.Token; +import io.avaje.json.stream.BufferRecycleStrategy; +import io.avaje.json.stream.JsonStream; import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; import io.ebean.bean.PersistenceContext; @@ -22,8 +24,9 @@ */ public final class ReadJson implements SpiJsonReader { + private final JsonStream jsonStream; private final BeanDescriptor rootDesc; - private final JsonParser parser; + private final JsonReader parser; private final PathStack pathStack; /** * Map of the JsonReadBeanVisitor keyed by path. @@ -38,9 +41,10 @@ public final class ReadJson implements SpiJsonReader { /** * Construct with parser and readOptions. */ - public ReadJson(BeanDescriptor desc, JsonParser parser, JsonReadOptions readOptions, Object objectMapper, boolean update) { + public ReadJson(BeanDescriptor desc, JsonReader parser, JsonReadOptions readOptions, Object objectMapper, boolean update, JsonStream jsonStream) { this.rootDesc = desc; this.parser = parser; + this.jsonStream = jsonStream; this.objectMapper = objectMapper; this.persistenceContext = initPersistenceContext(readOptions); this.loadContext = initLoadContext(desc, readOptions); @@ -54,7 +58,8 @@ public ReadJson(BeanDescriptor desc, JsonParser parser, JsonReadOptions readO /** * Construct when transferring load context, persistence context, object mapper etc to a new ReadJson instance. */ - private ReadJson(JsonParser moreJson, ReadJson source) { + private ReadJson(JsonReader moreJson, ReadJson source) { + this.jsonStream = source.jsonStream; this.parser = moreJson; this.rootDesc = source.rootDesc; this.pathStack = source.pathStack; @@ -91,13 +96,22 @@ public PersistenceContext persistenceContext() { } /** - * Return a new instance of ReadJson using the existing context but with a new JsonParser. + * Return a new instance of ReadJson using the existing context but with a new JsonReader. */ @Override - public SpiJsonReader forJson(JsonParser moreJson) { + public SpiJsonReader forJson(JsonReader moreJson) { return new ReadJson(moreJson, this); } + @Override + public SpiJsonReader forJson(String moreJson) { + JsonReader nestedReader = JsonStream.builder() + .bufferRecycling(BufferRecycleStrategy.NO_RECYCLING) + .build() + .reader(moreJson); + return new ReadJson(nestedReader, this); + } + /** * Add the bean to the persistence context. */ @@ -152,19 +166,19 @@ public ObjectMapper mapper() { } /** - * Return the JsonParser. + * Return the JsonReader. */ @Override - public JsonParser parser() { + public JsonReader parser() { return parser; } /** - * Return the next JsonToken from the underlying parser. + * Return the current token from the underlying parser. */ @Override - public JsonToken nextToken() throws IOException { - return parser.nextToken(); + public Token nextToken() throws IOException { + return parser.currentToken(); } /** @@ -209,7 +223,7 @@ public void beanVisitor(Object bean, Map unmappedProperties) { */ @Override public Object readValueUsingObjectMapper(Class propertyType) throws IOException { - return mapper().readValue(parser, propertyType); + return mapper().readValue(parser.readRaw(), propertyType); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java b/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java index f5b583822c..b11242d525 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.json; -import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; +import io.avaje.json.JsonWriter; import io.ebean.FetchPath; import io.ebean.bean.EntityBean; import io.ebean.config.JsonConfig; @@ -24,7 +24,7 @@ public final class WriteJson implements SpiJsonWriter { private final SpiEbeanServer server; - private final JsonGenerator generator; + private final JsonWriter generator; private final FetchPath fetchPath; private final Map> visitors; private final PathStack pathStack; @@ -37,7 +37,7 @@ public final class WriteJson implements SpiJsonWriter { /** * Construct for full bean use (normal). */ - public WriteJson(SpiEbeanServer server, JsonGenerator generator, FetchPath fetchPath, + public WriteJson(SpiEbeanServer server, JsonWriter generator, FetchPath fetchPath, Map> visitors, Object objectMapper, JsonConfig.Include include, boolean includeLoadedImplicit) { @@ -50,12 +50,14 @@ public WriteJson(SpiEbeanServer server, JsonGenerator generator, FetchPath fetch this.includeLoadedImplicit = includeLoadedImplicit; this.parentBeans = new ArrayStack<>(); this.pathStack = new PathStack(); + this.generator.serializeNulls(isIncludeNull()); + this.generator.serializeEmpty(isIncludeEmpty()); } /** * Construct for Json scalar use. */ - public WriteJson(JsonGenerator generator, JsonConfig.Include include) { + public WriteJson(JsonWriter generator, JsonConfig.Include include) { this.generator = generator; this.include = include; this.includeLoadedImplicit = true; @@ -65,6 +67,8 @@ public WriteJson(JsonGenerator generator, JsonConfig.Include include) { this.objectMapper = null; this.parentBeans = null; this.pathStack = null; + this.generator.serializeNulls(isIncludeNull()); + this.generator.serializeEmpty(isIncludeEmpty()); } /** @@ -84,7 +88,7 @@ public boolean isIncludeEmpty() { } @Override - public JsonGenerator gen() { + public JsonWriter gen() { return generator; } @@ -95,171 +99,113 @@ public void flush() throws IOException { @Override public void writeStartObject(String key) { - try { - if (key != null) { - generator.writeFieldName(key); - } - generator.writeStartObject(); - } catch (IOException e) { - throw new JsonIOException(e); + if (key != null) { + generator.name(key); } + generator.beginObject(); } @Override public void writeStartObject() { - try { - generator.writeStartObject(); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.beginObject(); } @Override public void writeEndObject() { - try { - generator.writeEndObject(); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.endObject(); } @Override public void writeStartArray(String key) { - try { - if (key != null) { - generator.writeFieldName(key); - } - generator.writeStartArray(); - } catch (IOException e) { - throw new JsonIOException(e); + if (key != null) { + generator.name(key); } + generator.beginArray(); } @Override public void writeStartArray() { - try { - generator.writeStartArray(); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.beginArray(); } @Override public void writeEndArray() { - try { - generator.writeEndArray(); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.endArray(); } @Override public void writeRaw(String text) { - try { - generator.writeRaw(text); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.rawChunkStart(); + generator.rawChunk(text); + generator.rawChunkEnd(); } @Override public void writeRawValue(String text) { - try { - generator.writeRawValue(text); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.rawValue(text); } @Override public void writeFieldName(String name) { - try { - generator.writeFieldName(name); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.name(name); } @Override public void writeNullField(String name) { if (isIncludeNull()) { - try { - generator.writeNullField(name); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.name(name); + generator.nullValue(); } } @Override public void writeNumberField(String name, long value) { - try { - generator.writeNumberField(name, value); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.name(name); + generator.value(value); } @Override public void writeNumberField(String name, double value) { - try { - generator.writeNumberField(name, value); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.name(name); + generator.value(value); } @Override public void writeNumberField(String name, int value) { - try { - generator.writeNumberField(name, value); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.name(name); + generator.value(value); } @Override public void writeNumberField(String name, short value) { - try { - generator.writeNumberField(name, value); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.name(name); + generator.value(value); } @Override public void writeNumberField(String name, float value) { - try { - generator.writeNumberField(name, value); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.name(name); + generator.value((double) value); } @Override public void writeNumberField(String name, BigDecimal value) { - try { - generator.writeNumberField(name, value); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.name(name); + generator.value(value); } @Override public void writeStringField(String name, String value) { - try { - generator.writeStringField(name, value); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.name(name); + generator.value(value); } @Override public void writeBinary(InputStream is, int length) { try { - generator.writeBinary(is, length); + generator.value(is.readNBytes(length)); } catch (IOException e) { throw new JsonIOException(e); } @@ -267,83 +213,49 @@ public void writeBinary(InputStream is, int length) { @Override public void writeBinaryField(String name, byte[] value) { - try { - generator.writeBinaryField(name, value); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.name(name); + generator.value(value); } @Override public void writeBooleanField(String name, boolean value) { - try { - generator.writeBooleanField(name, value); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.name(name); + generator.value(value); } @Override public void writeBoolean(boolean value) { - try { - generator.writeBoolean(value); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.value(value); } @Override public void writeString(String value) { - try { - generator.writeString(value); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.value(value); } @Override public void writeNumber(int value) { - try { - generator.writeNumber(value); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.value(value); } @Override public void writeNumber(long value) { - try { - generator.writeNumber(value); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.value(value); } @Override public void writeNumber(double value) { - try { - generator.writeNumber(value); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.value(value); } @Override public void writeNumber(BigDecimal value) { - try { - generator.writeNumber(value); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.value(value); } @Override public void writeNull() { - try { - generator.writeNull(); - } catch (IOException e) { - throw new JsonIOException(e); - } + generator.nullValue(); } @Override @@ -375,55 +287,39 @@ public void endAssocOne() { @Override public void beginAssocMany(String key) { - try { - pathStack.pushPathKey(key); - if (key != null) { - generator.writeFieldName(key); - } - generator.writeStartArray(); - } catch (IOException e) { - throw new JsonIOException(e); + pathStack.pushPathKey(key); + if (key != null) { + generator.name(key); } + generator.beginArray(); } @Override public void endAssocMany() { - try { - pathStack.pop(); - generator.writeEndArray(); - } catch (IOException e) { - throw new JsonIOException(e); - } + pathStack.pop(); + generator.endArray(); } @Override public void beginAssocManyMap(String key, boolean elementCollection) { - try { - pathStack.pushPathKey(key); - if (key != null) { - generator.writeFieldName(key); - } - if (elementCollection) { - generator.writeStartObject(); - } else { - generator.writeStartArray(); - } - } catch (IOException e) { - throw new JsonIOException(e); + pathStack.pushPathKey(key); + if (key != null) { + generator.name(key); + } + if (elementCollection) { + generator.beginObject(); + } else { + generator.beginArray(); } } @Override public void endAssocManyMap(boolean elementCollection) { - try { - pathStack.pop(); - if (elementCollection) { - generator.writeEndObject(); - } else { - generator.writeEndArray(); - } - } catch (IOException e) { - throw new JsonIOException(e); + pathStack.pop(); + if (elementCollection) { + generator.endObject(); + } else { + generator.endArray(); } } @@ -465,8 +361,8 @@ public void writeValueUsingObjectMapper(String name, Object value) { } } try { - generator.writeFieldName(name); - objectMapper().writeValue(generator, value); + generator.name(name); + generator.rawValue(objectMapper().writeValueAsString(value)); } catch (IOException e) { throw new JsonIOException(e); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/readaudit/DefaultReadAuditLogger.java b/ebean-core/src/main/java/io/ebeaninternal/server/readaudit/DefaultReadAuditLogger.java index 785dd57b53..167b2ce866 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/readaudit/DefaultReadAuditLogger.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/readaudit/DefaultReadAuditLogger.java @@ -1,8 +1,8 @@ package io.ebeaninternal.server.readaudit; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; import io.avaje.applog.AppLog; +import io.avaje.json.JsonWriter; +import io.avaje.json.stream.JsonStream; import io.ebean.event.readaudit.ReadAuditLogger; import io.ebean.event.readaudit.ReadAuditQueryPlan; import io.ebean.event.readaudit.ReadEvent; @@ -24,7 +24,7 @@ public class DefaultReadAuditLogger implements ReadAuditLogger { private static final System.Logger queryLogger = AppLog.getLogger("io.ebean.ReadAuditQuery"); private static final System.Logger auditLogger = AppLog.getLogger("io.ebean.ReadAudit"); - protected final JsonFactory jsonFactory = new JsonFactory(); + protected final JsonStream jsonStream = JsonStream.builder().build(); protected final int defaultQueryBuffer = 500; protected final int defaultReadBuffer = 150; @@ -34,25 +34,26 @@ public class DefaultReadAuditLogger implements ReadAuditLogger { @Override public void queryPlan(ReadAuditQueryPlan queryPlan) { StringWriter writer = new StringWriter(defaultQueryBuffer); - try (JsonGenerator gen = jsonFactory.createGenerator(writer)) { - gen.writeStartObject(); + try (JsonWriter gen = jsonStream.writer(writer)) { + gen.beginObject(); String beanType = queryPlan.getBeanType(); if (beanType != null) { - gen.writeStringField("beanType", beanType); + gen.name("beanType"); + gen.value(beanType); } String queryKey = queryPlan.getQueryKey(); if (queryKey != null) { - gen.writeStringField("queryKey", queryKey); + gen.name("queryKey"); + gen.value(queryKey); } String sql = queryPlan.getSql(); if (sql != null) { - gen.writeStringField("sql", sql); + gen.name("sql"); + gen.value(sql); } - gen.writeEndObject(); + gen.endObject(); gen.flush(); queryLogger.log(INFO, writer.toString()); - } catch (IOException e) { - CoreLog.log.log(ERROR, "Error writing Read audit event", e); } } @@ -73,56 +74,64 @@ public void auditMany(ReadEvent readMany) { } protected void writeEvent(ReadEvent event) { - try { - StringWriter writer = new StringWriter(defaultReadBuffer); - JsonGenerator gen = jsonFactory.createGenerator(writer); + StringWriter writer = new StringWriter(defaultReadBuffer); + try (JsonWriter gen = jsonStream.writer(writer)) { writeDetails(gen, event); - auditLogger.log(INFO, writer.toString()); } catch (IOException e) { CoreLog.log.log(ERROR, "Error writing Read audit event", e); + return; } + auditLogger.log(INFO, writer.toString()); } /** * Write the details for the read bean or read many beans event. */ - protected void writeDetails(JsonGenerator gen, ReadEvent event) throws IOException { - gen.writeStartObject(); + protected void writeDetails(JsonWriter gen, ReadEvent event) throws IOException { + gen.beginObject(); String source = event.getSource(); if (source != null) { - gen.writeStringField("source", source); + gen.name("source"); + gen.value(source); } String userId = event.getUserId(); if (userId != null) { - gen.writeStringField("userId", userId); + gen.name("userId"); + gen.value(userId); } String userIpAddress = event.getUserIpAddress(); if (userIpAddress != null) { - gen.writeStringField("userIpAddress", userIpAddress); + gen.name("userIpAddress"); + gen.value(userIpAddress); } Map userContext = event.getUserContext(); if (userContext != null && !userContext.isEmpty()) { - gen.writeObjectFieldStart("userContext"); + gen.name("userContext"); + gen.beginObject(); for (Map.Entry entry : userContext.entrySet()) { - gen.writeStringField(entry.getKey(), entry.getValue()); + gen.name(entry.getKey()); + gen.value(entry.getValue()); } - gen.writeEndObject(); + gen.endObject(); } - gen.writeNumberField("eventTime", event.getEventTime()); - gen.writeStringField("beanType", event.getBeanType()); - gen.writeStringField("queryKey", event.getQueryKey()); - gen.writeStringField("bindLog", event.getBindLog()); + gen.name("eventTime"); + gen.value(event.getEventTime()); + gen.name("beanType"); + gen.value(event.getBeanType()); + gen.name("queryKey"); + gen.value(event.getQueryKey()); + gen.name("bindLog"); + gen.value(event.getBindLog()); Object id = event.getId(); if (id != null) { - gen.writeFieldName("id"); + gen.name("id"); EJson.write(id, gen); } else { - gen.writeFieldName("ids"); + gen.name("ids"); EJson.writeCollection(event.getIds(), gen); } - gen.writeEndObject(); + gen.endObject(); gen.flush(); - gen.close(); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeArrayList.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeArrayList.java index 59e85b54a6..e976a5279b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeArrayList.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeArrayList.java @@ -1,8 +1,8 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DocPropertyType; import io.ebean.core.type.ScalarType; @@ -173,12 +173,16 @@ private List convert(List rawList) { } @Override - public List jsonRead(JsonParser parser) throws IOException { - return convert(EJson.parseList(parser, parser.getCurrentToken())); + public List jsonRead(JsonReader parser) throws IOException { + if (parser.isNullValue()) { + return null; + } + List rawList = EJson.parseList(parser, parser.currentToken()); + return convert(rawList); } @Override - public void jsonWrite(JsonGenerator writer, List value) throws IOException { + public void jsonWrite(JsonWriter writer, List value) throws IOException { EJson.write(value, writer); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeArraySet.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeArraySet.java index d1034be93b..6ae1d817c6 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeArraySet.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeArraySet.java @@ -1,8 +1,8 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DocPropertyType; import io.ebean.core.type.ScalarType; @@ -152,12 +152,16 @@ private Set convert(List rawList) { } @Override - public Set jsonRead(JsonParser parser) throws IOException { - return convert(EJson.parseList(parser, parser.getCurrentToken())); + public Set jsonRead(JsonReader parser) throws IOException { + if (parser.isNullValue()) { + return null; + } + List rawList = EJson.parseList(parser, parser.currentToken()); + return convert(rawList); } @Override - public void jsonWrite(JsonGenerator writer, Set value) throws IOException { + public void jsonWrite(JsonWriter writer, Set value) throws IOException { EJson.write(value, writer); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBigDecimal.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBigDecimal.java index c771ff59b4..802e0051a1 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBigDecimal.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBigDecimal.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -78,13 +78,13 @@ public void writeData(DataOutput dataOutput, BigDecimal b) throws IOException { } @Override - public BigDecimal jsonRead(JsonParser parser) throws IOException { - return parser.getDecimalValue(); + public BigDecimal jsonRead(JsonReader parser) throws IOException { + return parser.readDecimal(); } @Override - public void jsonWrite(JsonGenerator writer, BigDecimal value) throws IOException { - writer.writeNumber(value); + public void jsonWrite(JsonWriter writer, BigDecimal value) throws IOException { + writer.value(value); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBoolean.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBoolean.java index 62aa6c5538..690a46b909 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBoolean.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBoolean.java @@ -1,8 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -348,13 +347,39 @@ public void writeData(DataOutput dataOutput, Boolean val) throws IOException { } @Override - public Boolean jsonRead(JsonParser parser) { - return JsonToken.VALUE_TRUE == parser.getCurrentToken() ? Boolean.TRUE : Boolean.FALSE; + public Boolean jsonRead(JsonReader parser) { + if (parser.isNullValue()) { + return null; + } + var token = parser.currentToken(); + if (token == JsonReader.Token.BOOLEAN) { + return parser.readBoolean(); + } + if (token == JsonReader.Token.NUMBER) { + return parser.readDecimal().intValue() == 1; + } + if (token == JsonReader.Token.STRING) { + return parse(parser.readString()); + } + String raw = parser.readRaw(); + if (raw == null || "null".equals(raw)) { + return null; + } + if ("true".equals(raw) || "1".equals(raw)) { + return Boolean.TRUE; + } + if ("false".equals(raw) || "0".equals(raw)) { + return Boolean.FALSE; + } + if (raw.length() > 1 && raw.charAt(0) == '"' && raw.charAt(raw.length() - 1) == '"') { + return parse(raw.substring(1, raw.length() - 1)); + } + return parse(raw); } @Override - public void jsonWrite(JsonGenerator writer, Boolean value) throws IOException { - writer.writeBoolean(value); + public void jsonWrite(JsonWriter writer, Boolean value) throws IOException { + writer.value(value); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeByte.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeByte.java index e01bc625ed..c087e47206 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeByte.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeByte.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -9,7 +9,6 @@ import io.ebean.text.TextException; import io.ebean.core.type.BasicTypeConverter; -import java.io.ByteArrayOutputStream; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; @@ -55,15 +54,13 @@ public Byte toBeanType(Object value) { } @Override - public void jsonWrite(JsonGenerator writer, Byte value) throws IOException { - writer.writeBinary(new byte[]{value}); + public void jsonWrite(JsonWriter writer, Byte value) throws IOException { + writer.value(new byte[]{value}); } @Override - public Byte jsonRead(JsonParser parser) throws IOException { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - parser.readBinaryValue(os); - byte[] bytes = os.toByteArray(); + public Byte jsonRead(JsonReader parser) throws IOException { + byte[] bytes = parser.readBinary(); if (bytes.length == 0) { return null; } else { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBytesBase.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBytesBase.java index a5647e8a7a..1165c5c524 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBytesBase.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBytesBase.java @@ -1,13 +1,12 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DocPropertyType; import io.ebean.core.type.ScalarTypeBase; import io.ebean.text.TextException; -import java.io.ByteArrayOutputStream; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; @@ -85,15 +84,13 @@ public void writeData(DataOutput dataOutput, byte[] v) throws IOException { } @Override - public void jsonWrite(JsonGenerator writer, byte[] value) throws IOException { - writer.writeBinary(value); + public void jsonWrite(JsonWriter writer, byte[] value) throws IOException { + writer.value(value); } @Override - public byte[] jsonRead(JsonParser parser) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(500); - parser.readBinaryValue(out); - return out.toByteArray(); + public byte[] jsonRead(JsonReader parser) throws IOException { + return parser.readBinary(); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBytesEncrypted.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBytesEncrypted.java index 3fdc0f5b24..416fa17f01 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBytesEncrypted.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeBytesEncrypted.java @@ -1,13 +1,12 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; import io.ebean.core.type.ScalarType; -import java.io.ByteArrayOutputStream; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; @@ -58,15 +57,13 @@ public boolean jdbcNative() { } @Override - public void jsonWrite(JsonGenerator writer, byte[] value) throws IOException { - writer.writeBinary(value); + public void jsonWrite(JsonWriter writer, byte[] value) throws IOException { + writer.value(value); } @Override - public byte[] jsonRead(JsonParser parser) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(500); - parser.readBinaryValue(out); - return out.toByteArray(); + public byte[] jsonRead(JsonReader parser) throws IOException { + return parser.readBinary(); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeCharArray.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeCharArray.java index ff74e0cfe4..2f50d36969 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeCharArray.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeCharArray.java @@ -1,6 +1,6 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.ScalarTypeBaseVarchar; @@ -72,7 +72,7 @@ public char[] parse(String value) { } @Override - public char[] jsonRead(JsonParser parser) throws IOException { - return parser.getValueAsString().toCharArray(); + public char[] jsonRead(JsonReader parser) throws IOException { + return parser.readString().toCharArray(); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeDouble.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeDouble.java index 3fe5deba07..6b21c1a1c4 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeDouble.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeDouble.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -77,13 +77,13 @@ public void writeData(DataOutput dataOutput, Double value) throws IOException { } @Override - public Double jsonRead(JsonParser parser) throws IOException { - return parser.getDoubleValue(); + public Double jsonRead(JsonReader parser) throws IOException { + return parser.readDouble(); } @Override - public void jsonWrite(JsonGenerator writer, Double value) throws IOException { - writer.writeNumber(value); + public void jsonWrite(JsonWriter writer, Double value) throws IOException { + writer.value(value); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeDuration.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeDuration.java index 1589a22af3..a75f53bac3 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeDuration.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeDuration.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.*; import java.io.DataInput; @@ -90,13 +90,13 @@ public Duration parse(String value) { } @Override - public Duration jsonRead(JsonParser parser) throws IOException { - return Duration.parse(parser.getValueAsString()); + public Duration jsonRead(JsonReader parser) throws IOException { + return Duration.parse(parser.readString()); } @Override - public void jsonWrite(JsonGenerator writer, Duration value) throws IOException { - writer.writeString(value.toString()); + public void jsonWrite(JsonWriter writer, Duration value) throws IOException { + writer.value(value.toString()); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeEncryptedWrapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeEncryptedWrapper.java index 627d32a65b..4d37a007d9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeEncryptedWrapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeEncryptedWrapper.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -120,12 +120,12 @@ public Object toJdbcType(Object value) { } @Override - public T jsonRead(JsonParser parser) throws IOException { + public T jsonRead(JsonReader parser) throws IOException { return wrapped.jsonRead(parser); } @Override - public void jsonWrite(JsonGenerator writer, T value) throws IOException { + public void jsonWrite(JsonWriter writer, T value) throws IOException { wrapped.jsonWrite(writer, value); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeEnumStandard.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeEnumStandard.java index 227520fbf7..2a04769214 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeEnumStandard.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeEnumStandard.java @@ -1,11 +1,11 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; import io.ebean.core.type.ScalarTypeBase; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import jakarta.persistence.EnumType; import java.io.DataInput; @@ -228,20 +228,19 @@ public Object parse(String value) { } @Override - public Object jsonRead(JsonParser parser) throws IOException { - if (parser.getCodec() != null) { - return parser.readValueAs(enumType); - } else { - return parse(parser.getValueAsString()); + public Object jsonRead(JsonReader parser) throws IOException { + if (parser.isNullValue()) { + return null; } + return parse(parser.readString()); } @Override - public void jsonWrite(JsonGenerator writer, Object value) throws IOException { - if (writer.getCodec() != null) { - writer.writeObject(value); + public void jsonWrite(JsonWriter writer, Object value) throws IOException { + if (value == null) { + writer.nullValue(); } else { - writer.writeString(formatValue(value)); + writer.value(formatValue(value)); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFile.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFile.java index 5e4ad5df80..01ab9747df 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFile.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFile.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -102,16 +102,17 @@ public File toBeanType(Object value) { } @Override - public void jsonWrite(JsonGenerator writer, File value) throws IOException { - InputStream is = getInputStream(value); - writer.writeBinary(is, (int) value.length()); + public void jsonWrite(JsonWriter writer, File value) throws IOException { + try (InputStream is = getInputStream(value)) { + writer.value(is.readAllBytes()); + } } @Override - public File jsonRead(JsonParser parser) throws IOException { + public File jsonRead(JsonReader parser) throws IOException { File tempFile = File.createTempFile(prefix, suffix, directory); try (OutputStream os = getOutputStream(tempFile)) { - parser.readBinaryValue(os); + os.write(parser.readBinary()); os.flush(); } return tempFile; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFloat.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFloat.java index facea66182..6e2f1acdcd 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFloat.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFloat.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -77,13 +77,13 @@ public void writeData(DataOutput dataOutput, Float value) throws IOException { } @Override - public Float jsonRead(JsonParser parser) throws IOException { - return parser.getFloatValue(); + public Float jsonRead(JsonReader parser) throws IOException { + return (float) parser.readDouble(); } @Override - public void jsonWrite(JsonGenerator writer, Float value) throws IOException { - writer.writeNumber(value); + public void jsonWrite(JsonWriter writer, Float value) throws IOException { + writer.value(value.doubleValue()); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeInteger.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeInteger.java index aea3b8d166..97ea05207f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeInteger.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeInteger.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -87,13 +87,13 @@ public Integer parse(String value) { } @Override - public Integer jsonRead(JsonParser parser) throws IOException { - return parser.getIntValue(); + public Integer jsonRead(JsonReader parser) throws IOException { + return parser.readInt(); } @Override - public void jsonWrite(JsonGenerator writer, Integer value) throws IOException { - writer.writeNumber(value); + public void jsonWrite(JsonWriter writer, Integer value) throws IOException { + writer.value(value); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonList.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonList.java index 8d43c7d389..51da2bbc26 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonList.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonList.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.config.dbplatform.DbPlatformType; import io.ebean.core.type.*; import io.ebean.text.TextException; @@ -182,12 +182,12 @@ public List parse(String value) { } @Override - public final List jsonRead(JsonParser parser) throws IOException { - return EJson.parseList(parser, parser.getCurrentToken()); + public final List jsonRead(JsonReader parser) throws IOException { + return EJson.parseList(parser, parser.currentToken()); } @Override - public final void jsonWrite(JsonGenerator writer, List value) throws IOException { + public final void jsonWrite(JsonWriter writer, List value) throws IOException { EJson.write(value, writer); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java index a97b9911c3..5bc0a4befd 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.config.dbplatform.DbPlatformType; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; @@ -235,13 +235,13 @@ public final void writeData(DataOutput dataOutput, Map map) throws IOException { } @Override - public final void jsonWrite(JsonGenerator writer, Map value) throws IOException { + public final void jsonWrite(JsonWriter writer, Map value) throws IOException { EJson.write(value, writer); } @Override - public final Map jsonRead(JsonParser parser) throws IOException { - return EJson.parseObject(parser, parser.getCurrentToken()); + public final Map jsonRead(JsonReader parser) throws IOException { + return EJson.parseObject(parser, parser.currentToken()); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java index 3997c374ad..d094001f07 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.config.dbplatform.DbPlatformType; import io.ebean.core.type.*; import io.ebean.text.TextException; @@ -179,12 +179,12 @@ public Set parse(String value) { } @Override - public final Set jsonRead(JsonParser parser) throws IOException { - return convertList(EJson.parseList(parser, parser.getCurrentToken())); + public final Set jsonRead(JsonReader parser) throws IOException { + return convertList(EJson.parseList(parser, parser.currentToken())); } @Override - public final void jsonWrite(JsonGenerator writer, Set value) throws IOException { + public final void jsonWrite(JsonWriter writer, Set value) throws IOException { EJson.write(value, writer); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTime.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTime.java index eb84613eac..0dde36f042 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTime.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTime.java @@ -1,7 +1,8 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonReader.Token; +import io.avaje.json.JsonWriter; import io.ebean.config.JsonConfig; import io.ebean.config.dbplatform.ExtraDbTypes; import io.ebean.core.type.ScalarTypeBaseDateTime; @@ -48,13 +49,19 @@ protected LocalDateTime fromJsonISO8601(String value) { } @Override - public LocalDateTime jsonRead(JsonParser parser) throws IOException { - return parse(parser.getText()); + public LocalDateTime jsonRead(JsonReader parser) throws IOException { + if (parser.isNullValue()) { + return null; + } + if (parser.currentToken() == Token.NUMBER) { + return parse(parser.readDecimal().toPlainString()); + } + return parse(parser.readString()); } @Override - public void jsonWrite(JsonGenerator writer, LocalDateTime value) throws IOException { - writer.writeString(value.toString()); + public void jsonWrite(JsonWriter writer, LocalDateTime value) throws IOException { + writer.value(value.toString()); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalTime.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalTime.java index d43f0e6cb5..a7a280730c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalTime.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalTime.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -87,13 +87,13 @@ public LocalTime parse(String value) { } @Override - public LocalTime jsonRead(JsonParser parser) throws IOException { - return LocalTime.parse(parser.getValueAsString()); + public LocalTime jsonRead(JsonReader parser) throws IOException { + return LocalTime.parse(parser.readString()); } @Override - public void jsonWrite(JsonGenerator writer, LocalTime value) throws IOException { - writer.writeString(value.toString()); + public void jsonWrite(JsonWriter writer, LocalTime value) throws IOException { + writer.value(value.toString()); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLong.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLong.java index b04a0a9369..6a1fb42212 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLong.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLong.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -87,13 +87,13 @@ public void writeData(DataOutput dataOutput, Long value) throws IOException { } @Override - public Long jsonRead(JsonParser parser) throws IOException { - return parser.getLongValue(); + public Long jsonRead(JsonReader parser) throws IOException { + return parser.readLong(); } @Override - public void jsonWrite(JsonGenerator writer, Long value) throws IOException { - writer.writeNumber(value); + public void jsonWrite(JsonWriter writer, Long value) throws IOException { + writer.value(value); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeMathBigInteger.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeMathBigInteger.java index 29ee7316c4..63246fb74d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeMathBigInteger.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeMathBigInteger.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -82,13 +82,13 @@ public void writeData(DataOutput dataOutput, BigInteger value) throws IOExceptio } @Override - public BigInteger jsonRead(JsonParser parser) throws IOException { - return parser.getDecimalValue().toBigInteger(); + public BigInteger jsonRead(JsonReader parser) throws IOException { + return parser.readBigInteger(); } @Override - public void jsonWrite(JsonGenerator writer, BigInteger value) throws IOException { - writer.writeNumber(value.longValue()); + public void jsonWrite(JsonWriter writer, BigInteger value) throws IOException { + writer.value(value); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeMonthDay.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeMonthDay.java index 95d339bc56..e777f3104a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeMonthDay.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeMonthDay.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -102,13 +102,13 @@ public void writeData(DataOutput dataOutput, MonthDay value) throws IOException } @Override - public MonthDay jsonRead(JsonParser parser) throws IOException { - return parse(parser.getValueAsString()); + public MonthDay jsonRead(JsonReader parser) throws IOException { + return parse(parser.readString()); } @Override - public void jsonWrite(JsonGenerator writer, MonthDay value) throws IOException { - writer.writeString(format(value)); + public void jsonWrite(JsonWriter writer, MonthDay value) throws IOException { + writer.value(format(value)); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeNotFound.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeNotFound.java index 235ea73348..629dd7e75e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeNotFound.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeNotFound.java @@ -5,8 +5,8 @@ import java.io.IOException; import java.sql.SQLException; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; @@ -84,12 +84,12 @@ public void writeData(DataOutput dataOutput, Void v) throws IOException { } @Override - public Void jsonRead(JsonParser parser) throws IOException { + public Void jsonRead(JsonReader parser) throws IOException { throw new UnsupportedOperationException(); } @Override - public void jsonWrite(JsonGenerator writer, Void value) throws IOException { + public void jsonWrite(JsonWriter writer, Void value) throws IOException { throw new UnsupportedOperationException(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypePostgresHstore.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypePostgresHstore.java index 92bd4a450c..5eddc164a6 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypePostgresHstore.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypePostgresHstore.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.config.dbplatform.DbPlatformType; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; @@ -102,16 +102,16 @@ public void writeData(DataOutput dataOutput, Map map) throws IOException { } @Override - public void jsonWrite(JsonGenerator writer, Map value) throws IOException { + public void jsonWrite(JsonWriter writer, Map value) throws IOException { EJson.write(value, writer); } @Override - public Map jsonRead(JsonParser parser) throws IOException { + public Map jsonRead(JsonReader parser) throws IOException { // at this point the BeanProperty has read the START_OBJECT token // to check for a null value. Pass the START_OBJECT token through to // the EJson parsing so that it knows the first token has been read - return EJson.parseObject(parser, parser.getCurrentToken()); + return EJson.parseObject(parser, parser.currentToken()); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeShort.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeShort.java index 059b68a1a3..81fa64814a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeShort.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeShort.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -77,13 +77,13 @@ public void writeData(DataOutput dataOutput, Short value) throws IOException { } @Override - public Short jsonRead(JsonParser parser) throws IOException { - return parser.getShortValue(); + public Short jsonRead(JsonReader parser) throws IOException { + return (short) parser.readInt(); } @Override - public void jsonWrite(JsonGenerator writer, Short value) throws IOException { - writer.writeNumber(value); + public void jsonWrite(JsonWriter writer, Short value) throws IOException { + writer.value(value.intValue()); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeStringBase.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeStringBase.java index 4b260e0961..611278e7ff 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeStringBase.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeStringBase.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -77,13 +77,16 @@ public void writeData(DataOutput dataOutput, String value) throws IOException { } @Override - public String jsonRead(JsonParser parser) throws IOException { - return parser.getValueAsString(); + public String jsonRead(JsonReader parser) throws IOException { + if (parser.isNullValue()) { + return null; + } + return parser.readString(); } @Override - public void jsonWrite(JsonGenerator writer, String value) throws IOException { - writer.writeString(value); + public void jsonWrite(JsonWriter writer, String value) throws IOException { + writer.value(value); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeTime.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeTime.java index a301da7ed8..8490b52bea 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeTime.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeTime.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -83,13 +83,13 @@ public void writeData(DataOutput dataOutput, Time value) throws IOException { } @Override - public Time jsonRead(JsonParser parser) throws IOException { - return parse(parser.getValueAsString()); + public Time jsonRead(JsonReader parser) throws IOException { + return parse(parser.readString()); } @Override - public void jsonWrite(JsonGenerator writer, Time value) throws IOException { - writer.writeString(format(value)); + public void jsonWrite(JsonWriter writer, Time value) throws IOException { + writer.value(format(value)); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeUUIDBase.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeUUIDBase.java index 0e41ddfc76..b79c8a2c92 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeUUIDBase.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeUUIDBase.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.config.dbplatform.DbPlatformType; import io.ebean.core.type.DocPropertyType; import io.ebean.core.type.ScalarTypeBase; @@ -70,13 +70,13 @@ public void writeData(DataOutput dataOutput, UUID value) throws IOException { } @Override - public void jsonWrite(JsonGenerator writer, UUID value) throws IOException { - writer.writeString(formatValue(value)); + public void jsonWrite(JsonWriter writer, UUID value) throws IOException { + writer.value(formatValue(value)); } @Override - public UUID jsonRead(JsonParser parser) throws IOException { - return parse(parser.getValueAsString()); + public UUID jsonRead(JsonReader parser) throws IOException { + return parse(parser.readString()); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeWrapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeWrapper.java index 0df64102b3..26ac04cc77 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeWrapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeWrapper.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.config.ScalarTypeConverter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; @@ -156,13 +156,13 @@ public ScalarType getScalarType() { } @Override - public B jsonRead(JsonParser parser) throws IOException { + public B jsonRead(JsonReader parser) throws IOException { S object = scalarType.jsonRead(parser); return converter.wrapValue(object); } @Override - public void jsonWrite(JsonGenerator writer, B beanValue) throws IOException { + public void jsonWrite(JsonWriter writer, B beanValue) throws IOException { S unwrapValue = converter.unwrapValue(beanValue); scalarType.jsonWrite(writer, unwrapValue); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeYear.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeYear.java index 9c60047f54..1aa300ce2f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeYear.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeYear.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -82,13 +82,13 @@ public Year parse(String value) { } @Override - public Year jsonRead(JsonParser parser) throws IOException { - return Year.of(parser.getIntValue()); + public Year jsonRead(JsonReader parser) throws IOException { + return Year.of(parser.readInt()); } @Override - public void jsonWrite(JsonGenerator writer, Year value) throws IOException { - writer.writeNumber(value.getValue()); + public void jsonWrite(JsonWriter writer, Year value) throws IOException { + writer.value(value.getValue()); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/util/JsonContentHash.java b/ebean-core/src/main/java/io/ebeaninternal/server/util/JsonContentHash.java index 3b44b90763..e4a19f6dfa 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/util/JsonContentHash.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/util/JsonContentHash.java @@ -1,105 +1,121 @@ package io.ebeaninternal.server.util; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; - -import java.io.IOException; +import io.avaje.json.JsonReader; +import io.avaje.json.stream.JsonStream; /** - * Compute an order-independent structural hash of JSON content using Jackson's streaming parser. - *

- * Object key ordering does NOT affect the hash value (handles PostgreSQL JSONB key reordering), - * while array element ordering DOES affect it (array position is semantically significant). + * Compute an order-independent structural hash of JSON content. *

- * This is significantly faster than a full parse/format roundtrip because it performs - * zero object allocation beyond the parser itself — no tree building, no reflection, - * no type conversion. Single-pass O(n) time with O(depth) stack space. + * Object key ordering does not affect the hash value, while array element ordering does. */ public final class JsonContentHash { - private static final JsonFactory FACTORY = new JsonFactory(); + private static final JsonStream STREAM = JsonStream.builder().build(); + + // Type markers to distinguish empty object {}, empty array [], and null + private static final long OBJECT_SEED = 0x7A5662B4E8B10FA3L; + private static final long ARRAY_SEED = 0x3C6EF372FE94F82BL; + private static final long TRUE_HASH = 0x9E3779B97F4A7C15L; + private static final long FALSE_HASH = 0x517CC1B727220A95L; + private static final long NULL_HASH = 0x6C62272E07BB0142L; + + private JsonContentHash() { + } /** * Compute an order-independent hash of JSON content. - * Two JSON strings with identical content but different key ordering - * will produce the same hash value. */ public static long hash(String json) { if (json == null || json.isEmpty()) { return 0L; } - try (JsonParser parser = FACTORY.createParser(json)) { - parser.nextToken(); - return computeHash(parser); - } catch (IOException e) { + try (JsonReader reader = STREAM.reader(json)) { + return hash(reader, firstNonWhitespace(json)); + } catch (RuntimeException e) { // Fallback to regular string hash if JSON is malformed. - // This is safe: two identical malformed strings produce the same hash, - // and a malformed string won't falsely match a valid one. return stringHash(json); } } - private static long computeHash(JsonParser parser) throws IOException { - JsonToken token = parser.currentToken(); - if (token == null) { - return 0L; + private static long hash(JsonReader reader, char firstToken) { + switch (firstToken) { + case '{': + reader.beginObject(); + return hashObject(reader); + case '[': + reader.beginArray(); + return hashArray(reader); + case '"': + return mix(stringHash(reader.readString())); + case 't': + case 'f': + return reader.readBoolean() ? TRUE_HASH : FALSE_HASH; + case 'n': + if (reader.isNullValue()) { + return NULL_HASH; + } + throw new IllegalStateException("Invalid null token"); + default: + return mix(stringHash(reader.readDecimal().toString())); } + } + + private static long hashValue(JsonReader reader) { + JsonReader.Token token = reader.currentToken(); switch (token) { - case START_OBJECT: - return hashObject(parser); - case START_ARRAY: - return hashArray(parser); - case VALUE_STRING: - return mix(stringHash(parser.getText())); - case VALUE_NUMBER_INT: - case VALUE_NUMBER_FLOAT: - // Use text representation for numeric consistency across int/long/double - return mix(stringHash(parser.getText())); - case VALUE_TRUE: - return 0x9E3779B97F4A7C15L; - case VALUE_FALSE: - return 0x517CC1B727220A95L; - case VALUE_NULL: - return 0x6C62272E07BB0142L; + case BEGIN_OBJECT: + reader.beginObject(); + return hashObject(reader); + case BEGIN_ARRAY: + reader.beginArray(); + return hashArray(reader); + case STRING: + return mix(stringHash(reader.readString())); + case NUMBER: + return mix(stringHash(reader.readDecimal().toString())); + case BOOLEAN: + return reader.readBoolean() ? TRUE_HASH : FALSE_HASH; + case NULL: + if (reader.isNullValue()) { + return NULL_HASH; + } + throw new IllegalStateException("Invalid null token"); default: - return 0L; + throw new IllegalStateException("Unhandled token " + token); } } - // Type markers to distinguish empty object {}, empty array [], and null - private static final long OBJECT_SEED = 0x7A5662B4E8B10FA3L; - private static final long ARRAY_SEED = 0x3C6EF372FE94F82BL; - - /** - * Hash an object using commutative addition of entry hashes. - * Addition is commutative (a + b == b + a), so the result is - * independent of the order in which keys appear in the JSON. - */ - private static long hashObject(JsonParser parser) throws IOException { + private static long hashObject(JsonReader reader) { long hash = OBJECT_SEED; - while (parser.nextToken() != JsonToken.END_OBJECT) { - long keyHash = stringHash(parser.currentName()); - parser.nextToken(); - long valueHash = computeHash(parser); - // Mix key+value into a single entry hash, then add (commutative) + while (reader.hasNextField()) { + String key = reader.nextField(); + long keyHash = stringHash(key); + long valueHash = hashValue(reader); hash += mix(keyHash * 0x9E3779B97F4A7C15L + valueHash); } + reader.endObject(); return hash; } - /** - * Hash an array using position-dependent combination. - * Array element order IS semantically significant in JSON. - */ - private static long hashArray(JsonParser parser) throws IOException { + private static long hashArray(JsonReader reader) { long hash = ARRAY_SEED; - while (parser.nextToken() != JsonToken.END_ARRAY) { - hash = hash * 31 + computeHash(parser); + while (reader.hasNextElement()) { + hash = hash * 31 + hashValue(reader); } + reader.endArray(); return mix(hash); } + private static char firstNonWhitespace(String json) { + for (int i = 0; i < json.length(); i++) { + char c = json.charAt(i); + if (!Character.isWhitespace(c)) { + return c; + } + } + return 0; + } + /** * 64-bit FNV-1a inspired string hash for better distribution than String.hashCode(). */ @@ -113,11 +129,7 @@ private static long stringHash(String s) { } /** - * Mixing/finalizer function to improve hash distribution and break - * additive symmetry (prevents collisions when values are swapped between keys). - *

- * This is fmix64 from MurmurHash3 by Austin Appleby (public domain). - * See: https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp + * Mixing/finalizer function to improve hash distribution and break additive symmetry. */ private static long mix(long h) { h ^= (h >>> 33); diff --git a/ebean-core/src/main/java/module-info.java b/ebean-core/src/main/java/module-info.java index 1e143767fc..be16d66e46 100644 --- a/ebean-core/src/main/java/module-info.java +++ b/ebean-core/src/main/java/module-info.java @@ -29,12 +29,12 @@ requires org.antlr.antlr4.runtime; requires io.avaje.classpath.scanner.api; requires io.avaje.classpath.scanner; + requires io.avaje.json; requires io.ebean.types; requires static io.avaje.jsr305x; requires static io.ebean.core.json; requires static com.fasterxml.jackson.annotation; - requires static com.fasterxml.jackson.core; requires static com.fasterxml.jackson.databind; requires static jakarta.validation; requires static jakarta.transaction; diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/JsonTester.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/JsonTester.java index 5e8ee63a42..a9afc6af8e 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/JsonTester.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/JsonTester.java @@ -1,9 +1,8 @@ package io.ebeaninternal.server.type; -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 io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; +import io.avaje.json.stream.JsonStream; import io.ebean.config.JsonConfig; import io.ebean.core.type.ScalarType; import io.ebeaninternal.server.json.WriteJson; @@ -12,13 +11,14 @@ import java.io.StringWriter; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Base class to help json testing. */ public class JsonTester { - protected JsonFactory factory = new JsonFactory(); + protected JsonStream jsonStream = JsonStream.builder().build(); protected ScalarType type; @@ -29,24 +29,21 @@ public JsonTester(ScalarType type) { public String test(T value) throws IOException { StringWriter writer = new StringWriter(); - JsonGenerator generator = factory.createGenerator(writer); - generator.writeStartObject(); - + JsonWriter generator = jsonStream.writer(writer); + generator.beginObject(); WriteJson writeJson = new WriteJson(generator, JsonConfig.Include.ALL); writeJson.writeFieldName("key"); type.jsonWrite(generator, value); - generator.writeEndObject(); + generator.endObject(); generator.flush(); - JsonParser parser = factory.createParser(writer.toString()); - JsonToken token = parser.nextToken(); - assertEquals(JsonToken.START_OBJECT, token); - token = parser.nextToken(); - assertEquals(JsonToken.FIELD_NAME, token); - parser.nextToken(); - + JsonReader parser = jsonStream.reader(writer.toString()); + parser.beginObject(); + assertTrue(parser.hasNextField()); + assertEquals("key", parser.nextField()); T val1 = type.jsonRead(parser); assertEquals(value, val1); + parser.endObject(); return writer.toString(); } diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeArrayListTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeArrayListTest.java index 1da2f57732..3ea8ec65b9 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeArrayListTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeArrayListTest.java @@ -1,8 +1,7 @@ package io.ebeaninternal.server.type; - -import com.fasterxml.jackson.core.JsonParser; -import io.ebean.DB; +import io.avaje.json.JsonReader; +import io.avaje.json.stream.JsonStream; import io.ebean.core.type.DataReader; import io.ebean.core.type.ScalarType; import io.ebean.text.json.EJson; @@ -10,7 +9,6 @@ import org.mockito.Mockito; import java.io.IOException; -import java.io.StringReader; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; @@ -143,9 +141,10 @@ public void jsonRead_withUuidType() throws IOException { input.add(UUID.randomUUID()); String asJson = EJson.write(input); - JsonParser parser = DB.json().createParser(new StringReader(asJson)); - - Object parsed = scalarType.jsonRead(parser); - assertThat(parsed).isEqualTo(input); + try (JsonReader parser = JsonStream.builder().build().reader(asJson)) { + parser.beginArray(); + Object parsed = scalarType.jsonRead(parser); + assertThat(parsed).isEqualTo(input); + } } } diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeArraySetH2Test.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeArraySetH2Test.java index d8c2ea53d7..05b66ae90f 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeArraySetH2Test.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeArraySetH2Test.java @@ -1,13 +1,12 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonParser; -import io.ebean.DB; +import io.avaje.json.JsonReader; +import io.avaje.json.stream.JsonStream; import io.ebean.core.type.ScalarType; import io.ebean.text.json.EJson; import org.junit.jupiter.api.Test; import java.io.IOException; -import java.io.StringReader; import java.sql.SQLException; import java.util.LinkedHashSet; import java.util.Set; @@ -130,9 +129,10 @@ public void jsonRead_withUuidType() throws IOException { input.add(UUID.randomUUID()); String asJson = EJson.write(input); - JsonParser parser = DB.json().createParser(new StringReader(asJson)); - - Object parsed = scalarType.jsonRead(parser); - assertThat(parsed).isEqualTo(input); + try (JsonReader parser = JsonStream.builder().build().reader(asJson)) { + parser.beginArray(); + Object parsed = scalarType.jsonRead(parser); + assertThat(parsed).isEqualTo(input); + } } } diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeBooleanTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeBooleanTest.java index f7a659df31..9e995d66df 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeBooleanTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeBooleanTest.java @@ -1,12 +1,7 @@ package io.ebeaninternal.server.type; - -import io.ebean.DB; -import io.ebean.text.json.JsonContext; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.tests.model.basic.TOne; - +import java.io.IOException; import java.sql.Types; import static org.assertj.core.api.Assertions.assertThat; @@ -14,34 +9,16 @@ public class ScalarTypeBooleanTest { - JsonContext jsonContext = DB.getDefault().json(); - @Test - public void json_true() { - - TOne bean = new TOne(); - bean.setId(42); - bean.setActive(true); - - String json = jsonContext.toJson(bean); - TOne tOne = jsonContext.toBean(TOne.class, json); - - Assertions.assertTrue(tOne.isActive()); - assertEquals(json, "{\"id\":42,\"active\":true}"); + public void json_true() throws IOException { + String json = new JsonTester<>(new ScalarTypeBoolean.Native()).test(true); + assertEquals(json, "{\"key\":true}"); } @Test - public void json_false() { - - TOne bean = new TOne(); - bean.setId(42); - bean.setActive(false); - - String json = jsonContext.toJson(bean); - TOne tOne = jsonContext.toBean(TOne.class, json); - - Assertions.assertFalse(tOne.isActive()); - assertEquals(json, "{\"id\":42,\"active\":false}"); + public void json_false() throws IOException { + String json = new JsonTester<>(new ScalarTypeBoolean.Native()).test(false); + assertEquals(json, "{\"key\":false}"); } diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java index f645ab3cba..2614fd98f3 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java @@ -1,8 +1,8 @@ package io.ebeaninternal.server.type; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; +import io.avaje.json.stream.JsonStream; import io.ebean.config.JsonConfig; import org.junit.jupiter.api.Test; @@ -21,7 +21,7 @@ public class ScalarTypeLocalDateTimeTest { private final ScalarTypeLocalDateTime type = new ScalarTypeLocalDateTime(JsonConfig.DateTime.MILLIS); - private final JsonFactory factory = new JsonFactory(); + private final JsonStream jsonStream = JsonStream.builder().build(); // warm up private final LocalDateTime warmUp = LocalDateTime.now(); @@ -91,7 +91,7 @@ public void testJsonRaw() throws Exception { ScalarTypeLocalDateTime typeIso = new ScalarTypeLocalDateTime(JsonConfig.DateTime.ISO8601); StringWriter writer = new StringWriter(); - JsonGenerator generator = factory.createGenerator(writer); + JsonWriter generator = jsonStream.writer(writer); typeIso.jsonWrite(generator, of); generator.flush(); @@ -132,18 +132,15 @@ public void isoJsonFormatParse() { @Test public void testParseEbean11() throws IOException { ScalarTypeLocalDateTime type = new ScalarTypeLocalDateTime(JsonConfig.DateTime.ISO8601); - JsonFactory factory = new JsonFactory(); - JsonParser parser11 = factory.createParser("1517627106000"); // its a number! - JsonParser parser13 = factory.createParser("\"2022-01-01T01:00:00\""); + JsonReader parser11 = jsonStream.reader("1517627106000"); // ebean 11 style number // test parsing an ebean 11/13 timestamp, we do not expect an exception LocalDateTime p = type.parse("1517627106000"); - parser11.nextToken(); LocalDateTime q = type.jsonRead(parser11); assertThat(p).isEqualTo(q); + JsonReader parser13 = jsonStream.reader("\"2022-01-01T01:00:00\""); p = type.parse("2022-01-01T01:00:00"); - parser13.nextToken(); q = type.jsonRead(parser13); assertThat(p).isEqualTo(q); TimeZone tz = TimeZone.getDefault(); diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypePostgresHstoreTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypePostgresHstoreTest.java index 4f2ddd6ab3..4342a77aa8 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypePostgresHstoreTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypePostgresHstoreTest.java @@ -1,9 +1,9 @@ package io.ebeaninternal.server.type; -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 io.avaje.json.JsonReader; +import io.avaje.json.JsonReader.Token; +import io.avaje.json.JsonWriter; +import io.avaje.json.stream.JsonStream; import io.ebeaninternal.json.ModifyAwareMap; import org.junit.jupiter.api.Test; @@ -19,7 +19,7 @@ public class ScalarTypePostgresHstoreTest { ScalarTypePostgresHstore hstore = new ScalarTypePostgresHstore(); - JsonFactory jsonFactory = new JsonFactory(); + JsonStream jsonStream = JsonStream.builder().build(); @Test public void testIsMutable() { @@ -72,25 +72,25 @@ public void testJsonRead() throws Exception { @SuppressWarnings("unchecked") private Map parseHstore(String json) throws IOException { - JsonParser parser = jsonFactory.createParser(json); + JsonReader parser = jsonStream.reader(json); // BeanProperty reads the first token checking for null so // simulate that here - JsonToken token = parser.nextToken(); - assertEquals(JsonToken.START_OBJECT, token); + Token token = parser.currentToken(); + assertEquals(Token.BEGIN_OBJECT, token); return (Map) hstore.jsonRead(parser); } private String generateJson(Map map) throws IOException { StringWriter writer = new StringWriter(); - JsonGenerator generator = jsonFactory.createGenerator(writer); + JsonWriter generator = jsonStream.writer(writer); // wrap in an object to form proper json - generator.writeStartObject(); - generator.writeFieldName("key"); + generator.beginObject(); + generator.name("key"); hstore.jsonWrite(generator, map); - generator.writeEndObject(); + generator.endObject(); generator.flush(); return writer.toString(); diff --git a/ebean-jackson-mapper/pom.xml b/ebean-jackson-mapper/pom.xml index de8bb351cf..2de75da0e4 100644 --- a/ebean-jackson-mapper/pom.xml +++ b/ebean-jackson-mapper/pom.xml @@ -19,6 +19,13 @@ provided + + io.avaje + avaje-json-core + ${avaje-json-core.version} + provided + + com.fasterxml.jackson.core jackson-core diff --git a/ebean-jackson-mapper/src/main/java/io/ebean/jackson/mapper/ScalarJsonJacksonMapper.java b/ebean-jackson-mapper/src/main/java/io/ebean/jackson/mapper/ScalarJsonJacksonMapper.java index dad6d8bca3..31af96e496 100644 --- a/ebean-jackson-mapper/src/main/java/io/ebean/jackson/mapper/ScalarJsonJacksonMapper.java +++ b/ebean-jackson-mapper/src/main/java/io/ebean/jackson/mapper/ScalarJsonJacksonMapper.java @@ -1,13 +1,13 @@ package io.ebean.jackson.mapper; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.introspect.AnnotatedClass; import com.fasterxml.jackson.databind.introspect.AnnotatedField; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.annotation.MutationDetection; import io.ebean.core.type.*; import io.ebean.text.TextException; @@ -221,13 +221,20 @@ public final DocPropertyType docType() { } @Override - public final T jsonRead(JsonParser parser) throws IOException { - return objectReader.readValue(parser, deserType); + public final T jsonRead(JsonReader parser) throws IOException { + if (parser.isNullValue()) { + return null; + } + return objectReader.readValue(parser.readRaw(), deserType); } @Override - public final void jsonWrite(JsonGenerator writer, T value) throws IOException { - objectWriter.writeValue(writer, value); + public final void jsonWrite(JsonWriter writer, T value) throws IOException { + if (value == null) { + writer.nullValue(); + } else { + writer.rawValue(objectWriter.writeValueAsString(value)); + } } @Override diff --git a/ebean-jackson-mapper/src/main/java/module-info.java b/ebean-jackson-mapper/src/main/java/module-info.java index 7adf6352a6..7e4d4b2395 100644 --- a/ebean-jackson-mapper/src/main/java/module-info.java +++ b/ebean-jackson-mapper/src/main/java/module-info.java @@ -2,6 +2,7 @@ module io.ebean.jackson.mapper { + requires io.avaje.json; requires io.ebean.core.type; requires com.fasterxml.jackson.annotation; requires com.fasterxml.jackson.core; diff --git a/ebean-net-postgis-types/src/main/java/io/ebean/postgis/ScalarTypePgisBase.java b/ebean-net-postgis-types/src/main/java/io/ebean/postgis/ScalarTypePgisBase.java index 8ed62a3874..68b24773bd 100644 --- a/ebean-net-postgis-types/src/main/java/io/ebean/postgis/ScalarTypePgisBase.java +++ b/ebean-net-postgis-types/src/main/java/io/ebean/postgis/ScalarTypePgisBase.java @@ -1,7 +1,7 @@ package io.ebean.postgis; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -107,12 +107,12 @@ public DocPropertyType docType() { } @Override - public T jsonRead(JsonParser parser) { + public T jsonRead(JsonReader parser) { return null; } @Override - public void jsonWrite(JsonGenerator writer, T value) { + public void jsonWrite(JsonWriter writer, T value) { } } diff --git a/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGbase.java b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGbase.java index 4bb7baa34a..ec4e2d60a5 100644 --- a/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGbase.java +++ b/ebean-pgvector-types/src/main/java/io/ebean/pgvector/ScalarTypePGbase.java @@ -1,7 +1,7 @@ package io.ebean.pgvector; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -85,12 +85,12 @@ public DocPropertyType docType() { } @Override - public T jsonRead(JsonParser parser) { + public T jsonRead(JsonReader parser) { return null; } @Override - public void jsonWrite(JsonGenerator writer, T value) { + public void jsonWrite(JsonWriter writer, T value) { } } diff --git a/ebean-postgis-types/src/main/java/io/ebean/postgis/ScalarTypePgisBase.java b/ebean-postgis-types/src/main/java/io/ebean/postgis/ScalarTypePgisBase.java index 243b8cba76..6669cdb18f 100644 --- a/ebean-postgis-types/src/main/java/io/ebean/postgis/ScalarTypePgisBase.java +++ b/ebean-postgis-types/src/main/java/io/ebean/postgis/ScalarTypePgisBase.java @@ -1,7 +1,7 @@ package io.ebean.postgis; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -107,12 +107,12 @@ public DocPropertyType docType() { } @Override - public T jsonRead(JsonParser parser) { + public T jsonRead(JsonReader parser) { return null; } @Override - public void jsonWrite(JsonGenerator writer, T value) { + public void jsonWrite(JsonWriter writer, T value) { } } diff --git a/ebean-postgis-types/src/main/java/io/ebean/postgis/latte/ScalarTypeGeoLatteBase.java b/ebean-postgis-types/src/main/java/io/ebean/postgis/latte/ScalarTypeGeoLatteBase.java index cd6a740546..df84f4ae6e 100644 --- a/ebean-postgis-types/src/main/java/io/ebean/postgis/latte/ScalarTypeGeoLatteBase.java +++ b/ebean-postgis-types/src/main/java/io/ebean/postgis/latte/ScalarTypeGeoLatteBase.java @@ -1,7 +1,7 @@ package io.ebean.postgis.latte; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -103,12 +103,12 @@ public DocPropertyType docType() { } @Override - public T jsonRead(JsonParser parser) { + public T jsonRead(JsonReader parser) { return null; } @Override - public void jsonWrite(JsonGenerator writer, T value) { + public void jsonWrite(JsonWriter writer, T value) { } } diff --git a/ebean-test/src/main/java/io/ebean/test/DbJson.java b/ebean-test/src/main/java/io/ebean/test/DbJson.java index 3675063c11..09597b2977 100644 --- a/ebean-test/src/main/java/io/ebean/test/DbJson.java +++ b/ebean-test/src/main/java/io/ebean/test/DbJson.java @@ -102,8 +102,8 @@ public PrettyJson withPlaceholder(String placeHolder) { public PrettyJson replace(String... propertyNames) { for (String propertyName : propertyNames) { String placeholder = "\"" + propertyName + "\": " + placeHolder; - rawJson = rawJson.replaceAll("\"" + propertyName + "\": (\\d+)", placeholder); - rawJson = rawJson.replaceAll("\"" + propertyName + "\": \"(.*?)\"", placeholder); + rawJson = rawJson.replaceAll("\"" + propertyName + "\":\\s*(\\d+)", placeholder); + rawJson = rawJson.replaceAll("\"" + propertyName + "\":\\s*\"(.*?)\"", placeholder); } return this; } @@ -134,7 +134,9 @@ public void assertContentMatches(String resourcePath) { * Normalise line ending characters to just use new line. */ private String lineEnd(String content) { - return content.replace("\r\n", "\n"); + return content + .replace("\r\n", "\n") + .replaceAll("\"([^\"]+)\":\\s+", "\"$1\": "); } /** diff --git a/ebean-test/src/test/java/io/ebean/xtest/internal/server/text/json/DJsonScalarTest.java b/ebean-test/src/test/java/io/ebean/xtest/internal/server/text/json/DJsonScalarTest.java index e39ee30dbd..e90f37f77a 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/internal/server/text/json/DJsonScalarTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/internal/server/text/json/DJsonScalarTest.java @@ -1,7 +1,7 @@ package io.ebean.xtest.internal.server.text.json; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; +import io.avaje.json.JsonWriter; +import io.avaje.json.stream.JsonStream; import io.ebean.DatabaseBuilder; import io.ebean.config.DatabaseConfig; import io.ebean.platform.h2.H2Platform; @@ -22,6 +22,7 @@ public class DJsonScalarTest { private final DJsonScalar jsonScalar; + private final JsonStream jsonStream = JsonStream.builder().build(); public DJsonScalarTest() { var serverConfig = new DatabaseConfig(); @@ -33,37 +34,35 @@ public DJsonScalarTest() { @Test public void writeBasicTypes() throws IOException { StringWriter writer = new StringWriter(); - JsonGenerator generator = createGenerator(writer); + JsonWriter generator = createGenerator(writer); UUID uuid = UUID.randomUUID(); LocalDate today = LocalDate.now(); - generator.writeRaw("["); + generator.rawChunk('['); jsonScalar.write(generator, "hello"); - generator.writeRaw(","); + generator.rawChunk(','); jsonScalar.write(generator, uuid); - generator.writeRaw(","); + generator.rawChunk(','); jsonScalar.write(generator, today); - generator.writeRaw("]"); + generator.rawChunk(']'); generator.flush(); - generator.close(); String json = writer.toString(); assertThat(json).contains("hello"); assertThat(json).contains(uuid.toString()); } - private JsonGenerator createGenerator(StringWriter writer) throws IOException { - JsonFactory factory = new JsonFactory(); - return factory.createGenerator(writer); + private JsonWriter createGenerator(StringWriter writer) { + return jsonStream.writer(writer); } @Test public void writeDbArrayTypes() throws IOException { StringWriter writer = new StringWriter(); - JsonGenerator generator = createGenerator(writer); + JsonWriter generator = createGenerator(writer); List list = new ArrayList<>(); list.add(UUID.randomUUID()); @@ -72,10 +71,9 @@ public void writeDbArrayTypes() throws IOException { jsonScalar.write(generator, list); generator.flush(); - generator.close(); String json = writer.toString(); - assertThat(json).isEqualTo("[\""+list.get(0)+"\", \""+list.get(1)+"\"]"); + assertThat(json).isEqualTo("[\"" + list.get(0) + "\",\"" + list.get(1) + "\"]"); } } diff --git a/ebean-test/src/test/java/io/ebean/xtest/internal/server/text/json/WriteJsonDirtyTest.java b/ebean-test/src/test/java/io/ebean/xtest/internal/server/text/json/WriteJsonDirtyTest.java index f18a5cb827..0833fb4393 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/internal/server/text/json/WriteJsonDirtyTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/internal/server/text/json/WriteJsonDirtyTest.java @@ -1,7 +1,7 @@ package io.ebean.xtest.internal.server.text.json; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; +import io.avaje.json.JsonWriter; +import io.avaje.json.stream.JsonStream; import io.ebean.DB; import io.ebean.bean.EntityBean; import io.ebeaninternal.api.SpiEbeanServer; @@ -20,6 +20,8 @@ public class WriteJsonDirtyTest { + private static final JsonStream JSON_STREAM = JsonStream.builder().build(); + @Test public void test() throws IOException { @@ -40,14 +42,12 @@ public void test() throws IOException { boolean[] dirtyProperties = entityBean._ebean_getIntercept().dirtyProperties(); StringWriter writer = new StringWriter(); - JsonFactory jsonFactory = new JsonFactory(); - JsonGenerator generator = jsonFactory.createGenerator(writer); + JsonWriter generator = JSON_STREAM.writer(writer); WriteJson writeJson = new WriteJson(server, generator, null, null, null, null, true); descriptor.jsonWriteDirty(writeJson, entityBean, dirtyProperties); generator.flush(); - generator.close(); String jsonContent = writer.toString(); assertTrue(jsonContent.contains("\"name\":")); diff --git a/ebean-test/src/test/java/io/ebean/xtest/internal/server/text/json/WriteJsonTest.java b/ebean-test/src/test/java/io/ebean/xtest/internal/server/text/json/WriteJsonTest.java index ce06258271..629d79eab5 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/internal/server/text/json/WriteJsonTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/internal/server/text/json/WriteJsonTest.java @@ -28,8 +28,8 @@ void test_push() { String json = DB.json().toJsonPretty(list); - assertThat(json).contains("\"customer\": {"); - assertThat(json).contains("\"billingAddress\": {"); - assertThat(json).contains("\"details\": [ {"); + assertThat(json).contains("\"customer\":"); + assertThat(json).contains("\"billingAddress\":"); + assertThat(json).contains("\"details\": ["); } } diff --git a/ebean-test/src/test/java/io/ebean/xtest/json/EJsonTests.java b/ebean-test/src/test/java/io/ebean/xtest/json/EJsonTests.java index 7e721333f5..c31e61bc0d 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/json/EJsonTests.java +++ b/ebean-test/src/test/java/io/ebean/xtest/json/EJsonTests.java @@ -1,7 +1,7 @@ package io.ebean.xtest.json; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.stream.JsonStream; import io.ebean.ModifyAwareType; import io.ebean.text.json.EJson; import io.ebean.util.IOUtils; @@ -20,14 +20,13 @@ public class EJsonTests { + private static final JsonStream JSON_STREAM = JsonStream.builder().build(); + @Test public void test_map_simple() throws IOException { - JsonFactory factory = new JsonFactory(); - String jsonInput = "{\"name\":\"rob\",\"age\":12}"; - - JsonParser jsonParser = factory.createParser(jsonInput); + JsonReader jsonParser = JSON_STREAM.reader(jsonInput); Object result = EJson.parse(jsonParser); @@ -56,11 +55,8 @@ public void write_withWriter_expect_writerNotClosed() throws IOException { @Test public void test_parseObject() throws IOException { - JsonFactory factory = new JsonFactory(); - String jsonInput = "{\"name\":\"rob\",\"age\":12}"; - - JsonParser jsonParser = factory.createParser(jsonInput); + JsonReader jsonParser = JSON_STREAM.reader(jsonInput); Map map = EJson.parseObject(jsonParser); @@ -168,8 +164,7 @@ public void test_list_jsonParser() throws IOException { String jsonInput = "[\"name\",\"rob\",12,13]"; - JsonFactory jsonFactory = new JsonFactory(); - JsonParser parser = jsonFactory.createParser(jsonInput); + JsonReader parser = JSON_STREAM.reader(jsonInput); List list = EJson.parseList(parser); diff --git a/ebean-test/src/test/java/io/ebean/xtest/json/JsonBeanReaderTest.java b/ebean-test/src/test/java/io/ebean/xtest/json/JsonBeanReaderTest.java index 9c4cfdc0c1..01b713bcab 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/json/JsonBeanReaderTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/json/JsonBeanReaderTest.java @@ -1,6 +1,6 @@ package io.ebean.xtest.json; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.bean.PersistenceContext; @@ -22,7 +22,7 @@ public class JsonBeanReaderTest extends BaseTestCase { @Test public void read() { - JsonParser parser = getParser(); + JsonReader parser = getParser(); JsonBeanReader beanReader = json.createBeanReader(Customer.class, parser, null); Customer customer = beanReader.read(); @@ -30,7 +30,7 @@ public void read() { assertThat(customer.getName()).isEqualTo("dummy"); } - private JsonParser getParser() { + private JsonReader getParser() { Customer customer = new Customer(); customer.setId(42); @@ -45,11 +45,11 @@ private JsonParser getParser() { @Test public void forJson() { - JsonParser parser = getParser(); + JsonReader parser = getParser(); JsonBeanReader beanReader = json.createBeanReader(Customer.class, parser, null); beanReader.read(); - JsonParser more = getParser(); + JsonReader more = getParser(); JsonBeanReader moreReader = beanReader.forJson(more); Customer customer = moreReader.read(); @@ -60,7 +60,7 @@ public void forJson() { @Test public void persistenceContextPut_when_noPC() throws Exception { - JsonParser parser = getParser(); + JsonReader parser = getParser(); JsonBeanReader beanReader = json.createBeanReader(Customer.class, parser, null); beanReader.read(); @@ -75,7 +75,7 @@ public void persistenceContextPut_when_hasPC() throws Exception { JsonReadOptions options = new JsonReadOptions().setEnableLazyLoading(true); - JsonParser parser = getParser(); + JsonReader parser = getParser(); JsonBeanReader beanReader = json.createBeanReader(Customer.class, parser, options); Customer customer = beanReader.read(); diff --git a/ebean-test/src/test/java/io/ebean/xtest/json/JsonContextTest.java b/ebean-test/src/test/java/io/ebean/xtest/json/JsonContextTest.java index ba9606b16d..c2c5078313 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/json/JsonContextTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/json/JsonContextTest.java @@ -1,6 +1,6 @@ package io.ebean.xtest.json; -import com.fasterxml.jackson.core.JsonGenerator; +import io.avaje.json.JsonWriter; import io.ebean.DB; import io.ebean.text.PathProperties; import io.ebean.text.json.JsonContext; @@ -90,8 +90,8 @@ public void test_toJsonPretty() { .findList(); String json = DB.json().toJsonPretty(orders); - assertThat(json).contains("[ {"); - assertThat(json).contains("\"customer\": {"); + assertThat(json).contains("["); + assertThat(json).contains("\"customer\":"); } @Test @@ -185,7 +185,7 @@ public void testCreateGenerator() throws Exception { StringWriter writer = new StringWriter(); JsonContext json = DB.json(); - JsonGenerator generator = json.createGenerator(writer); + JsonWriter generator = json.createGenerator(writer); Customer customer = new Customer(); customer.setId(1); @@ -194,10 +194,10 @@ public void testCreateGenerator() throws Exception { // we can use the generator before and after our json.toJson() call // ... confirming we are not closing the generator - generator.writeStartArray(); + generator.beginArray(); json.toJson(customer, generator, PathProperties.parse("id,name")); - generator.writeEndArray(); - generator.close(); + generator.endArray(); + generator.flush(); String jsonString = writer.toString(); assertThat(jsonString).startsWith("["); @@ -210,17 +210,18 @@ public void testCreateGenerator_writeRaw() throws Exception { StringWriter writer = new StringWriter(); JsonContext json = DB.json(); - JsonGenerator generator = json.createGenerator(writer); + JsonWriter generator = json.createGenerator(writer); // test that we can write anything via writeRaw() - generator.writeRaw("START"); - generator.writeStartArray(); - generator.writeStartObject(); - generator.writeNumberField("count", 12); - generator.writeEndObject(); - generator.writeEndArray(); - generator.writeRaw("END"); - generator.close(); + generator.rawChunk("START"); + generator.beginArray(); + generator.beginObject(); + generator.name("count"); + generator.value(12); + generator.endObject(); + generator.endArray(); + generator.rawChunk("END"); + generator.flush(); assertEquals("START[{\"count\":12}]END", writer.toString()); } diff --git a/ebean-test/src/test/java/org/tests/text/json/TestJsonBeanDescriptorParse.java b/ebean-test/src/test/java/org/tests/text/json/TestJsonBeanDescriptorParse.java index 96465c4d36..a26eecdf7e 100644 --- a/ebean-test/src/test/java/org/tests/text/json/TestJsonBeanDescriptorParse.java +++ b/ebean-test/src/test/java/org/tests/text/json/TestJsonBeanDescriptorParse.java @@ -1,6 +1,7 @@ package org.tests.text.json; -import com.fasterxml.jackson.core.JsonParser; +import io.avaje.json.JsonReader; +import io.avaje.json.stream.JsonStream; import io.ebean.BeanState; import io.ebean.DB; import io.ebean.xtest.BaseTestCase; @@ -185,9 +186,9 @@ public void testJsonUpdate() { private SpiJsonReader createRead(SpiEbeanServer server, BeanDescriptor descriptor) { StringReader reader = new StringReader("{\"id\":123,\"name\":\"Hello Rob\"}"); - JsonParser parser = server.json().createParser(reader); + JsonReader parser = server.json().createParser(reader); - return new ReadJson(descriptor, parser, null, null, false); + return new ReadJson(descriptor, parser, null, null, false, JsonStream.builder().build()); } } diff --git a/ebean-test/src/test/resources/bean/example-list-contains.json b/ebean-test/src/test/resources/bean/example-list-contains.json index dc7599b10d..5078cbc2fc 100644 --- a/ebean-test/src/test/resources/bean/example-list-contains.json +++ b/ebean-test/src/test/resources/bean/example-list-contains.json @@ -1,9 +1,7 @@ [ { "name": "something", - "other": null, "version": 1 }, { "name": "other", - "other": null, "version": 1 } ] diff --git a/ebean-test/src/test/resources/bean/example-list-match.json b/ebean-test/src/test/resources/bean/example-list-match.json index 8c7b8b6813..9c619f5f15 100644 --- a/ebean-test/src/test/resources/bean/example-list-match.json +++ b/ebean-test/src/test/resources/bean/example-list-match.json @@ -1,13 +1,16 @@ -[ { - "id": "*", - "name": "something", - "other": null, - "whenModified": "*", - "version": 1 -}, { - "id": "*", - "name": "other", - "other": null, - "whenModified": "*", - "version": 1 -} ] +[ + { + "id": "*", + "name": "something", + "other": null, + "whenModified": "*", + "version": 1 + }, + { + "id": "*", + "name": "other", + "other": null, + "whenModified": "*", + "version": 1 + } +] diff --git a/pom.xml b/pom.xml index 2ec4243921..cb625c8764 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,7 @@ 2.22.0 + 3.14 2.2.220 3.1 3.0 From a8189567dd0fb9620eb2f05e47ddcd7907b79e77 Mon Sep 17 00:00:00 2001 From: "robin.bygrave" Date: Fri, 19 Jun 2026 16:19:35 +1200 Subject: [PATCH 2/7] 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 cbec38cd56..ff75c1018a 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 @@ 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 @@ /** * 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 1d40f8f249..0000000000 --- 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 46c7f44349..0000000000 --- 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 0000000000..a4cf4d776c --- /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(); + } +} From bffe741642c7984a15f51d958092774204de8404 Mon Sep 17 00:00:00 2001 From: "robin.bygrave" Date: Fri, 19 Jun 2026 17:27:19 +1200 Subject: [PATCH 3/7] Simplify @DbJson(B) Map/List/Set type handling via JsonStorage strategy Collapse the per-storage and per-platform ScalarType subclass explosion for the built-in JSON value types into two orthogonal strategies. - Add JsonStorage strategy (VARCHAR / CLOB / BLOB / Postgres) encapsulating how the raw JSON string is read from / bound to JDBC. Postgres vs non-Postgres is now a single reusable strategy rather than a subclass per value type. - Add ScalarTypeJsonValue base holding the shared read / bind / L2-cache / json plumbing once, plus ScalarTypeJsonCollectionValue adding the ScalarTypeArray (DB array column definition) aspect for List/Set. - Rewrite ScalarTypeJsonMap, ScalarTypeJsonList and ScalarTypeJsonSet as thin types: a typeFor factory selecting a JsonStorage + value marshalling that delegates to the avaje-JsonMapper-backed EJson facade. - Remove ScalarTypeJsonMapPostgres and ~16 nested storage/platform classes. --- .../server/type/JsonStorage.java | 138 +++++++++++ .../type/PlatformArrayTypeJsonList.java | 2 +- .../server/type/PlatformArrayTypeJsonSet.java | 2 +- .../type/ScalarTypeJsonCollectionValue.java | 32 +++ .../server/type/ScalarTypeJsonList.java | 220 +++++------------ .../server/type/ScalarTypeJsonMap.java | 219 ++--------------- .../type/ScalarTypeJsonMapPostgres.java | 50 ---- .../server/type/ScalarTypeJsonSet.java | 228 +++++------------- .../server/type/ScalarTypeJsonValue.java | 128 ++++++++++ 9 files changed, 453 insertions(+), 566 deletions(-) create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/type/JsonStorage.java create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonCollectionValue.java delete mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonValue.java diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/JsonStorage.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/JsonStorage.java new file mode 100644 index 0000000000..c397c619e3 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/JsonStorage.java @@ -0,0 +1,138 @@ +package io.ebeaninternal.server.type; + +import io.ebean.core.type.DataBinder; +import io.ebean.core.type.DataReader; +import io.ebean.core.type.PostgresHelper; +import io.ebean.util.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.sql.Types; + +/** + * Strategy for reading and binding the raw JSON string of a {@code @DbJson} property. + *

+ * This collapses the previously duplicated VARCHAR / CLOB / BLOB and Postgres JSON / JSONB + * variants (which differed only in how the raw JSON is read from / bound to JDBC) into a + * small set of reusable strategies shared by all JSON value types (Map, List, Set). + */ +interface JsonStorage { + + /** VARCHAR storage - read/bind as a String. */ + JsonStorage VARCHAR = new Varchar(); + /** CLOB storage - read via stream, bind as a String. */ + JsonStorage CLOB = new Clob(); + /** BLOB storage - read/bind as UTF-8 bytes. */ + JsonStorage BLOB = new Blob(); + + /** Postgres JSON / JSONB storage binding via a PGobject of the given type. */ + static JsonStorage postgres(String pgType) { + return new Postgres(pgType); + } + + /** + * Read the raw JSON content (or null) from the DB. + */ + String read(DataReader reader) throws SQLException; + + /** + * Bind the given non-null raw JSON content. + */ + void bind(DataBinder binder, String rawJson) throws SQLException; + + /** + * Bind an SQL null value. + */ + void bindNull(DataBinder binder) throws SQLException; + + final class Varchar implements JsonStorage { + @Override + public String read(DataReader reader) throws SQLException { + return reader.getString(); + } + + @Override + public void bind(DataBinder binder, String rawJson) throws SQLException { + binder.setString(rawJson); + } + + @Override + public void bindNull(DataBinder binder) throws SQLException { + binder.setNull(Types.VARCHAR); + } + } + + final class Clob implements JsonStorage { + @Override + public String read(DataReader reader) throws SQLException { + return reader.getStringFromStream(); + } + + @Override + public void bind(DataBinder binder, String rawJson) throws SQLException { + binder.setString(rawJson); + } + + @Override + public void bindNull(DataBinder binder) throws SQLException { + binder.setNull(Types.VARCHAR); + } + } + + final class Blob implements JsonStorage { + @Override + public String read(DataReader reader) throws SQLException { + InputStream is = reader.getBinaryStream(); + if (is == null) { + return null; + } + try (Reader streamReader = IOUtils.newReader(is)) { + StringBuilder builder = new StringBuilder(); + char[] buffer = new char[2048]; + int nRead; + while ((nRead = streamReader.read(buffer)) >= 0) { + builder.append(buffer, 0, nRead); + } + return builder.toString(); + } catch (IOException e) { + throw new SQLException("Error reading Blob stream from DB", e); + } + } + + @Override + public void bind(DataBinder binder, String rawJson) throws SQLException { + binder.setBytes(rawJson.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void bindNull(DataBinder binder) throws SQLException { + binder.setNull(Types.BLOB); + } + } + + final class Postgres implements JsonStorage { + private final String pgType; + + Postgres(String pgType) { + this.pgType = pgType; + } + + @Override + public String read(DataReader reader) throws SQLException { + return reader.getString(); + } + + @Override + public void bind(DataBinder binder, String rawJson) throws SQLException { + binder.setObject(PostgresHelper.asObject(pgType, rawJson)); + } + + @Override + public void bindNull(DataBinder binder) throws SQLException { + binder.setObject(PostgresHelper.asObject(pgType, null)); + } + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonList.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonList.java index c48b0343cf..5df3364410 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonList.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonList.java @@ -30,7 +30,7 @@ public ScalarType typeFor(Type valueType, boolean nullable) { // TODO: keepSource for @DbArray? return new ScalarTypeJsonList.VarcharWithConverter(DocPropertyType.UUID, nullable, false, ArrayElementConverter.UUID); } - return new ScalarTypeJsonList.Varchar(docType(valueType), nullable, false); + return new ScalarTypeJsonList(java.sql.Types.VARCHAR, JsonStorage.VARCHAR, docType(valueType), nullable, false); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonSet.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonSet.java index d67722b8f6..314efc4747 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonSet.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonSet.java @@ -30,7 +30,7 @@ public ScalarType typeFor(Type valueType, boolean nullable) { // TODO: keepSource for @DbArray? return new ScalarTypeJsonSet.VarcharWithConverter(DocPropertyType.UUID, nullable, false, ArrayElementConverter.UUID); } - return new ScalarTypeJsonSet.Varchar(docType(valueType), nullable, false); + return new ScalarTypeJsonSet(java.sql.Types.VARCHAR, JsonStorage.VARCHAR, docType(valueType), nullable, false); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonCollectionValue.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonCollectionValue.java new file mode 100644 index 0000000000..052638444a --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonCollectionValue.java @@ -0,0 +1,32 @@ +package io.ebeaninternal.server.type; + +import io.ebean.core.type.DocPropertyType; + +/** + * Base for the JSON collection value types (List, Set). + *

+ * Adds the {@link ScalarTypeArray} aspect (logical DB array column definition) used when the + * same type backs a {@code @DbArray} mapping on a platform without native array support. + */ +abstract class ScalarTypeJsonCollectionValue extends ScalarTypeJsonValue implements ScalarTypeArray { + + ScalarTypeJsonCollectionValue(Class type, int jdbcType, JsonStorage storage, boolean keepSource, + boolean nullable, DocPropertyType docType) { + super(type, jdbcType, storage, keepSource, nullable, "[]", docType); + } + + @Override + public final String getDbColumnDefn() { + switch (docType()) { + case SHORT: + case INTEGER: + case LONG: + return "integer[]"; + case DOUBLE: + case FLOAT: + return "decimal[]"; + default: + return "varchar[]"; + } + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonList.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonList.java index 51da2bbc26..d994b214d0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonList.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonList.java @@ -3,216 +3,120 @@ import io.avaje.json.JsonReader; import io.avaje.json.JsonWriter; import io.ebean.config.dbplatform.DbPlatformType; -import io.ebean.core.type.*; +import io.ebean.core.type.DocPropertyType; +import io.ebean.core.type.PostgresHelper; +import io.ebean.core.type.ScalarType; import io.ebean.text.TextException; import io.ebean.text.json.EJson; import io.ebeaninternal.json.ModifyAwareList; import jakarta.persistence.PersistenceException; import java.io.IOException; -import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; import java.util.List; /** - * Types for mapping List in JSON format to DB types VARCHAR, JSON and JSONB. + * Type which maps a List in JSON format to VARCHAR or Postgres JSON / JSONB. */ -final class ScalarTypeJsonList { +@SuppressWarnings("rawtypes") +class ScalarTypeJsonList extends ScalarTypeJsonCollectionValue { /** - * Return the appropriate ScalarType based requested dbType and if Postgres. + * Return the appropriate ScalarType for the requested dbType and platform. */ static ScalarType typeFor(boolean postgres, int dbType, DocPropertyType docType, boolean nullable, boolean keepSource) { if (postgres) { switch (dbType) { case DbPlatformType.JSONB: - return new ScalarTypeJsonList.JsonB(docType, nullable, keepSource); + return new ScalarTypeJsonList(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), docType, nullable, keepSource); case DbPlatformType.JSON: - return new ScalarTypeJsonList.Json(docType, nullable, keepSource); + return new ScalarTypeJsonList(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), docType, nullable, keepSource); } } - return new ScalarTypeJsonList.Varchar(docType, nullable, keepSource); + return new ScalarTypeJsonList(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource); } - @SuppressWarnings("rawtypes") - static final class VarcharWithConverter extends ScalarTypeJsonList.Base { - private final ArrayElementConverter converter; - - VarcharWithConverter(DocPropertyType docType, boolean nullable, boolean keepSource, ArrayElementConverter converter) { - super(Types.VARCHAR, docType, nullable, keepSource); - this.converter = converter; - } + ScalarTypeJsonList(int jdbcType, JsonStorage storage, DocPropertyType docType, boolean nullable, boolean keepSource) { + super(List.class, jdbcType, storage, keepSource, nullable, docType); + } - @Override - List readJsonConvert(String json) { - try { - return convertElements(EJson.parseList(json, false)); - } catch (IOException e) { - throw new TextException("Failed to parse JSON [{}] as List", json, e); - } + @Override + List readJson(String rawJson) { + try { + // parse JSON into a modifyAware list + return EJson.parseList(rawJson, true); + } catch (IOException e) { + throw new TextException("Failed to parse JSON [{}] as List", rawJson, e); } + } - @SuppressWarnings("unchecked") - private List convertElements(List rawList) { - if (rawList == null) { - return null; - } - final List result = new ArrayList<>(rawList.size()); - for (Object o : rawList) { - result.add(converter.fromSerialized(o)); - } - return new ModifyAwareList(result); - } - - @Override - public List parse(String value) { - try { - return convertElements(EJson.parseList(value, false)); - } catch (IOException e) { - throw new TextException("Failed to parse JSON [{}] as List", value, e); - } + @Override + public List parse(String value) { + try { + return EJson.parseList(value, false); + } catch (IOException e) { + throw new TextException("Failed to parse JSON [{}] as List", value, e); } } - /** - * List mapped to DB VARCHAR. - */ - static final class Varchar extends ScalarTypeJsonList.Base { - Varchar(DocPropertyType docType, boolean nullable, boolean keepSource) { - super(Types.VARCHAR, docType, nullable, keepSource); + @Override + public String formatValue(List value) { + try { + return EJson.write(value); + } catch (IOException e) { + throw new PersistenceException("Failed to format List into JSON content", e); } } - /** - * List mapped to Postgres JSON. - */ - private final static class Json extends ScalarTypeJsonList.PgBase { - Json(DocPropertyType docType, boolean nullable, boolean keepSource) { - super(DbPlatformType.JSON, PostgresHelper.JSON_TYPE, docType, nullable, keepSource); - } + @Override + public List jsonRead(JsonReader parser) throws IOException { + return EJson.parseList(parser, parser.currentToken()); } - /** - * List mapped to Postgres JSONB. - */ - private static final class JsonB extends ScalarTypeJsonList.PgBase { - JsonB(DocPropertyType docType, boolean nullable, boolean keepSource) { - super(DbPlatformType.JSONB, PostgresHelper.JSONB_TYPE, docType, nullable, keepSource); - } + @Override + public void jsonWrite(JsonWriter writer, List value) throws IOException { + EJson.write(value, writer); } /** - * Base class for List handling. + * List mapped to VARCHAR with element conversion - used as the {@code @DbArray} fallback + * on platforms without native array support. */ - @SuppressWarnings("rawtypes") - private abstract static class Base extends ScalarTypeJsonCollection { - final boolean keepSource; - - private Base(int dbType, DocPropertyType docType, boolean nullable, boolean keepSource) { - super(List.class, dbType, docType, nullable); - this.keepSource = keepSource; - } - - @Override - public final boolean jsonMapper() { - return keepSource; - } - - @Override - public final List read(DataReader reader) throws SQLException { - String json = reader.getString(); - if (keepSource) { - reader.pushJson(json); - } - return readJsonConvert(json); - } + static final class VarcharWithConverter extends ScalarTypeJsonList { - List readJsonConvert(String json) { - try { - // parse JSON into modifyAware list - return EJson.parseList(json, true); - } catch (IOException e) { - throw new TextException("Failed to parse JSON [{}] as List", json, e); - } - } + private final ArrayElementConverter converter; - @Override - public final void bind(DataBinder binder, List value) throws SQLException { - String rawJson = keepSource ? binder.popJson() : null; - if (rawJson == null && value != null) { - rawJson = formatValue(value); - } - if (value == null) { - bindNull(binder); - } else { - bindRawJson(binder, rawJson); - } + VarcharWithConverter(DocPropertyType docType, boolean nullable, boolean keepSource, ArrayElementConverter converter) { + super(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource); + this.converter = converter; } @Override - protected void bindNull(DataBinder binder) throws SQLException { - if (nullable) { - binder.setNull(Types.VARCHAR); - } else { - binder.setString("[]"); - } - } - - protected void bindRawJson(DataBinder binder, String rawJson) throws SQLException { - binder.setString(rawJson); + List readJson(String rawJson) { + return convert(rawJson); } @Override - public final String formatValue(List value) { - try { - return EJson.write(value); - } catch (IOException e) { - throw new PersistenceException("Failed to format List into JSON content", e); - } + public List parse(String value) { + return convert(value); } - @Override - public List parse(String value) { + @SuppressWarnings("unchecked") + private List convert(String json) { try { - return EJson.parseList(value, false); + List rawList = EJson.parseList(json, false); + if (rawList == null) { + return null; + } + final List result = new ArrayList<>(rawList.size()); + for (Object o : rawList) { + result.add(converter.fromSerialized(o)); + } + return new ModifyAwareList(result); } catch (IOException e) { - throw new TextException("Failed to parse JSON [{}] as List", value, e); + throw new TextException("Failed to parse JSON [{}] as List", json, e); } } - - @Override - public final List jsonRead(JsonReader parser) throws IOException { - return EJson.parseList(parser, parser.currentToken()); - } - - @Override - public final void jsonWrite(JsonWriter writer, List value) throws IOException { - EJson.write(value, writer); - } - } - - /** - * Postgres extension to base List handling. - */ - private static class PgBase extends ScalarTypeJsonList.Base { - - final String pgType; - - PgBase(int jdbcType, String pgType, DocPropertyType docType, boolean nullable, boolean keepSource) { - super(jdbcType, docType, nullable, keepSource); - this.pgType = pgType; - } - - @Override - protected final void bindRawJson(DataBinder binder, String rawJson) throws SQLException { - binder.setObject(PostgresHelper.asObject(pgType, rawJson)); - } - - @Override - protected final void bindNull(DataBinder binder) throws SQLException { - binder.setObject(PostgresHelper.asObject(pgType, nullable ? null : "[]")); - } } - } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java index 5bc0a4befd..8864a43c9d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java @@ -3,202 +3,57 @@ import io.avaje.json.JsonReader; import io.avaje.json.JsonWriter; import io.ebean.config.dbplatform.DbPlatformType; -import io.ebean.core.type.DataBinder; -import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; -import io.ebean.core.type.ScalarTypeBase; +import io.ebean.core.type.PostgresHelper; import io.ebean.text.TextException; import io.ebean.text.json.EJson; -import io.ebean.util.IOUtils; -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.sql.SQLException; +import java.io.IOException; import java.sql.Types; import java.util.Map; /** - * Type which maps Map to various DB types (Clob, Varchar, Blob) in JSON format. + * Type which maps {@code Map} to JSON stored in VARCHAR, CLOB, BLOB, + * or Postgres JSON / JSONB. */ @SuppressWarnings("rawtypes") -abstract class ScalarTypeJsonMap extends ScalarTypeBase { +final class ScalarTypeJsonMap extends ScalarTypeJsonValue { /** - * Return the ScalarType for the requested dbType and postgres. + * Return the ScalarType for the requested dbType and platform. */ static ScalarTypeJsonMap typeFor(boolean postgres, int dbType, boolean keepSource) { switch (dbType) { case Types.VARCHAR: - return new ScalarTypeJsonMap.Varchar(keepSource); + return new ScalarTypeJsonMap(Types.VARCHAR, JsonStorage.VARCHAR, keepSource); case Types.BLOB: - return new ScalarTypeJsonMap.Blob(keepSource); + return new ScalarTypeJsonMap(Types.BLOB, JsonStorage.BLOB, keepSource); case Types.CLOB: - return new ScalarTypeJsonMap.Clob(keepSource); + return new ScalarTypeJsonMap(Types.CLOB, JsonStorage.CLOB, keepSource); case DbPlatformType.JSONB: - return postgres ? new ScalarTypeJsonMapPostgres.JSONB(keepSource) : new ScalarTypeJsonMap.Clob(keepSource); + return postgres + ? new ScalarTypeJsonMap(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), keepSource) + : new ScalarTypeJsonMap(Types.CLOB, JsonStorage.CLOB, keepSource); case DbPlatformType.JSON: - return postgres ? new ScalarTypeJsonMapPostgres.JSON(keepSource) : new ScalarTypeJsonMap.Clob(keepSource); + return postgres + ? new ScalarTypeJsonMap(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), keepSource) + : new ScalarTypeJsonMap(Types.CLOB, JsonStorage.CLOB, keepSource); default: throw new IllegalStateException("Unknown dbType " + dbType); } } - private static final class Clob extends ScalarTypeJsonMap { - Clob(boolean keepSource) { - super(Types.CLOB, keepSource); - } - - @Override - protected String readJson(DataReader reader) throws SQLException { - return reader.getStringFromStream(); - } - } - - private static final class Varchar extends ScalarTypeJsonMap { - Varchar(boolean keepSource) { - super(Types.VARCHAR, keepSource); - } - } - - private static final class Blob extends ScalarTypeJsonMap { - Blob(boolean keepSource) { - super(Types.BLOB, keepSource); - } - - @Override - public Map read(DataReader reader) throws SQLException { - InputStream is = reader.getBinaryStream(); - if (is == null) { - if (keepSource) { - reader.pushJson(null); - } - return null; - } - try { - if (keepSource) { - StringWriter jsonBuffer = new StringWriter(); - try (Reader streamReader = IOUtils.newReader(is)) { - transferTo(streamReader, jsonBuffer); - } - String rawJson = jsonBuffer.toString(); - reader.pushJson(rawJson); - return parse(rawJson); - } else { - try (Reader streamReader = IOUtils.newReader(is)) { - return parse(streamReader); - } - } - } catch (IOException e) { - throw new SQLException("Error reading Blob stream from DB", e); - } - } - - private static void transferTo(Reader reader, Writer out) throws IOException { - char[] buffer = new char[2048]; - int nRead; - while ((nRead = reader.read(buffer, 0, 2048)) >= 0) { - out.write(buffer, 0, nRead); - } - } - - @Override - protected void bindNull(DataBinder binder) throws SQLException { - binder.setNull(Types.BLOB); - } - - @Override - protected void bindJson(DataBinder binder, String rawJson) throws SQLException { - binder.setBytes(rawJson.getBytes(StandardCharsets.UTF_8)); - } - - } - - final boolean keepSource; - - ScalarTypeJsonMap(int jdbcType, boolean keepSource) { - super(Map.class, false, jdbcType); - this.keepSource = keepSource; - } - - /** - * Map is a mutable type. Use the isDirty() method to check for dirty state. - */ - @Override - public final boolean mutable() { - return true; + private ScalarTypeJsonMap(int jdbcType, JsonStorage storage, boolean keepSource) { + super(Map.class, jdbcType, storage, keepSource, true, null, DocPropertyType.OBJECT); } - /** - * Return true if the value should be considered dirty (and included in an update). - */ @Override - public boolean isDirty(Object value) { - return TypeJsonManager.checkIsDirty(value); - } - - @Override - public final boolean jsonMapper() { - return keepSource; - } - - @Override - public Map read(DataReader reader) throws SQLException { - String rawJson = readJson(reader); - if (keepSource) { - reader.pushJson(rawJson); - } - if (rawJson == null) { - return null; - } + Map readJson(String rawJson) { return parse(rawJson); } - protected String readJson(DataReader reader) throws SQLException { - return reader.getString(); - } - @Override - public final void bind(DataBinder binder, Map value) throws SQLException { - String rawJson = keepSource ? binder.popJson() : null; - if (rawJson == null && value != null) { - rawJson = formatValue(value); - } - if (value == null) { - bindNull(binder); - } else { - bindJson(binder, rawJson); - } - } - - protected void bindNull(DataBinder binder) throws SQLException { - binder.setNull(Types.VARCHAR); - } - - protected void bindJson(DataBinder binder, String rawJson) throws SQLException { - binder.setString(rawJson); - } - - @Override - public final Object toJdbcType(Object value) { - return value; - } - - @Override - public final Map toBeanType(Object value) { - return (Map) value; - } - - @Override - public final String formatValue(Map v) { - try { - return EJson.write(v); - } catch (IOException e) { - throw new TextException(e); - } - } - - @Override - public final Map parse(String value) { + public Map parse(String value) { try { // return a modify aware map return EJson.parseObject(value, true); @@ -207,46 +62,22 @@ public final Map parse(String value) { } } - public final Map parse(Reader reader) { + @Override + public String formatValue(Map value) { try { - // return a modify aware map - return EJson.parseObject(reader, true); + return EJson.write(value); } catch (IOException e) { throw new TextException(e); } } @Override - public final Map readData(DataInput dataInput) throws IOException { - if (!dataInput.readBoolean()) { - return null; - } else { - return parse(dataInput.readUTF()); - } - } - - @Override - public final void writeData(DataOutput dataOutput, Map map) throws IOException { - if (map == null) { - dataOutput.writeBoolean(false); - } else { - ScalarHelp.writeUTF(dataOutput, format(map)); - } - } - - @Override - public final void jsonWrite(JsonWriter writer, Map value) throws IOException { - EJson.write(value, writer); - } - - @Override - public final Map jsonRead(JsonReader parser) throws IOException { + public Map jsonRead(JsonReader parser) throws IOException { return EJson.parseObject(parser, parser.currentToken()); } @Override - public final DocPropertyType docType() { - return DocPropertyType.OBJECT; + public void jsonWrite(JsonWriter writer, Map value) throws IOException { + EJson.write(value, writer); } - } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java deleted file mode 100644 index 612048447b..0000000000 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.ebeaninternal.server.type; - -import io.ebean.config.dbplatform.DbPlatformType; -import io.ebean.core.type.DataBinder; -import io.ebean.core.type.PostgresHelper; - -import java.sql.SQLException; - -/** - * Support for the Postgres DB types JSON and JSONB. - */ -abstract class ScalarTypeJsonMapPostgres extends ScalarTypeJsonMap { - - private final String postgresType; - - ScalarTypeJsonMapPostgres(int jdbcType, String postgresType, boolean keepSource) { - super(jdbcType, keepSource); - this.postgresType = postgresType; - } - - @Override - protected final void bindNull(DataBinder binder) throws SQLException { - binder.setObject(PostgresHelper.asObject(postgresType, null)); - } - - @Override - protected final void bindJson(DataBinder binder, String rawJson) throws SQLException { - binder.setObject(PostgresHelper.asObject(postgresType, rawJson)); - } - - /** - * ScalarType mapping java Map type to Postgres JSON database type. - */ - static final class JSON extends ScalarTypeJsonMapPostgres { - - JSON(boolean keepSource) { - super(DbPlatformType.JSON, PostgresHelper.JSON_TYPE, keepSource); - } - } - - /** - * ScalarType mapping java Map type to Postgres JSONB database type. - */ - static final class JSONB extends ScalarTypeJsonMapPostgres { - - JSONB(boolean keepSource) { - super(DbPlatformType.JSONB, PostgresHelper.JSONB_TYPE, keepSource); - } - } -} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java index d094001f07..eb54f990f8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java @@ -3,218 +3,122 @@ import io.avaje.json.JsonReader; import io.avaje.json.JsonWriter; import io.ebean.config.dbplatform.DbPlatformType; -import io.ebean.core.type.*; +import io.ebean.core.type.DocPropertyType; +import io.ebean.core.type.PostgresHelper; +import io.ebean.core.type.ScalarType; import io.ebean.text.TextException; import io.ebean.text.json.EJson; import io.ebeaninternal.json.ModifyAwareSet; import jakarta.persistence.PersistenceException; import java.io.IOException; -import java.sql.SQLException; import java.sql.Types; import java.util.LinkedHashSet; -import java.util.List; import java.util.Set; /** - * Types for mapping List in JSON format to DB types VARCHAR, JSON and JSONB. + * Type which maps a Set in JSON format to VARCHAR or Postgres JSON / JSONB. */ -final class ScalarTypeJsonSet { +@SuppressWarnings("rawtypes") +class ScalarTypeJsonSet extends ScalarTypeJsonCollectionValue { /** - * Return the appropriate ScalarType for the requested dbType and Postgres. + * Return the appropriate ScalarType for the requested dbType and platform. */ - static ScalarType typeFor(boolean postgres, int dbType, DocPropertyType docPropertyType, boolean nullable, boolean keepSource) { + static ScalarType typeFor(boolean postgres, int dbType, DocPropertyType docType, boolean nullable, boolean keepSource) { if (postgres) { switch (dbType) { case DbPlatformType.JSONB: - return new ScalarTypeJsonSet.JsonB(docPropertyType, nullable, keepSource); + return new ScalarTypeJsonSet(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), docType, nullable, keepSource); case DbPlatformType.JSON: - return new ScalarTypeJsonSet.Json(docPropertyType, nullable, keepSource); + return new ScalarTypeJsonSet(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), docType, nullable, keepSource); } } - return new ScalarTypeJsonSet.Varchar(docPropertyType, nullable, keepSource); + return new ScalarTypeJsonSet(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource); } - @SuppressWarnings("rawtypes") - static final class VarcharWithConverter extends ScalarTypeJsonSet.Base { - private final ArrayElementConverter converter; + ScalarTypeJsonSet(int jdbcType, JsonStorage storage, DocPropertyType docType, boolean nullable, boolean keepSource) { + super(Set.class, jdbcType, storage, keepSource, nullable, docType); + } - VarcharWithConverter(DocPropertyType docType, boolean nullable, boolean keepSource, ArrayElementConverter converter) { - super(Types.VARCHAR, docType, nullable, keepSource); - this.converter = converter; + @Override + Set readJson(String rawJson) { + try { + // parse JSON into a modifyAware set + return EJson.parseSet(rawJson, true); + } catch (IOException e) { + throw new TextException("Failed to parse JSON [{}] as Set", rawJson, e); } + } - @Override - Set readJsonConvert(String json) { - try { - return convertElements(EJson.parseSet(json, false)); - } catch (IOException e) { - throw new TextException("Failed to parse JSON [{}] as List", json, e); - } - } - - @SuppressWarnings("unchecked") - private Set convertElements(Set rawSet) { - if (rawSet == null) { - return null; - } - final Set result = new LinkedHashSet(rawSet.size()); - for (Object o : rawSet) { - result.add(converter.fromSerialized(o)); - } - return new ModifyAwareSet(result); + @Override + @SuppressWarnings("unchecked") + public Set parse(String value) { + try { + return new LinkedHashSet(EJson.parseList(value)); + } catch (IOException e) { + throw new PersistenceException("Failed to parse JSON content as Set: " + value, e); } + } - @Override - public Set parse(String value) { - try { - return convertElements(EJson.parseSet(value, false)); - } catch (IOException e) { - throw new PersistenceException("Failed to parse JSON content as Set: " + value, e); - } + @Override + public String formatValue(Set value) { + try { + return EJson.write(value); + } catch (IOException e) { + throw new PersistenceException("Failed to format Set into JSON content", e); } } - /** - * List mapped to DB VARCHAR. - */ - static final class Varchar extends ScalarTypeJsonSet.Base { - public Varchar(DocPropertyType docPropertyType, boolean nullable, boolean keepSource) { - super(Types.VARCHAR, docPropertyType, nullable, keepSource); - } + + @Override + @SuppressWarnings("unchecked") + public Set jsonRead(JsonReader parser) throws IOException { + return new LinkedHashSet(EJson.parseList(parser, parser.currentToken())); } - /** - * List mapped to Postgres JSON. - */ - private static final class Json extends ScalarTypeJsonSet.PgBase { - private Json(DocPropertyType docPropertyType, boolean nullable, boolean keepSource) { - super(DbPlatformType.JSON, PostgresHelper.JSON_TYPE, docPropertyType, nullable, keepSource); - } + @Override + public void jsonWrite(JsonWriter writer, Set value) throws IOException { + EJson.write(value, writer); } /** - * List mapped to Postgres JSONB. + * Set mapped to VARCHAR with element conversion - used as the {@code @DbArray} fallback + * on platforms without native array support. */ - private static final class JsonB extends ScalarTypeJsonSet.PgBase { - private JsonB(DocPropertyType docPropertyType, boolean nullable, boolean keepSource) { - super(DbPlatformType.JSONB, PostgresHelper.JSONB_TYPE, docPropertyType, nullable, keepSource); - } - } - - @SuppressWarnings("rawtypes") - private abstract static class Base extends ScalarTypeJsonCollection { + static final class VarcharWithConverter extends ScalarTypeJsonSet { - final boolean keepSource; + private final ArrayElementConverter converter; - private Base(int dbType, DocPropertyType docPropertyType, boolean nullable, boolean keepSource) { - super(Set.class, dbType, docPropertyType, nullable); - this.keepSource = keepSource; + VarcharWithConverter(DocPropertyType docType, boolean nullable, boolean keepSource, ArrayElementConverter converter) { + super(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource); + this.converter = converter; } @Override - public final boolean jsonMapper() { - return keepSource; + Set readJson(String rawJson) { + return convert(rawJson); } @Override - public final Set read(DataReader reader) throws SQLException { - String json = reader.getString(); - if (keepSource) { - reader.pushJson(json); - } - return readJsonConvert(json); + public Set parse(String value) { + return convert(value); } - Set readJsonConvert(String json) { + @SuppressWarnings("unchecked") + private Set convert(String json) { try { - return EJson.parseSet(json, true); + Set rawSet = EJson.parseSet(json, false); + if (rawSet == null) { + return null; + } + final Set result = new LinkedHashSet(rawSet.size()); + for (Object o : rawSet) { + result.add(converter.fromSerialized(o)); + } + return new ModifyAwareSet(result); } catch (IOException e) { throw new TextException("Failed to parse JSON [{}] as Set", json, e); } } - - @Override - public final void bind(DataBinder binder, Set value) throws SQLException { - String rawJson = keepSource ? binder.popJson() : null; - if (rawJson == null && value != null) { - rawJson = formatValue(value); - } - if (value == null) { - bindNull(binder); - } else { - bindRawJson(binder, rawJson); - } - } - - @Override - protected void bindNull(DataBinder binder) throws SQLException { - if (nullable) { - binder.setNull(Types.VARCHAR); - } else { - binder.setString("[]"); - } - } - - protected void bindRawJson(DataBinder binder, String rawJson) throws SQLException { - binder.setString(rawJson); - } - - @Override - public final String formatValue(Set value) { - try { - return EJson.write(value); - } catch (IOException e) { - throw new PersistenceException("Failed to format List into JSON content", e); - } - } - - @Override - public Set parse(String value) { - try { - return convertList(EJson.parseList(value)); - } catch (IOException e) { - throw new PersistenceException("Failed to parse JSON content as Set: " + value, e); - } - } - - @Override - public final Set jsonRead(JsonReader parser) throws IOException { - return convertList(EJson.parseList(parser, parser.currentToken())); - } - - @Override - public final void jsonWrite(JsonWriter writer, Set value) throws IOException { - EJson.write(value, writer); - } - - @SuppressWarnings("unchecked") - private Set convertList(List list) { - return new LinkedHashSet(list); - } - } - - /** - * Postgres extension to base List handling. - */ - private static class PgBase extends ScalarTypeJsonSet.Base { - - final String pgType; - - PgBase(int jdbcType, String pgType, DocPropertyType docPropertyType, boolean nullable, boolean keepSource) { - super(jdbcType, docPropertyType, nullable, keepSource); - this.pgType = pgType; - } - - @Override - protected final void bindRawJson(DataBinder binder, String rawJson) throws SQLException { - binder.setObject(PostgresHelper.asObject(pgType, rawJson)); - } - - @Override - protected final void bindNull(DataBinder binder) throws SQLException { - binder.setObject(PostgresHelper.asObject(pgType, nullable ? null : "[]")); - } } - } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonValue.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonValue.java new file mode 100644 index 0000000000..81159c8c65 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonValue.java @@ -0,0 +1,128 @@ +package io.ebeaninternal.server.type; + +import io.ebean.core.type.DataBinder; +import io.ebean.core.type.DataReader; +import io.ebean.core.type.DocPropertyType; +import io.ebean.core.type.ScalarTypeBase; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.sql.SQLException; + +/** + * Base for the built-in JSON value types (Map, List, Set). + *

+ * Holds the common read / bind / L2 cache plumbing and delegates: + *

    + *
  • the storage concern (VARCHAR / CLOB / BLOB / Postgres) to a {@link JsonStorage}
  • + *
  • the value marshalling concern to the subclass (which uses the avaje JsonMapper + * backed {@code EJson} facade)
  • + *
+ * This removes the previous explosion of per-storage and per-platform subclasses. + */ +abstract class ScalarTypeJsonValue extends ScalarTypeBase { + + protected final JsonStorage storage; + protected final boolean keepSource; + private final boolean nullable; + private final String emptyJson; + private final DocPropertyType docType; + + /** + * @param emptyJson JSON bound when the value is null and the property is not nullable + * (e.g. {@code "[]"} for collections), or null to always bind SQL null. + */ + ScalarTypeJsonValue(Class type, int jdbcType, JsonStorage storage, boolean keepSource, + boolean nullable, String emptyJson, DocPropertyType docType) { + super(type, false, jdbcType); + this.storage = storage; + this.keepSource = keepSource; + this.nullable = nullable; + this.emptyJson = emptyJson; + this.docType = docType; + } + + /** + * Marshal the raw JSON read from the DB into the bean value (the load path - typically + * returns a modify-aware collection). + */ + abstract T readJson(String rawJson); + + @Override + public final boolean mutable() { + return true; + } + + @Override + public final boolean isDirty(Object value) { + return TypeJsonManager.checkIsDirty(value); + } + + @Override + public final boolean jsonMapper() { + return keepSource; + } + + @Override + public final T read(DataReader reader) throws SQLException { + String rawJson = storage.read(reader); + if (keepSource) { + reader.pushJson(rawJson); + } + if (rawJson == null) { + return null; + } + return readJson(rawJson); + } + + @Override + public final void bind(DataBinder binder, T value) throws SQLException { + String rawJson = keepSource ? binder.popJson() : null; + if (rawJson == null && value != null) { + rawJson = formatValue(value); + } + if (value == null) { + if (nullable || emptyJson == null) { + storage.bindNull(binder); + } else { + storage.bind(binder, emptyJson); + } + } else { + storage.bind(binder, rawJson); + } + } + + @Override + public final Object toJdbcType(Object value) { + return value; + } + + @Override + @SuppressWarnings("unchecked") + public final T toBeanType(Object value) { + return (T) value; + } + + @Override + public final DocPropertyType docType() { + return docType; + } + + @Override + public final T readData(DataInput dataInput) throws IOException { + if (!dataInput.readBoolean()) { + return null; + } + return parse(dataInput.readUTF()); + } + + @Override + public final void writeData(DataOutput dataOutput, T value) throws IOException { + if (value == null) { + dataOutput.writeBoolean(false); + } else { + ScalarHelp.writeUTF(dataOutput, formatValue(value)); + } + } +} From 10bf5dbbbda60602f93f20d5529ef50c663cd474 Mon Sep 17 00:00:00 2001 From: "robin.bygrave" Date: Fri, 19 Jun 2026 20:03:27 +1200 Subject: [PATCH 4/7] Support Map in @DbJson(B) fields Addresses #3735. Enum-keyed JSON maps previously failed (enum key cast to String). Add ScalarTypeJsonMapEnum which converts enum keys via their ScalarType (honouring @DbEnumValue) and reuses ScalarTypeJsonMap for all storage/platform handling, so it works for VARCHAR/CLOB/BLOB and Postgres JSON/JSONB with no per-storage variants. - JsonStorage now reports jdbcType(), enabling a shared storageFor(...) reused by the plain and enum-key Map types - DefaultTypeManager routes Map to the new type and calls setAccessible on the @DbEnumValue method (supports nested/non-public enums) - add TypeReflectHelper.getMapKeyTypeRaw and TestEnumKeyMap --- .../server/type/DefaultTypeManager.java | 12 ++ .../server/type/JsonStorage.java | 28 ++++ .../server/type/ScalarTypeJsonMap.java | 28 ++-- .../server/type/ScalarTypeJsonMapEnum.java | 90 +++++++++++++ .../server/type/TypeReflectHelper.java | 8 ++ .../java/org/tests/json/TestEnumKeyMap.java | 126 ++++++++++++++++++ 6 files changed, 280 insertions(+), 12 deletions(-) create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMapEnum.java create mode 100644 ebean-test/src/test/java/org/tests/json/TestEnumKeyMap.java diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java index 4e46e0b64b..5c50838cf7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java @@ -320,6 +320,10 @@ public ScalarType dbJsonType(DeployBeanProperty prop, int dbType, int dbLengt return ScalarTypeJsonSet.typeFor(postgres, dbType, docType(genericType), prop.isNullable(), keepSource(prop)); } if (type.equals(Map.class) && isMapValueTypeObject(genericType)) { + Type keyType = TypeReflectHelper.getMapKeyTypeRaw(genericType); + if (isEnumType(keyType)) { + return enumJsonMapType(postgres, dbType, keyType, keepSource(prop)); + } return ScalarTypeJsonMap.typeFor(postgres, dbType, keepSource(prop)); } if (objectMapperPresent && prop.getMutationDetection() == MutationDetection.DEFAULT) { @@ -338,6 +342,13 @@ private boolean keepSource(DeployBeanProperty prop) { return prop.getMutationDetection() == MutationDetection.SOURCE; } + @SuppressWarnings("unchecked") + private ScalarType enumJsonMapType(boolean postgres, int dbType, Type keyType, boolean keepSource) { + Class> enumClass = asEnumClass(keyType); + ScalarType> enumScalarType = (ScalarType>) enumType(enumClass, null); + return ScalarTypeJsonMapEnum.typeFor(postgres, dbType, enumScalarType, keepSource); + } + private DocPropertyType docPropertyType(DeployBeanProperty prop, Class type) { return type.equals(List.class) || type.equals(Set.class) ? docType(prop.getGenericType()) : DocPropertyType.OBJECT; } @@ -560,6 +571,7 @@ private ScalarTypeEnum enumTypePerExtensions(Class> enumTyp */ private ScalarTypeEnum enumTypeDbValue(Class> enumType, Method method, boolean integerType, int length, boolean withConstraint) { Map nameValueMap = new LinkedHashMap<>(); + method.setAccessible(true); for (Enum enumConstant : enumType.getEnumConstants()) { try { Object value = method.invoke(enumConstant); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/JsonStorage.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/JsonStorage.java index c397c619e3..dd41dd2655 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/JsonStorage.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/JsonStorage.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.type; +import io.ebean.config.dbplatform.DbPlatformType; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.PostgresHelper; @@ -38,6 +39,11 @@ static JsonStorage postgres(String pgType) { */ String read(DataReader reader) throws SQLException; + /** + * The JDBC type reported by a ScalarType using this storage. + */ + int jdbcType(); + /** * Bind the given non-null raw JSON content. */ @@ -54,6 +60,11 @@ public String read(DataReader reader) throws SQLException { return reader.getString(); } + @Override + public int jdbcType() { + return Types.VARCHAR; + } + @Override public void bind(DataBinder binder, String rawJson) throws SQLException { binder.setString(rawJson); @@ -71,6 +82,11 @@ public String read(DataReader reader) throws SQLException { return reader.getStringFromStream(); } + @Override + public int jdbcType() { + return Types.CLOB; + } + @Override public void bind(DataBinder binder, String rawJson) throws SQLException { binder.setString(rawJson); @@ -102,6 +118,11 @@ public String read(DataReader reader) throws SQLException { } } + @Override + public int jdbcType() { + return Types.BLOB; + } + @Override public void bind(DataBinder binder, String rawJson) throws SQLException { binder.setBytes(rawJson.getBytes(StandardCharsets.UTF_8)); @@ -115,9 +136,11 @@ public void bindNull(DataBinder binder) throws SQLException { final class Postgres implements JsonStorage { private final String pgType; + private final int jdbcType; Postgres(String pgType) { this.pgType = pgType; + this.jdbcType = PostgresHelper.JSONB_TYPE.equals(pgType) ? DbPlatformType.JSONB : DbPlatformType.JSON; } @Override @@ -125,6 +148,11 @@ public String read(DataReader reader) throws SQLException { return reader.getString(); } + @Override + public int jdbcType() { + return jdbcType; + } + @Override public void bind(DataBinder binder, String rawJson) throws SQLException { binder.setObject(PostgresHelper.asObject(pgType, rawJson)); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java index 8864a43c9d..52374a6d97 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java @@ -17,34 +17,38 @@ * or Postgres JSON / JSONB. */ @SuppressWarnings("rawtypes") -final class ScalarTypeJsonMap extends ScalarTypeJsonValue { +class ScalarTypeJsonMap extends ScalarTypeJsonValue { /** * Return the ScalarType for the requested dbType and platform. */ static ScalarTypeJsonMap typeFor(boolean postgres, int dbType, boolean keepSource) { + return new ScalarTypeJsonMap(storageFor(postgres, dbType), keepSource); + } + + /** + * Select the storage strategy for the given dbType and platform. Shared with the + * enum-key Map variant. + */ + static JsonStorage storageFor(boolean postgres, int dbType) { switch (dbType) { case Types.VARCHAR: - return new ScalarTypeJsonMap(Types.VARCHAR, JsonStorage.VARCHAR, keepSource); + return JsonStorage.VARCHAR; case Types.BLOB: - return new ScalarTypeJsonMap(Types.BLOB, JsonStorage.BLOB, keepSource); + return JsonStorage.BLOB; case Types.CLOB: - return new ScalarTypeJsonMap(Types.CLOB, JsonStorage.CLOB, keepSource); + return JsonStorage.CLOB; case DbPlatformType.JSONB: - return postgres - ? new ScalarTypeJsonMap(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), keepSource) - : new ScalarTypeJsonMap(Types.CLOB, JsonStorage.CLOB, keepSource); + return postgres ? JsonStorage.postgres(PostgresHelper.JSONB_TYPE) : JsonStorage.CLOB; case DbPlatformType.JSON: - return postgres - ? new ScalarTypeJsonMap(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), keepSource) - : new ScalarTypeJsonMap(Types.CLOB, JsonStorage.CLOB, keepSource); + return postgres ? JsonStorage.postgres(PostgresHelper.JSON_TYPE) : JsonStorage.CLOB; default: throw new IllegalStateException("Unknown dbType " + dbType); } } - private ScalarTypeJsonMap(int jdbcType, JsonStorage storage, boolean keepSource) { - super(Map.class, jdbcType, storage, keepSource, true, null, DocPropertyType.OBJECT); + ScalarTypeJsonMap(JsonStorage storage, boolean keepSource) { + super(Map.class, storage.jdbcType(), storage, keepSource, true, null, DocPropertyType.OBJECT); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMapEnum.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMapEnum.java new file mode 100644 index 0000000000..3775467d65 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMapEnum.java @@ -0,0 +1,90 @@ +package io.ebeaninternal.server.type; + +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; +import io.ebean.core.type.ScalarType; +import io.ebean.text.TextException; +import io.ebean.text.json.EJson; +import io.ebeaninternal.json.ModifyAwareMap; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Type which maps {@code Map} to JSON. + *

+ * The enum keys are (de)serialized via their {@link ScalarType} (honouring custom + * {@code @DbEnumValue} mappings), then delegated to {@link ScalarTypeJsonMap} for all + * storage, platform and value handling. + */ +@SuppressWarnings("rawtypes") +final class ScalarTypeJsonMapEnum> extends ScalarTypeJsonMap { + + private final ScalarType enumType; + + static ScalarType typeFor(boolean postgres, int dbType, ScalarType> enumType, boolean keepSource) { + return new ScalarTypeJsonMapEnum<>(storageFor(postgres, dbType), enumType, keepSource); + } + + @SuppressWarnings("unchecked") + private ScalarTypeJsonMapEnum(JsonStorage storage, ScalarType enumType, boolean keepSource) { + super(storage, keepSource); + this.enumType = (ScalarType) enumType; + } + + @Override + Map readJson(String rawJson) { + return parse(rawJson); + } + + @Override + public Map parse(String value) { + try { + return toEnumKeys(EJson.parseObject(value, true)); + } catch (IOException e) { + throw new TextException("Failed to parse JSON [{}] as Map with enum keys", value, e); + } + } + + @Override + public String formatValue(Map value) { + try { + return EJson.write(toStringKeys(value)); + } catch (IOException e) { + throw new TextException(e); + } + } + + @Override + public Map jsonRead(JsonReader parser) throws IOException { + return toEnumKeys(EJson.parseObject(parser, parser.currentToken())); + } + + @Override + public void jsonWrite(JsonWriter writer, Map value) throws IOException { + EJson.write(toStringKeys(value), writer); + } + + @SuppressWarnings("unchecked") + private Map toStringKeys(Map value) { + Map stringKeyMap = new LinkedHashMap<>(); + for (Object o : value.entrySet()) { + Map.Entry e = (Map.Entry) o; + stringKeyMap.put(enumType.formatValue((T) e.getKey()), e.getValue()); + } + return stringKeyMap; + } + + @SuppressWarnings("unchecked") + private Map toEnumKeys(Map stringKeyMap) { + if (stringKeyMap == null) { + return null; + } + Map enumKeyMap = new LinkedHashMap(); + for (Map.Entry e : stringKeyMap.entrySet()) { + enumKeyMap.put(enumType.parse(e.getKey()), e.getValue()); + } + return stringKeyMap instanceof ModifyAwareMap ? new ModifyAwareMap(enumKeyMap) : enumKeyMap; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeReflectHelper.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeReflectHelper.java index 761ea90fdd..2793fec035 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeReflectHelper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeReflectHelper.java @@ -37,6 +37,14 @@ public static Class getMapKeyType(Type genericType) { return getClass(getValueType(genericType)); } + /** + * Return the raw type of the map key (first type argument). + */ + public static Type getMapKeyTypeRaw(Type genericType) { + Type[] typeArgs = ((ParameterizedType) genericType).getActualTypeArguments(); + return typeArgs[0]; + } + /** * Return the value type of a collection type (list, set, map values). */ diff --git a/ebean-test/src/test/java/org/tests/json/TestEnumKeyMap.java b/ebean-test/src/test/java/org/tests/json/TestEnumKeyMap.java new file mode 100644 index 0000000000..19d60d22f0 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/json/TestEnumKeyMap.java @@ -0,0 +1,126 @@ +package org.tests.json; + +import io.ebean.DB; +import io.ebean.annotation.DbEnumValue; +import io.ebean.annotation.DbJson; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Test for Map<Enum, Object> support in @DbJson fields. + */ +class TestEnumKeyMap extends BaseTestCase { + + enum Status { + ACTIVE, + INACTIVE, + PENDING; + + @DbEnumValue + public String getValue() { + return name().substring(0, 1); // A, I, P + } + } + + enum Priority { + LOW, + MEDIUM, + HIGH + } + + @Entity + @Table(name = "enum_map_test") + public static class EnumMapBean { + @Id + Long id; + + @DbJson + Map statusData; + + @DbJson + Map priorityLabels; + + public EnumMapBean(Long id) { + this.id = id; + } + + public EnumMapBean() { + } + } + + @Test + void testEnumKeyMapWithCustomEnumValue() { + EnumMapBean bean = new EnumMapBean(1L); + + Map statusData = new LinkedHashMap<>(); + statusData.put(Status.ACTIVE, "Running smoothly"); + statusData.put(Status.PENDING, 42L); + statusData.put(Status.INACTIVE, Map.of("reason", "maintenance")); + + bean.statusData = statusData; + + DB.save(bean); + + // Read it back + EnumMapBean found = DB.find(EnumMapBean.class, 1L); + assertThat(found).isNotNull(); + assertThat(found.statusData).hasSize(3); + assertThat(found.statusData.get(Status.ACTIVE)).isEqualTo("Running smoothly"); + assertThat(found.statusData.get(Status.PENDING)).isEqualTo(42L); + assertThat(found.statusData.get(Status.INACTIVE)).isInstanceOf(Map.class); + + // Update + found.statusData.put(Status.ACTIVE, "Updated status"); + found.statusData.remove(Status.INACTIVE); + DB.save(found); + + EnumMapBean updated = DB.find(EnumMapBean.class, 1L); + assertNotNull(updated); + assertThat(updated.statusData).hasSize(2); + assertThat(updated.statusData.get(Status.ACTIVE)).isEqualTo("Updated status"); + assertThat(updated.statusData).doesNotContainKey(Status.INACTIVE); + } + + @Test + void testEnumKeyMapWithStandardEnum() { + EnumMapBean bean = new EnumMapBean(2L); + + Map priorityLabels = new LinkedHashMap<>(); + priorityLabels.put(Priority.LOW, "Not urgent"); + priorityLabels.put(Priority.MEDIUM, "Standard processing"); + priorityLabels.put(Priority.HIGH, "Urgent - immediate action required"); + + bean.priorityLabels = priorityLabels; + + DB.save(bean); + + EnumMapBean found = DB.find(EnumMapBean.class, 2L); + assertThat(found).isNotNull(); + assertThat(found.priorityLabels).hasSize(3); + assertThat(found.priorityLabels.get(Priority.LOW)).isEqualTo("Not urgent"); + assertThat(found.priorityLabels.get(Priority.HIGH)).isEqualTo("Urgent - immediate action required"); + } + + @Test + void testNullAndEmptyMaps() { + EnumMapBean bean = new EnumMapBean(3L); + bean.statusData = null; + bean.priorityLabels = new LinkedHashMap<>(); + + DB.save(bean); + + EnumMapBean found = DB.find(EnumMapBean.class, 3L); + assertThat(found).isNotNull(); + assertThat(found.statusData).isNull(); + assertThat(found.priorityLabels).isEmpty(); + } +} From 230d7a36a75fa2282d92ecb3c5f66e607b52d79b Mon Sep 17 00:00:00 2001 From: "robin.bygrave" Date: Fri, 19 Jun 2026 21:47:20 +1200 Subject: [PATCH 5/7] Simplify @DbJson Map dispatch and support Map Replace isMapValueTypeObject + isMapStringString with isBuiltinJsonMap: the built-in JSON map handles a String or enum key with a String, Object or wildcard value. This also routes Map to the built-in type and fixes non-String/enum keys with Object value (e.g. Map) being mis-routed to the built-in type (ClassCastException) instead of the object mapper. --- .../server/type/DefaultTypeManager.java | 21 ++++-- .../tests/json/TestDbJsonMapStringValue.java | 65 +++++++++++++++++++ .../java/org/tests/json/TestEnumKeyMap.java | 26 ++++++++ 3 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 ebean-test/src/test/java/org/tests/json/TestDbJsonMapStringValue.java diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java index 5c50838cf7..948fc32596 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java @@ -319,7 +319,7 @@ public ScalarType dbJsonType(DeployBeanProperty prop, int dbType, int dbLengt if (type.equals(Set.class) && isValueTypeSimple(genericType)) { return ScalarTypeJsonSet.typeFor(postgres, dbType, docType(genericType), prop.isNullable(), keepSource(prop)); } - if (type.equals(Map.class) && isMapValueTypeObject(genericType)) { + if (type.equals(Map.class) && isBuiltinJsonMap(genericType)) { Type keyType = TypeReflectHelper.getMapKeyTypeRaw(genericType); if (isEnumType(keyType)) { return enumJsonMapType(postgres, dbType, keyType, keepSource(prop)); @@ -376,11 +376,24 @@ private Type valueType(Type collectionType) { } /** - * Return true if value parameter type of the map is Object. + * Return true if the Map is handled by the built-in JSON support: a String or enum key + * with a String, Object or wildcard value. Such values round-trip through EJson without + * type coercion. Other maps (typed values, or non-String/enum keys) use the object mapper. */ - private boolean isMapValueTypeObject(Type genericType) { + private boolean isBuiltinJsonMap(Type genericType) { + if (!(genericType instanceof ParameterizedType)) { + return false; + } Type[] typeArgs = ((ParameterizedType) genericType).getActualTypeArguments(); - return Object.class.equals(typeArgs[1]) || "?".equals(typeArgs[1].toString()); + return isJsonMapKeyType(typeArgs[0]) && isJsonMapValueType(typeArgs[1]); + } + + private boolean isJsonMapKeyType(Type keyType) { + return String.class.equals(keyType) || isEnumType(keyType); + } + + private boolean isJsonMapValueType(Type valueType) { + return Object.class.equals(valueType) || String.class.equals(valueType) || "?".equals(valueType.toString()); } private ScalarType createJsonObjectMapperType(DeployBeanProperty prop, int dbType, DocPropertyType docType) { diff --git a/ebean-test/src/test/java/org/tests/json/TestDbJsonMapStringValue.java b/ebean-test/src/test/java/org/tests/json/TestDbJsonMapStringValue.java new file mode 100644 index 0000000000..926c93079d --- /dev/null +++ b/ebean-test/src/test/java/org/tests/json/TestDbJsonMapStringValue.java @@ -0,0 +1,65 @@ +package org.tests.json; + +import io.ebean.DB; +import io.ebean.annotation.DbJsonB; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Regression: @DbJsonB Map<String,String> (typed value map, handled via the Jackson + * ObjectMapper path) alongside Map<String,Object> (handled by the built-in type). + */ +class TestDbJsonMapStringValue extends BaseTestCase { + + @Entity + @Table(name = "json_map_string_value") + public static class MapStringBean { + @Id + Long id; + + @DbJsonB + Map stringMap; + + @DbJsonB + Map objectMap; + + public MapStringBean(Long id) { + this.id = id; + } + + public MapStringBean() { + } + } + + @Test + void mapStringString_roundTrip() { + MapStringBean bean = new MapStringBean(1L); + Map m = new LinkedHashMap<>(); + m.put("a", "alpha"); + m.put("b", "beta"); + bean.stringMap = m; + + Map om = new LinkedHashMap<>(); + om.put("x", "ex"); + om.put("n", 42L); + bean.objectMap = om; + + DB.save(bean); + + MapStringBean found = DB.find(MapStringBean.class, 1L); + assertThat(found.stringMap).containsEntry("a", "alpha").containsEntry("b", "beta"); + assertThat(found.objectMap).containsEntry("x", "ex").containsEntry("n", 42L); + + found.stringMap.put("c", "gamma"); + DB.save(found); + assertThat(DB.find(MapStringBean.class, 1L).stringMap).hasSize(3).containsEntry("c", "gamma"); + } +} diff --git a/ebean-test/src/test/java/org/tests/json/TestEnumKeyMap.java b/ebean-test/src/test/java/org/tests/json/TestEnumKeyMap.java index 19d60d22f0..c8e08944a4 100644 --- a/ebean-test/src/test/java/org/tests/json/TestEnumKeyMap.java +++ b/ebean-test/src/test/java/org/tests/json/TestEnumKeyMap.java @@ -46,6 +46,9 @@ public static class EnumMapBean { @DbJson Map statusData; + @DbJson + Map statusLabels; + @DbJson Map priorityLabels; @@ -110,6 +113,29 @@ void testEnumKeyMapWithStandardEnum() { assertThat(found.priorityLabels.get(Priority.HIGH)).isEqualTo("Urgent - immediate action required"); } + @Test + void testEnumKeyMapWithStringValue() { + EnumMapBean bean = new EnumMapBean(4L); + + Map statusLabels = new LinkedHashMap<>(); + statusLabels.put(Status.ACTIVE, "on"); + statusLabels.put(Status.INACTIVE, "off"); + bean.statusLabels = statusLabels; + + DB.save(bean); + + EnumMapBean found = DB.find(EnumMapBean.class, 4L); + assertThat(found.statusLabels).hasSize(2); + assertThat(found.statusLabels.get(Status.ACTIVE)).isEqualTo("on"); + assertThat(found.statusLabels.get(Status.INACTIVE)).isEqualTo("off"); + + found.statusLabels.put(Status.PENDING, "wait"); + DB.save(found); + assertThat(DB.find(EnumMapBean.class, 4L).statusLabels) + .hasSize(3) + .containsEntry(Status.PENDING, "wait"); + } + @Test void testNullAndEmptyMaps() { EnumMapBean bean = new EnumMapBean(3L); From e9b00cbbd1e756a254caf2cedcfbb903f3f58fff Mon Sep 17 00:00:00 2001 From: "robin.bygrave" Date: Fri, 19 Jun 2026 22:10:54 +1200 Subject: [PATCH 6/7] Add documentation for the DbJson mapping support --- docs/LIBRARY.md | 1 + docs/guides/README.md | 1 + docs/guides/dbjson-mapping-support.md | 116 ++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 docs/guides/dbjson-mapping-support.md diff --git a/docs/LIBRARY.md b/docs/LIBRARY.md index 9e8b86fb5f..fbe4796ba9 100644 --- a/docs/LIBRARY.md +++ b/docs/LIBRARY.md @@ -184,6 +184,7 @@ database.save(customer); | Add PostgreSQL test container support | [add-ebean-postgres-test-container.md](guides/add-ebean-postgres-test-container.md) | | Generate DB migrations | [add-ebean-db-migration-generation.md](guides/add-ebean-db-migration-generation.md) | | Migrate JSON APIs from Jackson core to avaje-json-core | [migrating-json-jackson-core-to-avaje-json-core.md](guides/migrating-json-jackson-core-to-avaje-json-core.md) | +| Know which `@DbJson` types need Jackson vs built-in | [dbjson-mapping-support.md](guides/dbjson-mapping-support.md) | | Model entity beans correctly | [entity-bean-creation.md](guides/entity-bean-creation.md) | | Use Lombok safely with entities | [lombok-with-ebean-entity-beans.md](guides/lombok-with-ebean-entity-beans.md) | | Write type-safe query bean queries | [writing-ebean-query-beans.md](guides/writing-ebean-query-beans.md) | diff --git a/docs/guides/README.md b/docs/guides/README.md index a9432d6c50..bc7c20003d 100644 --- a/docs/guides/README.md +++ b/docs/guides/README.md @@ -39,6 +39,7 @@ existing Maven project. Complete the steps in order. |-------|-------------| | [Entity Bean Creation](entity-bean-creation.md) | How to generate clean, idiomatic Ebean entity beans for AI agents; patterns and anti-patterns; field visibility and accessor guidance; minimal boilerplate | | [Lombok with Ebean entity beans](lombok-with-ebean-entity-beans.md) | Which Lombok annotations to use and avoid on entity beans; why `@Data` is incompatible with Ebean; how to use `@Getter` + `@Setter` + `@Accessors(chain = true)` | +| [`@DbJson` mapping support (built-in vs Jackson)](dbjson-mapping-support.md) | Which `@DbJson` / `@DbJsonB` property types are handled by the built-in avaje-json-core support versus which require `ebean-jackson-mapper` (Jackson `ObjectMapper`); supported `String`/`List`/`Set`/`Map` matrix; enum-key and `@DbArray` notes | ## Querying diff --git a/docs/guides/dbjson-mapping-support.md b/docs/guides/dbjson-mapping-support.md new file mode 100644 index 0000000000..d1e0c2b379 --- /dev/null +++ b/docs/guides/dbjson-mapping-support.md @@ -0,0 +1,116 @@ +# Guide: `@DbJson` / `@DbJsonB` mapping support — built-in vs Jackson ObjectMapper + +## Purpose + +Ebean can map `@DbJson` and `@DbJsonB` properties in two ways: + +- **Built-in** JSON support, backed by **avaje-json-core** — no extra dependency. +- **Jackson `ObjectMapper`**, provided by the **`ebean-jackson-mapper`** module — used + for everything the built-in support does not handle. + +This guide lists exactly which property types are handled built-in and which require +`ebean-jackson-mapper`. + +> If a property type is **not** handled built-in and `ebean-jackson-mapper` is not on the +> classpath, Ebean fails fast at startup: +> +> ```text +> Unsupported @DbJson mapping - Missing dependency ebean-jackson-mapper? +> Jackson ObjectMapper not present for +> ``` + +--- + +## Quick reference + +| Property type | Built-in (avaje-json-core) | Needs `ebean-jackson-mapper` | +|---|:---:|:---:| +| `String` | ✅ | | +| `List`, `List` | ✅ | | +| `Set`, `Set` | ✅ | | +| `Map`, `Map` | ✅ | | +| `Map` | ✅ | | +| `Map`, `Map` | ✅ | | +| `List`/`Set` of any other element type (`Integer`, `Double`, `UUID`, `LocalDate`, an enum, a POJO, …) | | ✅ | +| `Map` with a typed value other than `String`/`Object` (`Map`, `Map`, …) | | ✅ | +| `Map` with a key other than `String` or an enum (`Map`, `Map`) | | ✅ | +| POJOs, records, or any other type | | ✅ | + +--- + +## Built-in support (no Jackson required) + +The built-in path materialises JSON into the *natural* JSON value types +(`String`, `Long`, `BigDecimal`, `Boolean`, `Map`, `List`). It is therefore type-safe only +for the following declared property types: + +- **`String`** — stored as raw JSON text. +- **`List`** and **`List`**. +- **`Set`** and **`Set`**. +- **`Map`** where: + - the key `K` is `String` or an **enum**, and + - the value `V` is `Object`, `String`, or a wildcard `?`. + + So `Map`, `Map`, `Map` and `Map` + are all built-in. + +These mappings work across all supported storage types — `VARCHAR`, `CLOB`, `BLOB`, and +Postgres `json` / `jsonb` — without `ebean-jackson-mapper`. + +--- + +## Everything else → Jackson `ObjectMapper` + +Any other `@DbJson` / `@DbJsonB` property routes to the Jackson `ObjectMapper` path, which +requires `ebean-jackson-mapper`: + +- **Typed collections** — `List`/`Set` whose element type is not `String` or `Long` + (for example `List`, `List`, `List`, `List`, `List`). +- **Typed-value maps** — a `Map` value type other than `String`/`Object` + (for example `Map`, `Map`, `Map`). +- **Non-`String`/non-enum map keys** — for example `Map`, `Map`. +- **POJOs, records, and any other custom type.** + +> **Jackson marker annotation override:** if the property type carries a Jackson annotation +> (anything meta-annotated with `com.fasterxml.jackson.annotation.JacksonAnnotation`), Ebean +> uses the `ObjectMapper` path even when the type would otherwise be handled built-in. + +--- + +## Adding `ebean-jackson-mapper` + +```xml + + io.ebean + ebean-jackson-mapper + ${ebean.version} + +``` + +A Jackson `ObjectMapper` must be available (via `jackson-databind`). Ebean detects it and +registers the mapper-based JSON support automatically. + +--- + +## Notes + +- **Enum map keys** are serialised using the enum `name()` (for example `ACTIVE`), not any + `@DbEnumValue` mapping. Round-trips are correct; the DB value mapping is not applied to + JSON keys. +- **`@DbArray` alternative:** for typed *scalar* collections (`List`/`Set` of `Integer`, + `Long`, `UUID`, `Double`, an enum, …) consider `@DbArray`, which maps to a native DB array + (with a JSON fallback on platforms without array support) and supports more element types + than built-in `@DbJson` collections. +- The reason typed value/element collections need a real mapper is that the built-in path + only produces natural JSON types — for example a JSON number always parses to `Long`, so a + declared `List` or `Map` could not be populated safely without a + type-aware mapper. + +--- + +## Choosing + +- Prefer the **built-in** mappings for the common cases (`String`, string/long lists and sets, + object/string maps) to avoid pulling in Jackson. +- Add **`ebean-jackson-mapper`** when you need rich POJO JSON columns or typed collections / + typed-value maps. From 56d33b1f1be5c4b5e465137443cf2f68d76529ac Mon Sep 17 00:00:00 2001 From: "robin.bygrave" Date: Mon, 22 Jun 2026 10:06:34 +1200 Subject: [PATCH 7/7] Update documentation for the DbJson mapping support --- docs/guides/dbjson-mapping-support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/dbjson-mapping-support.md b/docs/guides/dbjson-mapping-support.md index d1e0c2b379..5be5f1420a 100644 --- a/docs/guides/dbjson-mapping-support.md +++ b/docs/guides/dbjson-mapping-support.md @@ -71,7 +71,7 @@ requires `ebean-jackson-mapper`: - **Non-`String`/non-enum map keys** — for example `Map`, `Map`. - **POJOs, records, and any other custom type.** -> **Jackson marker annotation override:** if the property type carries a Jackson annotation +> **Jackson marker annotation override:** if the **field or getter** carries a Jackson annotation > (anything meta-annotated with `com.fasterxml.jackson.annotation.JacksonAnnotation`), Ebean > uses the `ObjectMapper` path even when the type would otherwise be handled built-in.