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 1400b2180..6e1e1f0d7 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 @@ -289,7 +289,7 @@ public final class DefaultTypeManager implements TypeManager { @Override public ScalarType getDbMapScalarType() { - return (postgres) ? hstoreType : ScalarTypeJsonMap.typeFor(false, Types.VARCHAR); + return (postgres) ? hstoreType : ScalarTypeJsonMap.typeFor(false, Types.VARCHAR, false); } @Override @@ -312,7 +312,7 @@ public final class DefaultTypeManager implements TypeManager { return arrayTypeSetFactory.typeFor(valueType, nullable); } // fallback to JSON storage in VARCHAR column - return new ScalarTypeJsonSet.Varchar(getDocType(valueType), nullable); + return new ScalarTypeJsonSet.Varchar(getDocType(valueType), nullable, false); // TODO: keepSource for @DbArray? } private ScalarType getArrayScalarTypeList(Type valueType, boolean nullable) { @@ -323,7 +323,7 @@ public final class DefaultTypeManager implements TypeManager { return arrayTypeListFactory.typeFor(valueType, nullable); } // fallback to JSON storage in VARCHAR column - return new ScalarTypeJsonList.Varchar(getDocType(valueType), nullable); + return new ScalarTypeJsonList.Varchar(getDocType(valueType), nullable, false); // TODO: keepSource for @DbArray? } private Class> asEnumClass(Type valueType) { @@ -340,10 +340,11 @@ public final class DefaultTypeManager implements TypeManager { Type genericType = prop.getGenericType(); boolean hasJacksonAnnotations = objectMapperPresent && checkJacksonAnnotations(prop); + boolean keepSource = prop.getMutationDetection() == MutationDetection.SOURCE; if (type.equals(List.class)) { DocPropertyType docType = getDocType(genericType); if (!hasJacksonAnnotations && isValueTypeSimple(genericType)) { - return ScalarTypeJsonList.typeFor(postgres, dbType, docType, prop.isNullable()); + return ScalarTypeJsonList.typeFor(postgres, dbType, docType, prop.isNullable(), keepSource); } else { return createJsonObjectMapperType(prop, dbType, docType); } @@ -351,19 +352,19 @@ public final class DefaultTypeManager implements TypeManager { if (type.equals(Set.class)) { DocPropertyType docType = getDocType(genericType); if (!hasJacksonAnnotations && isValueTypeSimple(genericType)) { - return ScalarTypeJsonSet.typeFor(postgres, dbType, docType, prop.isNullable()); + return ScalarTypeJsonSet.typeFor(postgres, dbType, docType, prop.isNullable(), keepSource); } else { return createJsonObjectMapperType(prop, dbType, docType); } } if (type.equals(Map.class)) { if (!hasJacksonAnnotations && isMapValueTypeObject(genericType)) { - return ScalarTypeJsonMap.typeFor(postgres, dbType); + return ScalarTypeJsonMap.typeFor(postgres, dbType, keepSource); } else { return createJsonObjectMapperType(prop, dbType, DocPropertyType.OBJECT); } } - if (objectMapperPresent) { + if (objectMapperPresent && prop.getMutationDetection() == MutationDetection.DEFAULT) { if (type.equals(JsonNode.class)) { switch (dbType) { case Types.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 a196085a6..699c7ad0f 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 @@ -19,47 +19,47 @@ import java.util.List; /** * Types for mapping List in JSON format to DB types VARCHAR, JSON and JSONB. */ -public class ScalarTypeJsonList { +class ScalarTypeJsonList { /** * Return the appropriate ScalarType based requested dbType and if Postgres. */ - public static ScalarType typeFor(boolean postgres, int dbType, DocPropertyType docType, boolean nullable) { + 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); + return new ScalarTypeJsonList.JsonB(docType, nullable, keepSource); case DbPlatformType.JSON: - return new ScalarTypeJsonList.Json(docType, nullable); + return new ScalarTypeJsonList.Json(docType, nullable, keepSource); } } - return new ScalarTypeJsonList.Varchar(docType, nullable); + return new ScalarTypeJsonList.Varchar(docType, nullable, keepSource); } /** * List mapped to DB VARCHAR. */ - public static class Varchar extends ScalarTypeJsonList.Base { - public Varchar(DocPropertyType docType, boolean nullable) { - super(Types.VARCHAR, docType, nullable); + static final class Varchar extends ScalarTypeJsonList.Base { + Varchar(DocPropertyType docType, boolean nullable, boolean keepSource) { + super(Types.VARCHAR, docType, nullable, keepSource); } } /** * List mapped to Postgres JSON. */ - private static class Json extends ScalarTypeJsonList.PgBase { - public Json(DocPropertyType docType, boolean nullable) { - super(DbPlatformType.JSON, PostgresHelper.JSON_TYPE, docType, nullable); + private final static class Json extends ScalarTypeJsonList.PgBase { + Json(DocPropertyType docType, boolean nullable, boolean keepSource) { + super(DbPlatformType.JSON, PostgresHelper.JSON_TYPE, docType, nullable, keepSource); } } /** * List mapped to Postgres JSONB. */ - private static class JsonB extends ScalarTypeJsonList.PgBase { - public JsonB(DocPropertyType docType, boolean nullable) { - super(DbPlatformType.JSONB, PostgresHelper.JSONB_TYPE, docType, nullable); + private static final class JsonB extends ScalarTypeJsonList.PgBase { + JsonB(DocPropertyType docType, boolean nullable, boolean keepSource) { + super(DbPlatformType.JSONB, PostgresHelper.JSONB_TYPE, docType, nullable, keepSource); } } @@ -68,14 +68,24 @@ public class ScalarTypeJsonList { */ @SuppressWarnings("rawtypes") private abstract static class Base extends ScalarTypeJsonCollection { + final boolean keepSource; - public Base(int dbType, DocPropertyType docType, boolean nullable) { + private Base(int dbType, DocPropertyType docType, boolean nullable, boolean keepSource) { super(List.class, dbType, docType, nullable); + this.keepSource = keepSource; } @Override - public List read(DataReader reader) throws SQLException { + public final boolean isJsonMapper() { + return keepSource; + } + + @Override + public final List read(DataReader reader) throws SQLException { String json = reader.getString(); + if (keepSource) { + reader.pushJson(json); + } try { // parse JSON into modifyAware list return EJson.parseList(json, true); @@ -85,17 +95,15 @@ public class ScalarTypeJsonList { } @Override - public void bind(DataBinder binder, List value) throws SQLException { + 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 if (value.isEmpty()) { - binder.setString("[]"); } else { - try { - binder.setString(EJson.write(value)); - } catch (IOException e) { - throw new SQLException("Failed to format List into JSON content", e); - } + bindRawJson(binder, rawJson); } } @@ -108,8 +116,15 @@ public class ScalarTypeJsonList { } } + protected void bindRawJson(DataBinder binder, String rawJson) throws SQLException { + binder.setString(rawJson); + } + @Override - public String formatValue(List value) { + public final String formatValue(List value) { + if (value.isEmpty()) { + return "[]"; + } try { return EJson.write(value); } catch (IOException e) { @@ -118,7 +133,7 @@ public class ScalarTypeJsonList { } @Override - public List parse(String value) { + public final List parse(String value) { try { return EJson.parseList(value, false); } catch (IOException e) { @@ -127,12 +142,12 @@ public class ScalarTypeJsonList { } @Override - public List jsonRead(JsonParser parser) throws IOException { + public final List jsonRead(JsonParser parser) throws IOException { return EJson.parseList(parser, parser.getCurrentToken()); } @Override - public void jsonWrite(JsonGenerator writer, List value) throws IOException { + public final void jsonWrite(JsonGenerator writer, List value) throws IOException { EJson.write(value, writer); } } @@ -144,23 +159,18 @@ public class ScalarTypeJsonList { final String pgType; - PgBase(int jdbcType, String pgType, DocPropertyType docType, boolean nullable) { - super(jdbcType, docType, nullable); + PgBase(int jdbcType, String pgType, DocPropertyType docType, boolean nullable, boolean keepSource) { + super(jdbcType, docType, nullable, keepSource); this.pgType = pgType; } - @SuppressWarnings("rawtypes") @Override - public void bind(DataBinder binder, List value) throws SQLException { - if (value == null) { - bindNull(binder); - } else { - binder.setObject(PostgresHelper.asObject(pgType, formatValue(value))); - } + protected final void bindRawJson(DataBinder binder, String rawJson) throws SQLException { + binder.setObject(PostgresHelper.asObject(pgType, rawJson)); } @Override - protected void bindNull(DataBinder binder) throws SQLException { + 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 6dd4f900b..93ecdab79 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 @@ -9,12 +9,7 @@ import io.ebean.core.type.DocPropertyType; import io.ebean.text.TextException; import io.ebean.text.json.EJson; -import java.io.DataInput; -import java.io.DataOutput; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; +import java.io.*; import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.sql.Types; @@ -24,97 +19,110 @@ import java.util.Map; * Type which maps Map to various DB types (Clob, Varchar, Blob) in JSON format. */ @SuppressWarnings("rawtypes") -public abstract class ScalarTypeJsonMap extends ScalarTypeBase { - - private static final ScalarTypeJsonMap CLOB = new ScalarTypeJsonMap.Clob(); - private static final ScalarTypeJsonMap BLOB = new ScalarTypeJsonMap.Blob(); - private static final ScalarTypeJsonMap VARCHAR = new ScalarTypeJsonMap.Varchar(); - private static final ScalarTypeJsonMap JSON = new ScalarTypeJsonMapPostgres.JSON(); - private static final ScalarTypeJsonMap JSONB = new ScalarTypeJsonMapPostgres.JSONB(); +abstract class ScalarTypeJsonMap extends ScalarTypeBase { /** * Return the ScalarType for the requested dbType and postgres. */ - public static ScalarTypeJsonMap typeFor(boolean postgres, int dbType) { + static ScalarTypeJsonMap typeFor(boolean postgres, int dbType, boolean keepSource) { switch (dbType) { case Types.VARCHAR: - return VARCHAR; + return new ScalarTypeJsonMap.Varchar(keepSource); case Types.BLOB: - return BLOB; + return new ScalarTypeJsonMap.Blob(keepSource); case Types.CLOB: - return CLOB; + return new ScalarTypeJsonMap.Clob(keepSource); case DbPlatformType.JSONB: - return postgres ? JSONB : CLOB; + return postgres ? new ScalarTypeJsonMapPostgres.JSONB(keepSource) : new ScalarTypeJsonMap.Clob(keepSource); case DbPlatformType.JSON: - return postgres ? JSON : CLOB; + return postgres ? new ScalarTypeJsonMapPostgres.JSON(keepSource) : new ScalarTypeJsonMap.Clob(keepSource); default: throw new IllegalStateException("Unknown dbType " + dbType); } } - public static class Clob extends ScalarTypeJsonMap { - - public Clob() { - super(Types.CLOB); + private static final class Clob extends ScalarTypeJsonMap { + Clob(boolean keepSource) { + super(Types.CLOB, keepSource); } @Override - public Map read(DataReader reader) throws SQLException { - String content = reader.getStringFromStream(); - if (content == null) { - return null; - } - return parse(content); + protected String readJson(DataReader reader) throws SQLException { + return reader.getStringFromStream(); } } - public static class Varchar extends ScalarTypeJsonMap { - - public Varchar() { - super(Types.VARCHAR); + private static final class Varchar extends ScalarTypeJsonMap { + Varchar(boolean keepSource) { + super(Types.VARCHAR, keepSource); } } - public static class Blob extends ScalarTypeJsonMap { - public Blob() { - super(Types.BLOB); + 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 { - try (InputStreamReader inputStreamReader = new InputStreamReader(is)) { - return parse(inputStreamReader); + if (keepSource) { + StringWriter jsonBuffer = new StringWriter(); + try (InputStreamReader streamReader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + transferTo(streamReader, jsonBuffer); + } + String rawJson = jsonBuffer.toString(); + reader.pushJson(rawJson); + return parse(rawJson); + } else { + try (InputStreamReader streamReader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + return parse(streamReader); + } } } catch (IOException e) { throw new SQLException("Error reading Blob stream from DB", e); } } - @Override - public void bind(DataBinder binder, Map value) throws SQLException { - if (value == null) { - binder.setNull(Types.BLOB); - } else { - String rawJson = formatValue(value); - binder.setBytes(rawJson.getBytes(StandardCharsets.UTF_8)); + 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)); + } + } - public ScalarTypeJsonMap(int jdbcType) { + 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 boolean isMutable() { + public final boolean isMutable() { return true; } @@ -126,37 +134,60 @@ public abstract class ScalarTypeJsonMap extends ScalarTypeBase { return TypeJsonManager.checkIsDirty(value); } + @Override + public final boolean isJsonMapper() { + return keepSource; + } + @Override public Map read(DataReader reader) throws SQLException { - String rawJson = reader.getString(); + String rawJson = readJson(reader); + if (keepSource) { + reader.pushJson(rawJson); + } if (rawJson == null) { return null; } return parse(rawJson); } - @Override - public void bind(DataBinder binder, Map value) throws SQLException { - if (value == null) { - binder.setNull(Types.VARCHAR); - } else { - String rawJson = formatValue(value); - binder.setString(rawJson); - } + protected String readJson(DataReader reader) throws SQLException { + return reader.getString(); } @Override - public Object toJdbcType(Object value) { + 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 Map toBeanType(Object value) { + public final Map toBeanType(Object value) { return (Map) value; } @Override - public String formatValue(Map v) { + public final String formatValue(Map v) { try { return EJson.write(v); } catch (IOException e) { @@ -165,7 +196,7 @@ public abstract class ScalarTypeJsonMap extends ScalarTypeBase { } @Override - public Map parse(String value) { + public final Map parse(String value) { try { // return a modify aware map return EJson.parseObject(value, true); @@ -174,7 +205,7 @@ public abstract class ScalarTypeJsonMap extends ScalarTypeBase { } } - public Map parse(Reader reader) { + public final Map parse(Reader reader) { try { // return a modify aware map return EJson.parseObject(reader, true); @@ -184,17 +215,17 @@ public abstract class ScalarTypeJsonMap extends ScalarTypeBase { } @Override - public Map convertFromMillis(long dateTime) { + public final Map convertFromMillis(long dateTime) { throw new RuntimeException("Should never be called"); } @Override - public boolean isDateTimeCapable() { + public final boolean isDateTimeCapable() { return false; } @Override - public Map readData(DataInput dataInput) throws IOException { + public final Map readData(DataInput dataInput) throws IOException { if (!dataInput.readBoolean()) { return null; } else { @@ -203,7 +234,7 @@ public abstract class ScalarTypeJsonMap extends ScalarTypeBase { } @Override - public void writeData(DataOutput dataOutput, Map map) throws IOException { + public final void writeData(DataOutput dataOutput, Map map) throws IOException { if (map == null) { dataOutput.writeBoolean(false); } else { @@ -212,17 +243,17 @@ public abstract class ScalarTypeJsonMap extends ScalarTypeBase { } @Override - public void jsonWrite(JsonGenerator writer, Map value) throws IOException { + public final void jsonWrite(JsonGenerator writer, Map value) throws IOException { EJson.write(value, writer); } @Override - public Map jsonRead(JsonParser parser) throws IOException { + public final Map jsonRead(JsonParser parser) throws IOException { return EJson.parseObject(parser, parser.getCurrentToken()); } @Override - public DocPropertyType getDocType() { + public final DocPropertyType getDocType() { return DocPropertyType.OBJECT; } 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 index 87eecc5f9..ae33fa0e4 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java @@ -4,44 +4,46 @@ import io.ebean.config.dbplatform.DbPlatformType; import io.ebean.core.type.DataBinder; import java.sql.SQLException; -import java.util.Map; /** * Support for the Postgres DB types JSON and JSONB. */ -public abstract class ScalarTypeJsonMapPostgres extends ScalarTypeJsonMap { +abstract class ScalarTypeJsonMapPostgres extends ScalarTypeJsonMap { - final String postgresType; + private final String postgresType; - ScalarTypeJsonMapPostgres(int jdbcType, String postgresType) { - super(jdbcType); + ScalarTypeJsonMapPostgres(int jdbcType, String postgresType, boolean keepSource) { + super(jdbcType, keepSource); this.postgresType = postgresType; } - @SuppressWarnings("rawtypes") @Override - public void bind(DataBinder binder, Map value) throws SQLException { - String rawJson = (value == null) ? null : formatValue(value); + 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. */ - public static class JSON extends ScalarTypeJsonMapPostgres { + static final class JSON extends ScalarTypeJsonMapPostgres { - public JSON() { - super(DbPlatformType.JSON, PostgresHelper.JSON_TYPE); + JSON(boolean keepSource) { + super(DbPlatformType.JSON, PostgresHelper.JSON_TYPE, keepSource); } } /** * ScalarType mapping java Map type to Postgres JSONB database type. */ - public static class JSONB extends ScalarTypeJsonMapPostgres { + static final class JSONB extends ScalarTypeJsonMapPostgres { - public JSONB() { - super(DbPlatformType.JSONB, PostgresHelper.JSONB_TYPE); + JSONB(boolean keepSource) { + super(DbPlatformType.JSONB, PostgresHelper.JSONB_TYPE, keepSource); } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java index 41cb28d49..65cb88cdc 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java @@ -46,7 +46,7 @@ class ScalarTypeJsonObjectMapper { /** * No mutation detection on this json property. */ - private static class NoMutationDetection extends Base { + private static final class NoMutationDetection extends Base { NoMutationDetection(TypeJsonManager jsonManager, AnnotatedField field, int dbType, DocPropertyType docType) { super(Object.class, jsonManager, field, dbType, docType); @@ -66,7 +66,7 @@ class ScalarTypeJsonObjectMapper { /** * Supports HASH and SOURCE dirty detection modes. */ - private static class GenericObject extends Base { + private static final class GenericObject extends Base { GenericObject(TypeJsonManager jsonManager, AnnotatedField field, int dbType, DocPropertyType docType) { super(Object.class, jsonManager, field, dbType, docType); @@ -167,20 +167,20 @@ class ScalarTypeJsonObjectMapper { } @Override - public Object toJdbcType(Object value) { + public final Object toJdbcType(Object value) { // no type conversion supported return value; } @Override @SuppressWarnings("unchecked") - public T toBeanType(Object value) { + public final T toBeanType(Object value) { // no type conversion supported return (T) value; } @Override - public String formatValue(T value) { + public final String formatValue(T value) { try { return objectWriter.writeValueAsString(value); } catch (JsonProcessingException e) { @@ -189,7 +189,7 @@ class ScalarTypeJsonObjectMapper { } @Override - public T parse(String value) { + public final T parse(String value) { try { return objectReader.readValue(value, deserType); } catch (IOException e) { @@ -198,32 +198,32 @@ class ScalarTypeJsonObjectMapper { } @Override - public DocPropertyType getDocType() { + public final DocPropertyType getDocType() { return docType; } @Override - public boolean isDateTimeCapable() { + public final boolean isDateTimeCapable() { return false; } @Override - public T convertFromMillis(long dateTime) { + public final T convertFromMillis(long dateTime) { throw new IllegalStateException("Not supported"); } @Override - public T jsonRead(JsonParser parser) throws IOException { + public final T jsonRead(JsonParser parser) throws IOException { return objectReader.readValue(parser, deserType); } @Override - public void jsonWrite(JsonGenerator writer, T value) throws IOException { + public final void jsonWrite(JsonGenerator writer, T value) throws IOException { objectWriter.writeValue(writer, value); } @Override - public T readData(DataInput dataInput) throws IOException { + public final T readData(DataInput dataInput) throws IOException { if (!dataInput.readBoolean()) { return null; } else { @@ -232,7 +232,7 @@ class ScalarTypeJsonObjectMapper { } @Override - public void writeData(DataOutput dataOutput, T value) throws IOException { + public final void writeData(DataOutput dataOutput, T value) throws IOException { if (value == null) { dataOutput.writeBoolean(false); } else { 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 d95adb572..86a39443e 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 @@ -21,63 +21,71 @@ import java.util.Set; /** * Types for mapping List in JSON format to DB types VARCHAR, JSON and JSONB. */ -public class ScalarTypeJsonSet { +final class ScalarTypeJsonSet { /** * Return the appropriate ScalarType for the requested dbType and Postgres. */ - public static ScalarType typeFor(boolean postgres, int dbType, DocPropertyType docPropertyType, boolean nullable) { + static ScalarType typeFor(boolean postgres, int dbType, DocPropertyType docPropertyType, boolean nullable, boolean keepSource) { if (postgres) { switch (dbType) { case DbPlatformType.JSONB: - return new ScalarTypeJsonSet.JsonB(docPropertyType, nullable); + return new ScalarTypeJsonSet.JsonB(docPropertyType, nullable, keepSource); case DbPlatformType.JSON: - return new ScalarTypeJsonSet.Json(docPropertyType, nullable); + return new ScalarTypeJsonSet.Json(docPropertyType, nullable, keepSource); } } - return new ScalarTypeJsonSet.Varchar(docPropertyType, nullable); + return new ScalarTypeJsonSet.Varchar(docPropertyType, nullable, keepSource); } /** * List mapped to DB VARCHAR. */ - public static class Varchar extends ScalarTypeJsonSet.Base { - public Varchar(DocPropertyType docPropertyType, boolean nullable) { - super(Types.VARCHAR, docPropertyType, nullable); + static final class Varchar extends ScalarTypeJsonSet.Base { + public Varchar(DocPropertyType docPropertyType, boolean nullable, boolean keepSource) { + super(Types.VARCHAR, docPropertyType, nullable, keepSource); } } /** * List mapped to Postgres JSON. */ - private static class Json extends ScalarTypeJsonSet.PgBase { - public Json(DocPropertyType docPropertyType, boolean nullable) { - super(DbPlatformType.JSON, PostgresHelper.JSON_TYPE, docPropertyType, nullable); + 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); } } /** * List mapped to Postgres JSONB. */ - private static class JsonB extends ScalarTypeJsonSet.PgBase { - public JsonB(DocPropertyType docPropertyType, boolean nullable) { - super(DbPlatformType.JSONB, PostgresHelper.JSONB_TYPE, docPropertyType, nullable); + 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); } } - /** - * Base class for List handling. - */ @SuppressWarnings("rawtypes") private abstract static class Base extends ScalarTypeJsonCollection { - public Base(int dbType, DocPropertyType docPropertyType, boolean nullable) { + final boolean keepSource; + + private Base(int dbType, DocPropertyType docPropertyType, boolean nullable, boolean keepSource) { super(Set.class, dbType, docPropertyType, nullable); + this.keepSource = keepSource; } @Override - public Set read(DataReader reader) throws SQLException { + public final boolean isJsonMapper() { + return keepSource; + } + + @Override + public final Set read(DataReader reader) throws SQLException { String json = reader.getString(); + if (keepSource) { + reader.pushJson(json); + } try { // parse JSON into modifyAware list return EJson.parseSet(json, true); @@ -87,17 +95,15 @@ public class ScalarTypeJsonSet { } @Override - public void bind(DataBinder binder, Set value) throws SQLException { + 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 if (value.isEmpty()) { - binder.setString("[]"); } else { - try { - binder.setString(EJson.write(value)); - } catch (IOException e) { - throw new SQLException("Failed to format Set into JSON content", e); - } + bindRawJson(binder, rawJson); } } @@ -110,8 +116,15 @@ public class ScalarTypeJsonSet { } } + protected void bindRawJson(DataBinder binder, String rawJson) throws SQLException { + binder.setString(rawJson); + } + @Override - public String formatValue(Set value) { + public final String formatValue(Set value) { + if (value.isEmpty()) { + return "[]"; + } try { return EJson.write(value); } catch (IOException e) { @@ -120,7 +133,7 @@ public class ScalarTypeJsonSet { } @Override - public Set parse(String value) { + public final Set parse(String value) { try { return convertList(EJson.parseList(value)); } catch (IOException e) { @@ -129,12 +142,12 @@ public class ScalarTypeJsonSet { } @Override - public Set jsonRead(JsonParser parser) throws IOException { + public final Set jsonRead(JsonParser parser) throws IOException { return convertList(EJson.parseList(parser, parser.getCurrentToken())); } @Override - public void jsonWrite(JsonGenerator writer, Set value) throws IOException { + public final void jsonWrite(JsonGenerator writer, Set value) throws IOException { EJson.write(value, writer); } @@ -151,23 +164,18 @@ public class ScalarTypeJsonSet { final String pgType; - PgBase(int jdbcType, String pgType, DocPropertyType docPropertyType, boolean nullable) { - super(jdbcType, docPropertyType, nullable); + PgBase(int jdbcType, String pgType, DocPropertyType docPropertyType, boolean nullable, boolean keepSource) { + super(jdbcType, docPropertyType, nullable, keepSource); this.pgType = pgType; } - @SuppressWarnings("rawtypes") @Override - public void bind(DataBinder binder, Set value) throws SQLException { - if (value == null) { - bindNull(binder); - } else { - binder.setObject(PostgresHelper.asObject(pgType, formatValue(value))); - } + protected final void bindRawJson(DataBinder binder, String rawJson) throws SQLException { + binder.setObject(PostgresHelper.asObject(pgType, rawJson)); } @Override - protected void bindNull(DataBinder binder) throws SQLException { + protected final void bindNull(DataBinder binder) throws SQLException { binder.setObject(PostgresHelper.asObject(pgType, nullable ? null : "[]")); } } diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeJsonListTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeJsonListTest.java index ff329efac..43ed867ba 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeJsonListTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeJsonListTest.java @@ -11,19 +11,19 @@ public class ScalarTypeJsonListTest extends BasePlatformArrayTypeFactoryTest { @Test public void typeFor_expect_nullToEmpty_when_postgresNonNull() throws SQLException { - assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, false)); - assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, false)); + assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, false, false)); + assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, false, false)); - assertBindNullTo_EmptyString(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, false)); + assertBindNullTo_EmptyString(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, false, false)); } @Test public void typeFor_expect_nullToNull_when_nullable() throws SQLException { - assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, true)); - assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, true)); + assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, true, false)); + assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, true, false)); - assertBindNullTo_Null(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, true)); + assertBindNullTo_Null(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, true, false)); } } diff --git a/ebean-core/src/test/java/org/tests/json/TestOldValue.java b/ebean-core/src/test/java/org/tests/json/TestOldValue.java new file mode 100644 index 000000000..f7bd391e5 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/json/TestOldValue.java @@ -0,0 +1,113 @@ +package org.tests.json; + +import io.ebean.BaseTestCase; +import io.ebean.DB; +import io.ebean.ValuePair; + +import org.assertj.core.api.SoftAssertions; +import org.junit.Ignore; +import org.junit.Test; +import org.tests.model.json.EBasicOldValue; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class TestOldValue extends BaseTestCase { + + + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testDbJsonOldValue() throws Exception { + EBasicOldValue bean = new EBasicOldValue(); + JsonNodeFactory jnf = new JsonNodeFactory(false); + + bean.getStringList().add("sl1"); + bean.getStringSet().add("ss1"); + bean.getObjectMap().put("sk1","sm1"); + bean.getLongList().add(1L); + bean.getLongSet().add(1001L); + bean.getLongMap().put("lk1",2001L); + bean.getIntList().add(2); + bean.getIntSet().add(1002); + bean.getIntMap().put("ik1",2002); + + bean.setJsonNode(jnf.arrayNode().add("Foo")); + + DB.save(bean); + bean = DB.find(EBasicOldValue.class, bean.getId()); + + bean.getStringList().add("sl2"); + bean.getStringSet().add("ss2"); + bean.getObjectMap().put("sk2","sm2"); + bean.getLongList().add(5L); + bean.getLongSet().add(1005L); + bean.getLongMap().put("lk2",2005L); + bean.getIntList().add(6); + bean.getIntSet().add(1006); + bean.getIntMap().put("ik2",2006); + ((ArrayNode)bean.getJsonNode()).add("Bar"); + + Map dirty = DB.getBeanState(bean).getDirtyValues(); + SoftAssertions softly = new SoftAssertions(); + softly.assertThat(dirty).hasSize(10); + + softly.assertThat((List)dirty.get("stringList").getOldValue()).containsExactly("sl1"); + softly.assertThat((List)dirty.get("stringList").getNewValue()).containsExactly("sl1", "sl2"); + softly.assertThat((List)dirty.get("longList").getOldValue()).containsExactly(1L); + softly.assertThat((List)dirty.get("longList").getNewValue()).containsExactly(1L, 5L); + softly.assertThat((List)dirty.get("intList").getOldValue()).containsExactly(2); + softly.assertThat((List)dirty.get("intList").getNewValue()).containsExactly(2, 6); + + softly.assertThat((Set)dirty.get("stringSet").getOldValue()).containsExactly("ss1"); + softly.assertThat((Set)dirty.get("stringSet").getNewValue()).containsExactly("ss1", "ss2"); + softly.assertThat((Set)dirty.get("longSet").getOldValue()).containsExactly(1001L); + softly.assertThat((Set)dirty.get("longSet").getNewValue()).containsExactly(1001L, 1005L); + softly.assertThat((Set)dirty.get("intSet").getOldValue()).containsExactly(1002); + softly.assertThat((Set)dirty.get("intSet").getNewValue()).containsExactly(1002, 1006); + + softly.assertThat((Map)dirty.get("objectMap").getOldValue()).containsEntry("sk1","sm1").hasSize(1); + softly.assertThat((Map)dirty.get("objectMap").getNewValue()).containsEntry("sk1","sm1").containsEntry("sk2","sm2").hasSize(2); + softly.assertThat((Map)dirty.get("longMap").getOldValue()).containsEntry("lk1",2001L).hasSize(1); + softly.assertThat((Map)dirty.get("longMap").getNewValue()).containsEntry("lk1",2001L).containsEntry("lk2",2005L).hasSize(2); + softly.assertThat((Map)dirty.get("intMap").getOldValue()).containsEntry("ik1",2002).hasSize(1); + softly.assertThat((Map)dirty.get("intMap").getNewValue()).containsEntry("ik1",2002).containsEntry("ik2",2006).hasSize(2); + + softly.assertThat((ArrayNode)dirty.get("jsonNode").getOldValue()).hasToString("[\"Foo\"]"); + softly.assertThat((ArrayNode)dirty.get("jsonNode").getNewValue()).hasToString("[\"Foo\",\"Bar\"]"); + + softly.assertAll(); + + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + @Ignore("Old value detection does not work for @DbArray") + public void testDbArrayOldValue() throws Exception { + EBasicOldValue bean = new EBasicOldValue(); + + bean.getStringArr().add("sa1"); + + + DB.save(bean); + bean = DB.find(EBasicOldValue.class, bean.getId()); + + bean.getStringArr().add("sa2"); + + + + Map dirty = DB.getBeanState(bean).getDirtyValues(); + SoftAssertions softly = new SoftAssertions(); + softly.assertThat(dirty).hasSize(1); + + softly.assertThat((List)dirty.get("stringArr").getOldValue()).containsExactly("sa1"); + softly.assertThat((List)dirty.get("stringArr").getNewValue()).containsExactly("sa1", "sa2"); + + softly.assertAll(); + + } +} diff --git a/ebean-core/src/test/java/org/tests/model/json/EBasicOldValue.java b/ebean-core/src/test/java/org/tests/model/json/EBasicOldValue.java new file mode 100644 index 000000000..37e805412 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/model/json/EBasicOldValue.java @@ -0,0 +1,161 @@ +package org.tests.model.json; + +import io.ebean.annotation.DbArray; +import io.ebean.annotation.DbJson; + +import javax.persistence.Entity; +import javax.persistence.Id; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.*; + +import static io.ebean.annotation.MutationDetection.SOURCE; + +@Entity +public class EBasicOldValue { + + @Id + Long id; + + String name; + + @DbJson(mutationDetection = SOURCE) + Set stringSet = new LinkedHashSet<>(); + + @DbJson(mutationDetection = SOURCE) + Set longSet = new LinkedHashSet<>(); + + @DbJson(mutationDetection = SOURCE) + Set intSet = new LinkedHashSet<>(); + + @DbJson(mutationDetection = SOURCE) + List stringList = new ArrayList<>(); + + @DbJson(mutationDetection = SOURCE) + List longList = new ArrayList<>(); + + @DbJson(mutationDetection = SOURCE) + List intList = new ArrayList<>(); + + @DbJson(mutationDetection = SOURCE) + Map objectMap = new LinkedHashMap<>(); + + @DbJson(mutationDetection = SOURCE) + Map longMap = new LinkedHashMap<>(); + + @DbJson(mutationDetection = SOURCE) + Map intMap = new LinkedHashMap<>(); + + @DbJson(mutationDetection = SOURCE) + JsonNode jsonNode; + + @DbArray() + List stringArr = new ArrayList<>(); + + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getStringSet() { + return stringSet; + } + + public void setStringSet(Set stringSet) { + this.stringSet = stringSet; + } + + public Set getLongSet() { + return longSet; + } + + public void setLongSet(Set longSet) { + this.longSet = longSet; + } + + public Set getIntSet() { + return intSet; + } + + public void setIntSet(Set intSet) { + this.intSet = intSet; + } + + public List getStringList() { + return stringList; + } + + public void setStringList(List stringList) { + this.stringList = stringList; + } + + public List getLongList() { + return longList; + } + + public void setLongList(List longList) { + this.longList = longList; + } + + public List getIntList() { + return intList; + } + + public void setIntList(List intList) { + this.intList = intList; + } + + public Map getObjectMap() { + return objectMap; + } + + public void setObjectMap(Map objectMap) { + this.objectMap = objectMap; + } + + public Map getLongMap() { + return longMap; + } + + public void setLongMap(Map longMap) { + this.longMap = longMap; + } + + public Map getIntMap() { + return intMap; + } + + public void setIntMap(Map intMap) { + this.intMap = intMap; + } + + public JsonNode getJsonNode() { + return jsonNode; + } + + public void setJsonNode(JsonNode jsonNode) { + this.jsonNode = jsonNode; + } + + public List getStringArr() { + return stringArr; + } + + public void setStringArr(List stringArr) { + this.stringArr = stringArr; + } + +}