diff --git a/docs/guides/dbjson-mapping-support.md b/docs/guides/dbjson-mapping-support.md index 5be5f1420a..e21b365bb8 100644 --- a/docs/guides/dbjson-mapping-support.md +++ b/docs/guides/dbjson-mapping-support.md @@ -1,40 +1,39 @@ -# Guide: `@DbJson` / `@DbJsonB` mapping support — built-in vs Jackson ObjectMapper +# Guide: `@DbJson` / `@DbJsonB` mapping support ## Purpose -Ebean can map `@DbJson` and `@DbJsonB` properties in two ways: +Ebean can map `@DbJson` and `@DbJsonB` properties in three 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. +- **Jackson `ObjectMapper`**, provided by **`ebean-jackson-mapper`**. +- **Avaje Jsonb**, provided by **`ebean-avajejsonb-mapper`** and its generated adapters. -This guide lists exactly which property types are handled built-in and which require -`ebean-jackson-mapper`. +The built-in mapper handles the common natural JSON types. Add one mapper module for +typed collections, POJOs, records, and other custom types. -> If a property type is **not** handled built-in and `ebean-jackson-mapper` is not on the -> classpath, Ebean fails fast at startup: +> If a property type is **not** handled built-in and no mapper module is on the classpath, +> Ebean fails fast at startup: > > ```text -> Unsupported @DbJson mapping - Missing dependency ebean-jackson-mapper? -> Jackson ObjectMapper not present for +> Unsupported @DbJson mapping - missing JSON mapper dependency for > ``` --- ## Quick reference -| Property type | Built-in (avaje-json-core) | Needs `ebean-jackson-mapper` | -|---|:---:|:---:| +| Property type | Built-in (avaje-json-core) | Mapper module | +|---|:---:|---| | `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 | | ✅ | +| `List`/`Set` of any other element type (`Integer`, `Double`, `UUID`, `LocalDate`, an enum, a POJO, …) | | Jackson or Avaje Jsonb | +| `Map` with a typed value other than `String`/`Object` (`Map`, `Map`, …) | | Jackson or Avaje Jsonb | +| `Map` with a key other than `String` or an enum (`Map`, `Map`) | | Jackson or Avaje Jsonb | +| POJOs, records, or any other type | | Jackson or Avaje Jsonb | --- @@ -59,10 +58,10 @@ Postgres `json` / `jsonb` — without `ebean-jackson-mapper`. --- -## Everything else → Jackson `ObjectMapper` +## Jackson `ObjectMapper` -Any other `@DbJson` / `@DbJsonB` property routes to the Jackson `ObjectMapper` path, which -requires `ebean-jackson-mapper`: +`ebean-jackson-mapper` uses a Jackson `ObjectMapper` for the property types not handled +built-in: - **Typed collections** — `List`/`Set` whose element type is not `String` or `Long` (for example `List`, `List`, `List`, `List`, `List`). @@ -77,7 +76,7 @@ requires `ebean-jackson-mapper`: --- -## Adding `ebean-jackson-mapper` +### Adding `ebean-jackson-mapper` ```xml @@ -92,6 +91,79 @@ registers the mapper-based JSON support automatically. --- +## Avaje Jsonb + +`ebean-avajejsonb-mapper` uses Avaje Jsonb adapters. Annotate each JSON payload type with +`@Json`, or use `@Json.Import`, and configure `avaje-jsonb-generator` as an annotation +processor. + +```xml + + io.ebean + ebean-avajejsonb-mapper + ${ebean.version} + +``` + +```xml + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.avaje + avaje-jsonb-generator + ${avaje-jsonb.version} + + + + +``` + +For example: + +```java +import io.avaje.jsonb.Json; + +@Json +public class Address { + public String line1; + public String city; +} +``` + +Avaje Jsonb preserves a property's declared generic type, so `List
` and other +parameterised JSON values use the generated `Address` adapter. + +### Avaje JsonNode + +`@DbJson` and `@DbJsonB` properties declared as `io.avaje.json.node.JsonNode` are supported +when the application includes `avaje-json-node`. Its Jsonb component supplies the JSON tree +adapters; no application-generated adapter is needed for the node hierarchy. + +```xml + + io.avaje + avaje-json-node + ${avaje-jsonb.version} + +``` + +## Mapper module selection + +Use exactly one mapper module in an application: `ebean-jackson-mapper` or +`ebean-avajejsonb-mapper`. Ebean selects a single `ScalarJsonMapper` service provider, so +having both modules on the runtime classpath is not supported. + +The `ebean` composite dependency includes `ebean-jackson-mapper`. Applications using Avaje +Jsonb should depend on the individual Ebean modules they need instead of that composite. + +To migrate from Jackson to Avaje Jsonb, remove `ebean-jackson-mapper`, add +`ebean-avajejsonb-mapper`, and generate adapters for each JSON payload type. + +--- + ## Notes - **Enum map keys** are serialised using the enum `name()` (for example `ACTIVE`), not any @@ -102,15 +174,13 @@ registers the mapper-based JSON support automatically. (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. + only produces natural JSON types — for example a JSON number always parses to `Long`. --- ## 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. + object/string maps) to avoid an additional mapper module. +- Add **`ebean-jackson-mapper`** or **`ebean-avajejsonb-mapper`** when you need rich POJO JSON + columns or typed collections / typed-value maps. diff --git a/ebean-avajejsonb-mapper/pom.xml b/ebean-avajejsonb-mapper/pom.xml new file mode 100644 index 0000000000..8730c3f484 --- /dev/null +++ b/ebean-avajejsonb-mapper/pom.xml @@ -0,0 +1,113 @@ + + + + ebean-parent + io.ebean + 18.3.0 + + 4.0.0 + + ebean-avajejsonb-mapper + ebean-avajejsonb-mapper + + + + io.ebean + ebean-core-type + 18.3.0 + provided + + + + io.avaje + avaje-json-core + ${avaje-json-core.version} + provided + + + + io.avaje + avaje-jsonb + ${avaje-jsonb.version} + + + + io.avaje + avaje-json-node + ${avaje-jsonb.version} + test + + + + io.ebean + ebean-core + 18.3.0 + test + + + + io.ebean + ebean-platform-h2 + 18.3.0 + test + + + + io.ebean + ebean-datasource + ${ebean-datasource.version} + test + + + + com.h2database + h2 + ${h2database.version} + test + + + + io.ebean + ebean-ddl-generator + 18.3.0 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.avaje + avaje-jsonb-generator + ${avaje-jsonb.version} + + + + + + + io.ebean + ebean-maven-plugin + ${ebean-maven-plugin.version} + + + test + process-test-classes + + org/example/avajejsonb/** + debug=0 + + + testEnhance + + + + + + + diff --git a/ebean-avajejsonb-mapper/src/main/java/io/ebean/avajejsonb/mapper/ScalarJsonAvajeJsonbMapper.java b/ebean-avajejsonb-mapper/src/main/java/io/ebean/avajejsonb/mapper/ScalarJsonAvajeJsonbMapper.java new file mode 100644 index 0000000000..5d1f0cac3c --- /dev/null +++ b/ebean-avajejsonb-mapper/src/main/java/io/ebean/avajejsonb/mapper/ScalarJsonAvajeJsonbMapper.java @@ -0,0 +1,223 @@ +package io.ebean.avajejsonb.mapper; + +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; +import io.avaje.jsonb.JsonType; +import io.avaje.jsonb.Jsonb; +import io.ebean.annotation.MutationDetection; +import io.ebean.core.type.DataBinder; +import io.ebean.core.type.DataReader; +import io.ebean.core.type.DocPropertyType; +import io.ebean.core.type.JsonTrim; +import io.ebean.core.type.PostgresHelper; +import io.ebean.core.type.ScalarJsonManager; +import io.ebean.core.type.ScalarJsonMapper; +import io.ebean.core.type.ScalarJsonRequest; +import io.ebean.core.type.ScalarType; +import io.ebean.core.type.ScalarTypeBase; +import io.ebean.text.TextException; +import jakarta.persistence.PersistenceException; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.sql.SQLException; +import java.sql.Types; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Supports {@code @DbJson} properties using Avaje Jsonb. + */ +public final class ScalarJsonAvajeJsonbMapper implements ScalarJsonMapper { + + private final ConcurrentHashMap> jsonTypes = new ConcurrentHashMap<>(); + + @Override + public Class markerAnnotation() { + return null; + } + + @Override + public ScalarType createType(ScalarJsonRequest request) { + Type genericType = genericType(request); + JsonType jsonType = jsonTypes.computeIfAbsent(genericType, type -> jsonb(request.manager()).type(type)); + if (request.mode() == MutationDetection.NONE) { + return new NoMutationDetection(request.manager(), jsonType, request.dbType(), request.docType()); + } + return new GenericObject(request.manager(), jsonType, request.dbType(), request.docType()); + } + + private Jsonb jsonb(ScalarJsonManager manager) { + Object mapper = manager.mapper(); + return mapper instanceof Jsonb ? (Jsonb) mapper : Jsonb.instance(); + } + + private Type genericType(ScalarJsonRequest request) { + Class type = request.beanType(); + while (type != null) { + try { + Field field = type.getDeclaredField(request.name()); + return field.getGenericType(); + } catch (NoSuchFieldException e) { + type = type.getSuperclass(); + } + } + throw new IllegalStateException("Field not found to match " + request.name()); + } + + private static final class NoMutationDetection extends Base { + + NoMutationDetection(ScalarJsonManager jsonManager, JsonType jsonType, int dbType, DocPropertyType docType) { + super(Object.class, jsonManager, jsonType, dbType, docType); + } + } + + private static final class GenericObject extends Base { + + private final boolean jsonb; + + GenericObject(ScalarJsonManager jsonManager, JsonType jsonType, int dbType, DocPropertyType docType) { + super(Object.class, jsonManager, jsonType, dbType, docType); + this.jsonb = "jsonb".equals(pgType); + } + + @Override + public boolean mutable() { + return true; + } + + @Override + public boolean jsonMapper() { + return true; + } + + @Override + public Object read(DataReader reader) throws SQLException { + String json = reader.getString(); + if (jsonb) { + json = JsonTrim.trim(json); + } + reader.pushJson(json); + return parseJson(json); + } + + @Override + public void bind(DataBinder binder, Object value) throws SQLException { + String rawJson = binder.popJson(); + if (rawJson == null && value != null) { + rawJson = formatValue(value); + } + bindJson(binder, value, rawJson); + } + } + + private static abstract class Base extends ScalarTypeBase { + + private final JsonType jsonType; + protected final String pgType; + private final DocPropertyType docType; + + Base(Class cls, ScalarJsonManager jsonManager, JsonType jsonType, int dbType, DocPropertyType docType) { + super(cls, false, dbType); + this.jsonType = jsonType; + this.pgType = jsonManager.postgresType(dbType); + this.docType = docType; + } + + @Override + public T read(DataReader reader) throws SQLException { + return parseJson(reader.getString()); + } + + @Override + public void bind(DataBinder binder, T value) throws SQLException { + bindJson(binder, value, value == null ? null : formatValue(value)); + } + + final T parseJson(String json) { + if (json == null || json.isEmpty()) { + return null; + } + try { + return jsonType.fromJson(json); + } catch (RuntimeException e) { + throw new TextException("Failed to parse JSON [{}] as " + jsonType, json, e); + } + } + + final void bindJson(DataBinder binder, Object value, String rawJson) throws SQLException { + if (pgType != null) { + binder.setObject(PostgresHelper.asObject(pgType, rawJson)); + } else if (value == null) { + binder.setNull(Types.VARCHAR); + } else { + binder.setString(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 String formatValue(T value) { + try { + return jsonType.toJson(value); + } catch (RuntimeException e) { + throw new PersistenceException("Unable to create JSON", e); + } + } + + @Override + public final T parse(String value) { + return parseJson(value); + } + + @Override + public final DocPropertyType docType() { + return docType; + } + + @Override + public final T jsonRead(JsonReader parser) { + if (parser.isNullValue()) { + return null; + } + return parseJson(parser.readRaw()); + } + + @Override + public final void jsonWrite(JsonWriter writer, T value) throws IOException { + if (value == null) { + writer.nullValue(); + } else { + writer.rawValue(formatValue(value)); + } + } + + @Override + public final T readData(DataInput dataInput) throws IOException { + return dataInput.readBoolean() ? parse(dataInput.readUTF()) : null; + } + + @Override + public final void writeData(DataOutput dataOutput, T value) throws IOException { + if (value == null) { + dataOutput.writeBoolean(false); + } else { + dataOutput.writeBoolean(true); + dataOutput.writeUTF(format(value)); + } + } + } +} diff --git a/ebean-avajejsonb-mapper/src/main/java/module-info.java b/ebean-avajejsonb-mapper/src/main/java/module-info.java new file mode 100644 index 0000000000..62ff8693d8 --- /dev/null +++ b/ebean-avajejsonb-mapper/src/main/java/module-info.java @@ -0,0 +1,9 @@ +import io.ebean.avajejsonb.mapper.ScalarJsonAvajeJsonbMapper; + +module io.ebean.avajejsonb.mapper { + + requires io.avaje.jsonb; + requires io.ebean.core.type; + + provides io.ebean.core.type.ScalarJsonMapper with ScalarJsonAvajeJsonbMapper; +} diff --git a/ebean-avajejsonb-mapper/src/main/resources/META-INF/services/io.ebean.core.type.ScalarJsonMapper b/ebean-avajejsonb-mapper/src/main/resources/META-INF/services/io.ebean.core.type.ScalarJsonMapper new file mode 100644 index 0000000000..c749942c49 --- /dev/null +++ b/ebean-avajejsonb-mapper/src/main/resources/META-INF/services/io.ebean.core.type.ScalarJsonMapper @@ -0,0 +1 @@ +io.ebean.avajejsonb.mapper.ScalarJsonAvajeJsonbMapper diff --git a/ebean-avajejsonb-mapper/src/test/java/io/ebean/avajejsonb/mapper/JsonbDatabaseTest.java b/ebean-avajejsonb-mapper/src/test/java/io/ebean/avajejsonb/mapper/JsonbDatabaseTest.java new file mode 100644 index 0000000000..e5e601b704 --- /dev/null +++ b/ebean-avajejsonb-mapper/src/test/java/io/ebean/avajejsonb/mapper/JsonbDatabaseTest.java @@ -0,0 +1,56 @@ +package io.ebean.avajejsonb.mapper; + +import io.avaje.json.node.JsonNode; +import io.avaje.json.node.JsonObject; +import io.ebean.Database; +import io.ebean.DatabaseBuilder; +import org.example.avajejsonb.JsonbEntity; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Properties; + +import static org.assertj.core.api.Assertions.assertThat; + +class JsonbDatabaseTest { + + @Test + void dbJson_roundTripsJsonbPayloadsAndJsonNode() { + Database database = buildDatabase(); + try { + JsonbEntity entity = new JsonbEntity(); + entity.setPayload(new JsonbPayload("main", 1)); + entity.setPayloads(List.of(new JsonbPayload("first", 2), new JsonbPayload("second", 3))); + entity.setNode(JsonObject.create().add("name", "node").add("count", 4)); + database.save(entity); + + JsonbEntity found = database.find(JsonbEntity.class, entity.getId()); + + assertThat(found.getPayload()).isEqualTo(new JsonbPayload("main", 1)); + assertThat(found.getPayloads()).containsExactly(new JsonbPayload("first", 2), new JsonbPayload("second", 3)); + JsonNode node = found.getNode(); + assertThat(node.extract("name")).isEqualTo("node"); + assertThat(node.extract("count", 0)).isEqualTo(4); + } finally { + database.shutdown(); + } + } + + private static Database buildDatabase() { + DatabaseBuilder config = Database.builder(); + config.setName("avajeJsonbMapper"); + config.setDefaultServer(false); + config.setDdlGenerate(true); + config.setDdlRun(true); + config.setDdlExtra(false); + + Properties properties = new Properties(); + properties.setProperty("datasource.avajeJsonbMapper.username", "sa"); + properties.setProperty("datasource.avajeJsonbMapper.password", ""); + properties.setProperty("datasource.avajeJsonbMapper.databaseUrl", "jdbc:h2:mem:avajeJsonbMapper"); + properties.setProperty("datasource.avajeJsonbMapper.databaseDriver", "org.h2.Driver"); + config.loadFromProperties(properties); + config.addClass(JsonbEntity.class); + return config.build(); + } +} diff --git a/ebean-avajejsonb-mapper/src/test/java/io/ebean/avajejsonb/mapper/JsonbPayload.java b/ebean-avajejsonb-mapper/src/test/java/io/ebean/avajejsonb/mapper/JsonbPayload.java new file mode 100644 index 0000000000..ddbecc8f04 --- /dev/null +++ b/ebean-avajejsonb-mapper/src/test/java/io/ebean/avajejsonb/mapper/JsonbPayload.java @@ -0,0 +1,37 @@ +package io.ebean.avajejsonb.mapper; + +import io.avaje.jsonb.Json; + +import java.util.Objects; + +@Json +public class JsonbPayload { + + public String name; + public int count; + + public JsonbPayload() { + } + + JsonbPayload(String name, int count) { + this.name = name; + this.count = count; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof JsonbPayload)) { + return false; + } + JsonbPayload other = (JsonbPayload) object; + return count == other.count && Objects.equals(name, other.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, count); + } +} diff --git a/ebean-avajejsonb-mapper/src/test/java/io/ebean/avajejsonb/mapper/ScalarJsonAvajeJsonbMapperTest.java b/ebean-avajejsonb-mapper/src/test/java/io/ebean/avajejsonb/mapper/ScalarJsonAvajeJsonbMapperTest.java new file mode 100644 index 0000000000..e2c619a55b --- /dev/null +++ b/ebean-avajejsonb-mapper/src/test/java/io/ebean/avajejsonb/mapper/ScalarJsonAvajeJsonbMapperTest.java @@ -0,0 +1,130 @@ +package io.ebean.avajejsonb.mapper; + +import io.avaje.json.node.JsonNode; +import io.avaje.json.node.JsonObject; +import io.avaje.jsonb.Json; +import io.ebean.annotation.MutationDetection; +import io.ebean.core.type.DocPropertyType; +import io.ebean.core.type.ScalarJsonManager; +import io.ebean.core.type.ScalarJsonRequest; +import io.ebean.core.type.ScalarType; +import org.junit.jupiter.api.Test; + +import java.sql.Types; +import java.util.List; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +class ScalarJsonAvajeJsonbMapperTest { + + private static final ScalarJsonManager JSON_MANAGER = new ScalarJsonManager() { + + @Override + public MutationDetection mutationDetection() { + return MutationDetection.HASH; + } + + @Override + public Object mapper() { + return null; + } + + @Override + public String postgresType(int dbType) { + return null; + } + }; + + private final ScalarJsonAvajeJsonbMapper mapper = new ScalarJsonAvajeJsonbMapper(); + + @Test + void pojo_roundTripsThroughGeneratedJsonbAdapter() { + ScalarType scalarType = scalarType("document", MutationDetection.HASH); + Payload payload = new Payload("hello", 42); + + String json = scalarType.formatValue(payload); + + assertThat(json).isEqualTo("{\"name\":\"hello\",\"count\":42}"); + assertThat(scalarType.parse(json)).isEqualTo(payload); + assertThat(scalarType.mutable()).isTrue(); + assertThat(scalarType.jsonMapper()).isTrue(); + } + + @Test + void genericList_roundTripsUsingPropertyGenericType() { + ScalarType scalarType = scalarType("payloads", MutationDetection.HASH); + List payloads = List.of(new Payload("one", 1), new Payload("two", 2)); + + String json = scalarType.formatValue(payloads); + + assertThat(json).isEqualTo("[{\"name\":\"one\",\"count\":1},{\"name\":\"two\",\"count\":2}]"); + assertThat(scalarType.parse(json)).isEqualTo(payloads); + } + + @Test + void jsonNode_roundTripsThroughAvajeJsonNodeComponent() { + ScalarType scalarType = scalarType("node", MutationDetection.HASH); + JsonNode node = JsonObject.create().add("name", "node").add("count", 3); + + String json = scalarType.formatValue(node); + + assertThat(json).isEqualTo("{\"name\":\"node\",\"count\":3}"); + JsonNode parsed = (JsonNode) scalarType.parse(json); + assertThat(parsed.extract("name")).isEqualTo("node"); + assertThat(parsed.extract("count", 0)).isEqualTo(3); + } + + @Test + void mutationDetectionNone_isNotMutable() { + ScalarType scalarType = scalarType("document", MutationDetection.NONE); + + assertThat(scalarType.mutable()).isFalse(); + assertThat(scalarType.jsonMapper()).isFalse(); + } + + @SuppressWarnings("unchecked") + private ScalarType scalarType(String property, MutationDetection mutationDetection) { + var request = new ScalarJsonRequest(JSON_MANAGER, Types.VARCHAR, DocPropertyType.OBJECT, Entity.class, mutationDetection, property); + return (ScalarType) mapper.createType(request); + } + + private static final class Entity { + + Payload document; + List payloads; + JsonNode node; + } + + @Json + static class Payload { + + public String name; + public int count; + + Payload() { + } + + Payload(String name, int count) { + this.name = name; + this.count = count; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof Payload)) { + return false; + } + Payload other = (Payload) object; + return count == other.count && Objects.equals(name, other.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, count); + } + } +} diff --git a/ebean-avajejsonb-mapper/src/test/java/org/example/avajejsonb/JsonbEntity.java b/ebean-avajejsonb-mapper/src/test/java/org/example/avajejsonb/JsonbEntity.java new file mode 100644 index 0000000000..44077e28e4 --- /dev/null +++ b/ebean-avajejsonb-mapper/src/test/java/org/example/avajejsonb/JsonbEntity.java @@ -0,0 +1,54 @@ +package org.example.avajejsonb; + +import io.avaje.json.node.JsonNode; +import io.ebean.avajejsonb.mapper.JsonbPayload; +import io.ebean.annotation.DbJson; +import io.ebean.annotation.DbJsonB; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import java.util.List; + +@Entity +public class JsonbEntity { + + @Id + long id; + + @DbJson + JsonbPayload payload; + + @DbJson + List payloads; + + @DbJsonB + JsonNode node; + + public long getId() { + return id; + } + + public JsonbPayload getPayload() { + return payload; + } + + public void setPayload(JsonbPayload payload) { + this.payload = payload; + } + + public List getPayloads() { + return payloads; + } + + public void setPayloads(List payloads) { + this.payloads = payloads; + } + + public JsonNode getNode() { + return node; + } + + public void setNode(JsonNode node) { + this.node = node; + } +} diff --git a/ebean-avajejsonb-mapper/src/test/resources/ebean.mf b/ebean-avajejsonb-mapper/src/test/resources/ebean.mf new file mode 100644 index 0000000000..3efad671f1 --- /dev/null +++ b/ebean-avajejsonb-mapper/src/test/resources/ebean.mf @@ -0,0 +1 @@ +entity-packages: org.example.avajejsonb diff --git a/ebean-bom/pom.xml b/ebean-bom/pom.xml index baec21e396..ce94b9e51f 100644 --- a/ebean-bom/pom.xml +++ b/ebean-bom/pom.xml @@ -128,6 +128,12 @@ 18.3.0 + + io.ebean + ebean-avajejsonb-mapper + 18.3.0 + + io.ebean ebean-ddl-generator diff --git a/ebean-jackson-mapper/src/main/java/io/ebean/jackson/mapper/JsonTrim.java b/ebean-core-type/src/main/java/io/ebean/core/type/JsonTrim.java similarity index 69% rename from ebean-jackson-mapper/src/main/java/io/ebean/jackson/mapper/JsonTrim.java rename to ebean-core-type/src/main/java/io/ebean/core/type/JsonTrim.java index 8559786814..7916fb9357 100644 --- a/ebean-jackson-mapper/src/main/java/io/ebean/jackson/mapper/JsonTrim.java +++ b/ebean-core-type/src/main/java/io/ebean/core/type/JsonTrim.java @@ -1,14 +1,19 @@ -package io.ebean.jackson.mapper; +package io.ebean.core.type; /** - * Helper that removes whitespace from JSON. Used to normalise Postgres JSONB content. + * Helper that removes whitespace from JSON content. + *

+ * Used to normalise PostgreSQL JSONB content before it is retained for mutation detection. */ -final class JsonTrim { +public final class JsonTrim { + + private JsonTrim() { + } /** * Return JSON with whitespace trimmed. */ - static String trim(String json) { + public static String trim(String json) { if (json == null) { return null; } @@ -18,7 +23,7 @@ static String trim(String json) { boolean quoted = false; for (int i = 0; i < len; i++) { char c = json.charAt(i); - if (c == '\"') { + if (c == '"') { if (!escaped) { quoted = !quoted; } else { 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 d497656020..e80823db9c 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 @@ -88,8 +88,8 @@ public DefaultTypeManager(DatabaseBuilder.Settings config, BootupClasses bootupC this.databasePlatform = config.getDatabasePlatform(); this.postgres = isPostgresCompatible(config.getDatabasePlatform()); this.objectMapperPresent = config.getClassLoadConfig().isJacksonObjectMapperPresent(); - this.objectMapper = (objectMapperPresent) ? initObjectMapper(config) : null; - this.jsonManager = (objectMapperPresent) ? new TypeJsonManager(postgres, objectMapper, config.getJsonMutationDetection()) : null; + this.objectMapper = (objectMapperPresent) ? initObjectMapper(config) : config.getObjectMapper(); + this.jsonManager = new TypeJsonManager(postgres, objectMapper, config.getJsonMutationDetection()); this.extraTypeFactory = new DefaultTypeFactory(config); this.arrayTypeListFactory = arrayTypeListFactory(config.getDatabasePlatform()); this.arrayTypeSetFactory = arrayTypeSetFactory(config.getDatabasePlatform()); @@ -413,7 +413,7 @@ private boolean isJsonMapValueType(Type valueType) { private ScalarType createJsonObjectMapperType(DeployProperty prop, int dbType, DocPropertyType docType) { if (jsonMapper == null) { - throw new IllegalArgumentException("Unsupported @DbJson mapping - Missing dependency ebean-jackson-mapper? Jackson ObjectMapper not present for " + prop); + throw new IllegalArgumentException("Unsupported @DbJson mapping - missing JSON mapper dependency for " + prop); } if (MutationDetection.DEFAULT == prop.mutationDetection()) { prop.setMutationDetection(jsonManager.mutationDetection()); diff --git a/ebean-jackson-mapper/src/test/java/io/ebean/jackson/mapper/JsonTrimTest.java b/ebean-jackson-mapper/src/test/java/io/ebean/jackson/mapper/JsonTrimTest.java index f5b78b1473..d1f0d5ed52 100644 --- a/ebean-jackson-mapper/src/test/java/io/ebean/jackson/mapper/JsonTrimTest.java +++ b/ebean-jackson-mapper/src/test/java/io/ebean/jackson/mapper/JsonTrimTest.java @@ -1,6 +1,6 @@ package io.ebean.jackson.mapper; - +import io.ebean.core.type.JsonTrim; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; diff --git a/ebean-test/src/test/java/org/tests/family/TestInheritance.java b/ebean-test/src/test/java/org/tests/family/TestInheritance.java index 937cd195f1..7243b5b448 100644 --- a/ebean-test/src/test/java/org/tests/family/TestInheritance.java +++ b/ebean-test/src/test/java/org/tests/family/TestInheritance.java @@ -103,6 +103,7 @@ public void testInheritance() { server().save(grandparent1); + var parent1Id = parent1.getIdentifier(); testMyDynamicFormulaWithIncludes(); testMyDynamicFormulaWithIncludesAndFetchJoin(); @@ -111,7 +112,7 @@ public void testInheritance() { //grandparent1 = server().find(GrandParentPerson.class).setId(grandparent1.getIdentifier()).where() // .in("effectiveBean.id",2,null) // geht nicht! // .findOne(); - parent1 = server().find(ParentPerson.class).setId(1).fetch("someBean").findOne(); + parent1 = server().find(ParentPerson.class).setId(parent1Id).fetch("someBean").findOne(); assertEquals("A Bean", parent1.getSomeBean().getName()); grandparent1 = server().find(GrandParentPerson.class) .fetch("children", "*") // FIXME If I do not fetch childrenBean, I will get an exception diff --git a/pom.xml b/pom.xml index e89c5433a0..375a12f8c7 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,7 @@ 2.22.0 3.14 + 3.14 2.2.220 3.2 3.0 @@ -98,6 +99,7 @@ platforms composites ebean-jackson-mapper + ebean-avajejsonb-mapper ebean-spring-txn ebean-opentelemetry ebean-core-json