diff --git a/src/main/java/com/avaje/ebean/annotation/DbJsonB.java b/src/main/java/com/avaje/ebean/annotation/DbJsonB.java index 1982c8db7..ebfaf9270 100644 --- a/src/main/java/com/avaje/ebean/annotation/DbJsonB.java +++ b/src/main/java/com/avaje/ebean/annotation/DbJsonB.java @@ -36,4 +36,8 @@ import java.lang.annotation.Target; @Target(ElementType.FIELD) public @interface DbJsonB { + /** + * For VARCHAR storage specify the column length. + */ + int length() default 0; } \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DbType.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbType.java index b2b7080aa..4c51be3de 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/DbType.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbType.java @@ -117,14 +117,26 @@ public class DbType { * the scale defined by deployment on a specific property. */ public String renderType(int deployLength, int deployScale) { + return renderType(deployLength, deployScale, true); + } + + /** + * Render the type defining strict mode. + *

+ * If strict mode if OFF then this will render with a scale value even if + * that is not strictly supported. The reason for supporting this is to enable + * use to use types like jsonb(200) as a "logical" type that maps to JSONB for + * Postgres and VARCHAR(200) for other databases. + *

+ */ + public String renderType(int deployLength, int deployScale, boolean strict) { StringBuilder sb = new StringBuilder(); sb.append(name); - if (canHaveLength) { + if (canHaveLength || !strict) { // see if there is a precision/scale to add (or not) int len = deployLength != 0 ? deployLength : defaultLength; - if (len > 0) { sb.append("("); sb.append(len); diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java index ab15a30e1..9dbe17b2e 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java @@ -108,9 +108,9 @@ public class DbTypeMap { if (logicalTypes) { // keep it logical for 2 layer DDL generation - put(DbType.HSTORE, new DbType("hstore")); - put(DbType.JSON, new DbType("json")); - put(DbType.JSONB, new DbType("jsonb")); + put(DbType.HSTORE, new DbType("hstore", false)); + put(DbType.JSON, new DbType("json", false)); + put(DbType.JSONB, new DbType("jsonb", false)); put(DbType.JSONClob, new DbType("jsonclob")); put(DbType.JSONBlob, new DbType("jsonblob")); put(DbType.JSONVarchar, new DbType("jsonvarchar", 1000)); @@ -137,7 +137,7 @@ public class DbTypeMap { /** * Lookup the platform specific DbType given the standard sql type name. */ - public DbType lookup(String name) { + public DbType lookup(String name, boolean withScale) { name = name.trim().toUpperCase(); Integer typeKey = lookup.get(name); if (typeKey == null) { @@ -152,18 +152,19 @@ public class DbTypeMap { case DbType.JSONVarchar: return get(Types.VARCHAR); case DbType.JSON: - return getJsonType(DbType.JSON); + return getJsonType(DbType.JSON, withScale); case DbType.JSONB: - return getJsonType(DbType.JSONB); + return getJsonType(DbType.JSONB, withScale); default: return get(typeKey); } } - private DbType getJsonType(int type) { + private DbType getJsonType(int type, boolean withScale) { DbType dbType = get(type); if (dbType == JSON_CLOB_PLACEHOLDER) { - return get(Types.CLOB); + // if we have scale that implies this maps to varchar + return withScale ? get(Types.VARCHAR) : get(Types.CLOB); } if (dbType == JSON_BLOB_PLACEHOLDER) { return get(Types.BLOB); diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java index 090a3c7b0..17a537988 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java @@ -43,9 +43,9 @@ public class PostgresPlatform extends DatabasePlatform { DbType dbTypeText = new DbType("text"); DbType dbBytea = new DbType("bytea", false); - dbTypeMap.put(DbType.HSTORE, new DbType("hstore")); - dbTypeMap.put(DbType.JSON, new DbType("json")); - dbTypeMap.put(DbType.JSONB, new DbType("jsonb")); + dbTypeMap.put(DbType.HSTORE, new DbType("hstore", false)); + dbTypeMap.put(DbType.JSON, new DbType("json", false)); + dbTypeMap.put(DbType.JSONB, new DbType("jsonb", false)); dbTypeMap.put(Types.INTEGER, new DbType("integer", false)); dbTypeMap.put(Types.DOUBLE, new DbType("float")); diff --git a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/util/PlatformTypeConverter.java b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/util/PlatformTypeConverter.java index d41ba3611..53fe5f23b 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/util/PlatformTypeConverter.java +++ b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/util/PlatformTypeConverter.java @@ -47,7 +47,7 @@ public class PlatformTypeConverter { String suffix = close + 1 < columnDefinition.length() ? columnDefinition.substring(close + 1) : ""; String type = columnDefinition.substring(0, open); try { - DbType dbType = platformTypes.lookup(type); + DbType dbType = platformTypes.lookup(type, true); int comma = columnDefinition.indexOf(',', open); if (comma > -1) { // scale and precision - decimal(10,4) @@ -73,7 +73,7 @@ public class PlatformTypeConverter { protected String convertNoScale(String columnDefinition) { try { - DbType dbType = platformTypes.lookup(columnDefinition); + DbType dbType = platformTypes.lookup(columnDefinition, false); return dbType.renderType(0, 0); } catch (IllegalArgumentException e) { diff --git a/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildContext.java b/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildContext.java index 52ac6c998..dc1c3cf49 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildContext.java +++ b/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildContext.java @@ -108,12 +108,15 @@ public class ModelBuildContext { } - public String getColumnDefn(BeanProperty p) { + /** + * Render the DB type for this property given the strict mode. + */ + public String getColumnDefn(BeanProperty p, boolean strict) { DbType dbType = getDbType(p); if (dbType == null) { throw new IllegalStateException("Unknown DbType mapping for " + p.getFullBeanName()); } - return p.renderDbType(dbType); + return p.renderDbType(dbType, strict); } private DbType getDbType(BeanProperty p) { diff --git a/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildIntersectionTable.java b/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildIntersectionTable.java index d895bcbb4..d85d1ca51 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildIntersectionTable.java +++ b/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildIntersectionTable.java @@ -107,7 +107,7 @@ public class ModelBuildIntersectionTable { throw new RuntimeException("Could not find id property for " + findPropColumn); } - MColumn col = new MColumn(column, ctx.getColumnDefn(p), true); + MColumn col = new MColumn(column, ctx.getColumnDefn(p, true), true); col.setPrimaryKey(true); table.addColumn(col); } diff --git a/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildPropertyVisitor.java b/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildPropertyVisitor.java index 3ed4c2bac..a2f694385 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildPropertyVisitor.java +++ b/src/main/java/com/avaje/ebean/dbmigration/model/build/ModelBuildPropertyVisitor.java @@ -184,7 +184,7 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor { if (importedProperty == null) { throw new RuntimeException("Imported BeanProperty not found?"); } - String columnDefn = ctx.getColumnDefn(importedProperty); + String columnDefn = ctx.getColumnDefn(importedProperty, true); String refColumn = importedProperty.getDbColumn(); MColumn col = table.addColumn(dbCol, columnDefn, !p.isNullable()); @@ -229,7 +229,9 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor { return; } - MColumn col = new MColumn(p.getDbColumn(), ctx.getColumnDefn(p)); + // using non-strict mode to render the DB type such that we have a + // "logical" type like jsonb(200) that can map to JSONB or VARCHAR(200) + MColumn col = new MColumn(p.getDbColumn(), ctx.getColumnDefn(p, false)); col.setDefaultValue(p.getDbColumnDefault()); col.setComment(p.getDbComment()); col.setDraftOnly(p.isDraftOnly()); diff --git a/src/main/java/com/avaje/ebean/text/json/EJson.java b/src/main/java/com/avaje/ebean/text/json/EJson.java index bc38e30d4..3090f0bf4 100644 --- a/src/main/java/com/avaje/ebean/text/json/EJson.java +++ b/src/main/java/com/avaje/ebean/text/json/EJson.java @@ -90,9 +90,15 @@ public class EJson { return EJsonReader.parseObject(parser, token); } + /** + * Parse the json and return as a modify aware List. + */ + public static List parseList(String json, boolean modifyAware) throws IOException { + return EJsonReader.parseList(json, modifyAware); + } + /** * Parse the json and return as a List. - * @throws IOException */ public static List parseList(String json) throws IOException { return EJsonReader.parseList(json); @@ -100,7 +106,6 @@ public class EJson { /** * Parse the json and return as a List taking a Reader. - * @throws IOException */ public static List parseList(Reader reader) throws IOException { return EJsonReader.parseList(reader); @@ -110,10 +115,16 @@ public class EJson { * Parse the json and return as a List taking a JsonParser. */ public static List parseList(JsonParser parser) throws IOException { - return EJsonReader.parseList(parser); + return EJsonReader.parseList(parser, false); } - - + + /** + * Parse the json returning as a List taking into account the current token. + */ + public static List parseList(JsonParser parser, JsonToken currentToken) throws IOException { + return (List)EJsonReader.parse(parser, currentToken, false); + } + /** * Parse the json and return as a List or Map. */ diff --git a/src/main/java/com/avaje/ebean/text/json/EJsonReader.java b/src/main/java/com/avaje/ebean/text/json/EJsonReader.java index 15badf35b..dba25227c 100644 --- a/src/main/java/com/avaje/ebean/text/json/EJsonReader.java +++ b/src/main/java/com/avaje/ebean/text/json/EJsonReader.java @@ -51,6 +51,11 @@ class EJsonReader { 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); @@ -62,15 +67,21 @@ class EJsonReader { } @SuppressWarnings("unchecked") - static List parseList(JsonParser parser) throws IOException { - return (List) parse(parser); + static List parseList(JsonParser parser, boolean modifyAware) throws IOException { + return (List) parse(parser, modifyAware); } static Object parse(String json) throws IOException { + if (json == null) { + return null; + } return parse(new StringReader(json)); } static Object parse(String json, boolean modifyAware) throws IOException { + if (json == null) { + return null; + } return parse(new StringReader(json), modifyAware); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java index 06091ed55..04aa3ec51 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java @@ -984,11 +984,11 @@ public class BeanProperty implements ElPropertyValue, Property { /** * Return the DB column type definition. */ - public String renderDbType(DbType dbType) { + public String renderDbType(DbType dbType, boolean strict) { if (dbColumnDefn != null) { return dbColumnDefn; } - return dbType.renderType(dbLength, dbScale); + return dbType.renderType(dbLength, dbScale, strict); } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java index 978fa0694..3d7a8cfd5 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java @@ -175,12 +175,16 @@ public class AnnotationFields extends AnnotationParser { if (comment != null) { prop.setDbComment(comment.value()); } + if (get(prop, DbHstore.class) != null) { + util.setDbHstore(prop); + } DbJson dbJson = get(prop, DbJson.class); if (dbJson != null) { util.setDbJsonType(prop, dbJson); } else { - if (get(prop, DbJsonB.class) != null) { - util.setDbJsonBType(prop); + DbJsonB dbJsonB = get(prop, DbJsonB.class); + if (dbJsonB != null) { + util.setDbJsonBType(prop, dbJsonB); } } @@ -197,10 +201,6 @@ public class AnnotationFields extends AnnotationParser { prop.setDocProperty(docProperty); } - if (get(prop, DbHstore.class) != null) { - util.setDbHstore(prop); - } - Formula formula = get(prop, Formula.class); if (formula != null) { prop.setSqlFormula(formula.select(), formula.join()); diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployCreateProperties.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployCreateProperties.java index c4961e248..071c57fbb 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployCreateProperties.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployCreateProperties.java @@ -223,21 +223,18 @@ public class DeployCreateProperties { Class propertyType = field.getType(); ManyToOne manyToOne = field.getAnnotation(ManyToOne.class); - if (manyToOne != null){ Class tt = manyToOne.targetEntity(); - - if (tt != null && !tt.equals(void.class)){ + if (!tt.equals(void.class)){ propertyType = tt; } } - if (isMappedType(field)) { + if (isSpecialScalarType(field)) { return new DeployBeanProperty(desc, propertyType, null, null); } // check for Collection type (list, set or map) ManyType manyType = determineManyType.getManyType(propertyType); - if (manyType != null) { // List, Set or Map based object Class targetType = determineTargetType(field); @@ -299,7 +296,7 @@ public class DeployCreateProperties { /** * Return true if the field has one of the special mappings. */ - private boolean isMappedType(Field field) { + private boolean isSpecialScalarType(Field field) { return (field.getAnnotation(DbJson.class) != null) || (field.getAnnotation(DbJsonB.class) != null) || (field.getAnnotation(DbHstore.class) != null); diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployUtil.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployUtil.java index 01b89783a..52124a5f6 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployUtil.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployUtil.java @@ -6,6 +6,7 @@ import javax.persistence.Enumerated; import javax.persistence.PersistenceException; import com.avaje.ebean.annotation.DbJson; +import com.avaje.ebean.annotation.DbJsonB; import com.avaje.ebean.annotation.DbJsonType; import com.avaje.ebean.config.EncryptDeploy; import com.avaje.ebean.config.EncryptDeployManager; @@ -103,6 +104,7 @@ public class DeployUtil { return new DataEncryptSupport(encryptKeyManager, bytesEncryptor, table, column); } + @SuppressWarnings("unchecked") public void setEnumScalarType(Enumerated enumerated, DeployBeanProperty prop) { Class enumType = prop.getPropertyType(); @@ -200,11 +202,9 @@ public class DeployUtil { * Map to Postgres HSTORE type. */ public void setDbHstore(DeployBeanProperty prop) { - ScalarType scalarType = typeManager.getScalarType(DbType.HSTORE); if (scalarType == null) { - // this should never occur actually - throw new RuntimeException("No ScalarType found for HSTORE on [" + prop.getFullBeanName() + "]"); + throw new RuntimeException("No ScalarType found for HSTORE on [" + prop.getFullBeanName() + "] ?"); } prop.setDbType(DbType.HSTORE); prop.setScalarType(scalarType); @@ -219,22 +219,20 @@ public class DeployUtil { setDbJsonType(prop, dbType, dbJsonType.length()); } - public void setDbJsonBType(DeployBeanProperty prop) { - setDbJsonType(prop, DbType.JSONB, 0); + public void setDbJsonBType(DeployBeanProperty prop, DbJsonB dbJsonB) { + setDbJsonType(prop, DbType.JSONB, dbJsonB.length()); } private void setDbJsonType(DeployBeanProperty prop, int dbType, int dbLength) { Class type = prop.getPropertyType(); - - ScalarType scalarType = typeManager.getJsonScalarType(type, dbType); + ScalarType scalarType = typeManager.getJsonScalarType(type, dbType, dbLength); if (scalarType == null) { - // this should never occur actually throw new RuntimeException("No ScalarType for JSON type [" + type + "] [" + dbType + "]"); } prop.setDbType(dbType); prop.setScalarType(scalarType); - if (dbType == Types.VARCHAR) { + if (dbType == Types.VARCHAR || dbLength > 0) { // determine the db column size int columnLength = (dbLength > 0) ? dbLength : DEFAULT_JSON_VARCHAR_LENGTH; prop.setDbLength(columnLength); diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java b/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java index 3f063c9db..44c116fe9 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java @@ -155,6 +155,8 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { private final boolean java7Present; + private final boolean postgres; + // OPTIONAL ScalarTypes registered if Jackson/JsonNode is in the classpath /** @@ -195,14 +197,14 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { this.objectMapperPresent = config.getClassLoadConfig().isJacksonObjectMapperPresent(); this.extraTypeFactory = new DefaultTypeFactory(config); + this.postgres = isPostgres(config.getDatabasePlatform()); initialiseStandard(jsonDateTime, config); initialiseJavaTimeTypes(jsonDateTime, config); initialiseJodaTypes(jsonDateTime, config); initialiseJacksonTypes(config); - if (isPostgres(config.getDatabasePlatform())) { - // Postgres has special DB types for JSON/JSONB + if (postgres) { this.jsonMapJson = new ScalarTypeJsonMapPostgres.JSON(); this.jsonMapJsonb = new ScalarTypeJsonMapPostgres.JSONB(); } else { @@ -368,7 +370,17 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { } @Override - public ScalarType getJsonScalarType(Class type, int dbType) { + public ScalarType getJsonScalarType(Class type, int dbType, int dbLength) { + + if (type.equals(List.class)) { + if (postgres) { + switch (dbType) { + case DbType.JSONB: return ScalarTypeJsonList.JSONB; + case DbType.JSON: return ScalarTypeJsonList.JSON; + } + } + return ScalarTypeJsonList.VARCHAR; + } if (type.equals(Map.class)) { // @DbJson Map property diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareList.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareList.java index 646116ddc..10b35fcbd 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareList.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareList.java @@ -24,6 +24,10 @@ public class ModifyAwareList implements List, ModifyAwareOwner { this.owner = owner; } + public String toString() { + return list.toString(); + } + @Override public boolean isMarkedDirty() { return owner.isMarkedDirty(); diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/PostgresHelper.java b/src/main/java/com/avaje/ebeaninternal/server/type/PostgresHelper.java new file mode 100644 index 000000000..e1d53bf09 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/PostgresHelper.java @@ -0,0 +1,29 @@ +package com.avaje.ebeaninternal.server.type; + +import org.postgresql.util.PGobject; + +import java.sql.SQLException; + +public class PostgresHelper { + + /** + * The Postgres JSON DB type. + */ + public static final String JSON_TYPE = "json"; + + /** + * The Postgres JSONB DB type. + */ + public static final String JSONB_TYPE = "jsonb"; + + /** + * Construct and return Postgres specific PG object. + */ + public static Object asObject(String pgType, String rawJson) throws SQLException { + + PGobject pgo = new PGobject(); + pgo.setType(pgType); + pgo.setValue(rawJson); + return pgo; + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonCollection.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonCollection.java new file mode 100644 index 000000000..d37442385 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonCollection.java @@ -0,0 +1,75 @@ +package com.avaje.ebeaninternal.server.type; + +import com.avaje.ebeanservice.docstore.api.mapping.DocPropertyType; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +abstract class ScalarTypeJsonCollection extends ScalarTypeBase { + + public ScalarTypeJsonCollection(Class type, int dbType) { + super(type, false, dbType); + } + + /** + * Consider as a mutable type. Use the isDirty() method to check for dirty state. + */ + @Override + public boolean isMutable() { + return true; + } + + /** + * Return true if the value should be considered dirty (and included in an update). + */ + @Override + public boolean isDirty(Object value) { + return !(value instanceof ModifyAwareOwner) || ((ModifyAwareOwner) value).isMarkedDirty(); + } + + @Override + public Object toJdbcType(Object value) { + return value; + } + + @Override + @SuppressWarnings("unchecked") + public T toBeanType(Object value) { + return (T)value; + } + + @Override + public DocPropertyType getDocType() { + return DocPropertyType.LIST; + } + + @Override + public boolean isDateTimeCapable() { + return false; + } + + @Override + public T convertFromMillis(long dateTime) { + return null; + } + + @Override + public T readData(DataInput dataInput) throws IOException { + if (!dataInput.readBoolean()) { + return null; + } else { + return parse(dataInput.readUTF()); + } + } + + @Override + public void writeData(DataOutput dataOutput, T value) throws IOException { + if (value == null) { + dataOutput.writeBoolean(false); + } else { + ScalarHelp.writeUTF(dataOutput, format(value)); + } + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonList.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonList.java new file mode 100644 index 000000000..6af41ce8e --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonList.java @@ -0,0 +1,137 @@ +package com.avaje.ebeaninternal.server.type; + +import com.avaje.ebean.config.dbplatform.DbType; +import com.avaje.ebean.text.json.EJson; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; + +import javax.persistence.PersistenceException; +import java.io.IOException; +import java.sql.SQLException; +import java.sql.Types; +import java.util.List; + +/** + * Types for mapping List in JSON format to DB types VARCHAR, JSON and JSONB. + */ +public class ScalarTypeJsonList { + + public static final ScalarTypeJsonCollection VARCHAR = new ScalarTypeJsonList.Varchar(); + + public static final ScalarTypeJsonCollection JSON = new ScalarTypeJsonList.Json(); + + public static final ScalarTypeJsonCollection JSONB = new ScalarTypeJsonList.JsonB(); + + /** + * List mapped to DB VARCHAR. + */ + private static class Varchar extends ScalarTypeJsonList.Base { + public Varchar() { + super(Types.VARCHAR); + } + } + + /** + * List mapped to Postgres JSON. + */ + private static class Json extends ScalarTypeJsonList.PgBase { + public Json() { + super(DbType.JSON, PostgresHelper.JSON_TYPE); + } + } + + /** + * List mapped to Postgres JSONB. + */ + private static class JsonB extends ScalarTypeJsonList.PgBase { + public JsonB() { + super(DbType.JSONB, PostgresHelper.JSONB_TYPE); + } + } + + /** + * Base class for List handling. + */ + private abstract static class Base extends ScalarTypeJsonCollection { + + public Base(int dbType) { + super(List.class, dbType); + } + + @Override + public List read(DataReader dataReader) throws SQLException { + try { + // parse JSON into modifyAware list + return EJson.parseList(dataReader.getString(), true); + } catch (IOException e) { + throw new SQLException("Failed to parse JSON content as List: ["+ dataReader.getString() +"]", e); + } + } + + @Override + public void bind(DataBind b, List value) throws SQLException { + + if (value == null) { + b.setNull(Types.VARCHAR); + } else if (value.isEmpty()) { + b.setString("[]"); + } else { + try { + b.setString(EJson.write(value)); + } catch (IOException e) { + throw new SQLException("Failed to format List into JSON content", e); + } + } + } + + @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); + } + } + + @Override + public List parse(String value) { + try { + return EJson.parseList(value, false); + } catch (IOException e) { + throw new PersistenceException("Failed to parse JSON content as List: ["+value+"]", e); + } + } + + @Override + public List jsonRead(JsonParser parser) throws IOException { + return EJson.parseList(parser, parser.getCurrentToken()); + } + + @Override + public void jsonWrite(JsonGenerator 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) { + super(jdbcType); + this.pgType = pgType; + } + + @Override + public void bind(DataBind bind, List value) throws SQLException { + + String rawJson = (value == null) ? null : formatValue(value); + bind.setObject(PostgresHelper.asObject(pgType, rawJson)); + } + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMap.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMap.java index e24811032..753bdd8b2 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMap.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMap.java @@ -2,6 +2,7 @@ package com.avaje.ebeaninternal.server.type; import com.avaje.ebean.text.TextException; import com.avaje.ebean.text.json.EJson; +import com.avaje.ebeaninternal.util.EncodeUtil; import com.avaje.ebeanservice.docstore.api.mapping.DocPropertyType; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -12,7 +13,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.sql.Types; import java.util.Map; @@ -77,7 +77,7 @@ public abstract class ScalarTypeJsonMap extends ScalarTypeBase { b.setNull(Types.BLOB); } else { String rawJson = formatValue(value); - b.setBytes(rawJson.getBytes(StandardCharsets.UTF_8)); + b.setBytes(EncodeUtil.utf8ToBytes(rawJson)); } } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java index 388826545..cd51a3fff 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java @@ -1,7 +1,6 @@ package com.avaje.ebeaninternal.server.type; import com.avaje.ebean.config.dbplatform.DbType; -import org.postgresql.util.PGobject; import java.sql.SQLException; import java.util.Map; @@ -11,10 +10,6 @@ import java.util.Map; */ public abstract class ScalarTypeJsonMapPostgres extends ScalarTypeJsonMap { - private static final String POSTGRES_TYPE_JSON = "json"; - - private static final String POSTGRES_TYPE_JSONB = "jsonb"; - final String postgresType; ScalarTypeJsonMapPostgres(int jdbcType, String postgresType) { @@ -23,14 +18,10 @@ public abstract class ScalarTypeJsonMapPostgres extends ScalarTypeJsonMap { } @Override - public void bind(DataBind b, Map value) throws SQLException { + public void bind(DataBind bind, Map value) throws SQLException { String rawJson = (value == null) ? null : formatValue(value); - - PGobject pgo = new PGobject(); - pgo.setType(postgresType); - pgo.setValue(rawJson); - b.setObject(pgo); + bind.setObject(PostgresHelper.asObject(postgresType, rawJson)); } /** @@ -39,7 +30,7 @@ public abstract class ScalarTypeJsonMapPostgres extends ScalarTypeJsonMap { public static class JSON extends ScalarTypeJsonMapPostgres { public JSON() { - super(DbType.JSON, POSTGRES_TYPE_JSON); + super(DbType.JSON, PostgresHelper.JSON_TYPE); } } @@ -49,7 +40,7 @@ public abstract class ScalarTypeJsonMapPostgres extends ScalarTypeJsonMap { public static class JSONB extends ScalarTypeJsonMapPostgres { public JSONB() { - super(DbType.JSONB, POSTGRES_TYPE_JSONB); + super(DbType.JSONB, PostgresHelper.JSONB_TYPE); } } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNode.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNode.java index 16c3447b2..70abd5ad5 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNode.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNode.java @@ -1,6 +1,7 @@ package com.avaje.ebeaninternal.server.type; import com.avaje.ebean.text.TextException; +import com.avaje.ebeaninternal.util.EncodeUtil; import com.avaje.ebeanservice.docstore.api.mapping.DocPropertyType; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -13,7 +14,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.sql.Types; @@ -85,7 +85,7 @@ public abstract class ScalarTypeJsonNode extends ScalarTypeBase { dataBind.setNull(Types.BLOB); } else { String rawJson = formatValue(value); - dataBind.setBlob(rawJson.getBytes(StandardCharsets.UTF_8)); + dataBind.setBlob(EncodeUtil.utf8ToBytes(rawJson)); } } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNodePostgres.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNodePostgres.java index ae12e8fe4..ae01ee6b8 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNodePostgres.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNodePostgres.java @@ -3,7 +3,6 @@ package com.avaje.ebeaninternal.server.type; import com.avaje.ebean.config.dbplatform.DbType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.postgresql.util.PGobject; import java.sql.SQLException; @@ -12,10 +11,6 @@ import java.sql.SQLException; */ public abstract class ScalarTypeJsonNodePostgres extends ScalarTypeJsonNode { - private static final String POSTGRES_TYPE_JSON = "json"; - - private static final String POSTGRES_TYPE_JSONB = "jsonb"; - final ObjectMapper objectMapper; final String postgresType; @@ -27,14 +22,9 @@ public abstract class ScalarTypeJsonNodePostgres extends ScalarTypeJsonNode { } @Override - public void bind(DataBind dataBind, JsonNode value) throws SQLException { - + public void bind(DataBind bind, JsonNode value) throws SQLException { String rawJson = (value == null) ? null : formatValue(value); - - PGobject pgo = new PGobject(); - pgo.setType(postgresType); - pgo.setValue(rawJson); - dataBind.setObject(pgo); + bind.setObject(PostgresHelper.asObject(postgresType, rawJson)); } /** @@ -43,7 +33,7 @@ public abstract class ScalarTypeJsonNodePostgres extends ScalarTypeJsonNode { public static class JSON extends ScalarTypeJsonNodePostgres { public JSON(ObjectMapper objectMapper) { - super(objectMapper, DbType.JSON, POSTGRES_TYPE_JSON); + super(objectMapper, DbType.JSON, PostgresHelper.JSON_TYPE); } } @@ -53,7 +43,7 @@ public abstract class ScalarTypeJsonNodePostgres extends ScalarTypeJsonNode { public static class JSONB extends ScalarTypeJsonNodePostgres { public JSONB(ObjectMapper objectMapper) { - super(objectMapper, DbType.JSONB, POSTGRES_TYPE_JSONB); + super(objectMapper, DbType.JSONB, PostgresHelper.JSONB_TYPE); } } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java b/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java index 56c2119d3..be47722d0 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java @@ -63,5 +63,5 @@ public interface TypeManager { * Note that type expected to be JsonNode or Map. *

*/ - ScalarType getJsonScalarType(Class type, int dbType); + ScalarType getJsonScalarType(Class type, int dbType, int dbLength); } diff --git a/src/test/java/com/avaje/ebean/config/dbplatform/DbTypeMapTest.java b/src/test/java/com/avaje/ebean/config/dbplatform/DbTypeMapTest.java index 3a0fc9055..abf0d6d6e 100644 --- a/src/test/java/com/avaje/ebean/config/dbplatform/DbTypeMapTest.java +++ b/src/test/java/com/avaje/ebean/config/dbplatform/DbTypeMapTest.java @@ -13,16 +13,16 @@ public class DbTypeMapTest { PostgresPlatform pg = new PostgresPlatform(); DbTypeMap dbTypeMap = pg.getDbTypeMap(); - assertThat(dbTypeMap.lookup("clob").renderType(0,0)).isEqualTo("text"); - assertThat(dbTypeMap.lookup("CLOB").renderType(0, 0)).isEqualTo("text"); + assertThat(dbTypeMap.lookup("clob", false).renderType(0,0)).isEqualTo("text"); + assertThat(dbTypeMap.lookup("CLOB", false).renderType(0, 0)).isEqualTo("text"); - assertThat(dbTypeMap.lookup("varchar").renderType(20,0)).isEqualTo("varchar(20)"); + assertThat(dbTypeMap.lookup("varchar", true).renderType(20,0)).isEqualTo("varchar(20)"); - assertThat(dbTypeMap.lookup("json").renderType(0,0)).isEqualTo("json"); - assertThat(dbTypeMap.lookup("jsonb").renderType(0,0)).isEqualTo("jsonb"); - assertThat(dbTypeMap.lookup("jsonclob").renderType(0,0)).isEqualTo("text"); - assertThat(dbTypeMap.lookup("jsonblob").renderType(0,0)).isEqualTo("bytea"); - assertThat(dbTypeMap.lookup("jsonvarchar").renderType(200,0)).isEqualTo("varchar(200)"); + assertThat(dbTypeMap.lookup("json", false).renderType(0,0)).isEqualTo("json"); + assertThat(dbTypeMap.lookup("jsonb", false).renderType(0,0)).isEqualTo("jsonb"); + assertThat(dbTypeMap.lookup("jsonclob", false).renderType(0,0)).isEqualTo("text"); + assertThat(dbTypeMap.lookup("jsonblob", false).renderType(0,0)).isEqualTo("bytea"); + assertThat(dbTypeMap.lookup("jsonvarchar", false).renderType(200,0)).isEqualTo("varchar(200)"); } @@ -31,7 +31,7 @@ public class DbTypeMapTest { DbTypeMap dbTypeMap = DbTypeMap.logicalTypes(); DbType dbType = dbTypeMap.get(DbType.JSON); - DbType json = dbTypeMap.lookup("json"); + DbType json = dbTypeMap.lookup("json", false); assertThat(dbType).isSameAs(json); } diff --git a/src/test/java/com/avaje/ebean/json/EJsonTests.java b/src/test/java/com/avaje/ebean/json/EJsonTests.java index 366831bf1..239fa0eaa 100644 --- a/src/test/java/com/avaje/ebean/json/EJsonTests.java +++ b/src/test/java/com/avaje/ebean/json/EJsonTests.java @@ -246,4 +246,18 @@ public class EJsonTests { } + + @Test + public void parse_when_null() throws IOException { + + Object nothing = EJson.parse((String) null); + Assert.assertNull(nothing); + } + + @Test + public void parseList_when_null() throws IOException { + + Object nothing = EJson.parseList((String) null); + Assert.assertNull(nothing); + } } diff --git a/src/test/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataSerializeTest.java b/src/test/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataSerializeTest.java index 9d8907573..dc7eec116 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataSerializeTest.java +++ b/src/test/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataSerializeTest.java @@ -98,7 +98,7 @@ public class CachedBeanDataSerializeTest extends BaseTestCase { TBytesOnly bean = new TBytesOnly(); bean.setId(42); - bean.setContent(stringContent.getBytes("UTF8")); + bean.setContent(stringContent.getBytes("UTF-8")); BeanDescriptor desc = getBeanDescriptor(TBytesOnly.class); CachedBeanData extract = CachedBeanDataFromBean.extract(desc, (EntityBean)bean); diff --git a/src/test/java/com/avaje/tests/json/TestDbJson_List.java b/src/test/java/com/avaje/tests/json/TestDbJson_List.java new file mode 100644 index 000000000..34e7891f7 --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestDbJson_List.java @@ -0,0 +1,78 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.tests.model.json.EBasicJsonList; +import org.avaje.ebeantest.LoggedSqlCollector; +import org.junit.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; + +public class TestDbJson_List extends BaseTestCase { + + private EBasicJsonList bean = new EBasicJsonList(); + + private EBasicJsonList found; + + @Test + public void insert() { + + bean.setName("stuff"); + + List tags = bean.getTags(); + tags.add("one"); + tags.add("two"); + + Ebean.save(bean); + + found = Ebean.find(EBasicJsonList.class, bean.getId()); + + assertThat(found.getTags()).containsExactly("one", "two"); + + json_parse_format(); + update_when_notDirty(); + update_when_dirty(); + } + + //@Test//(dependsOnMethods = "insert") + public void json_parse_format() { + + String asJson = Ebean.json().toJson(found); + assertThat(asJson).contains("\"tags\":[\"one\",\"two\"]"); + assertThat(asJson).contains("\"id\":"); + + EBasicJsonList fromJson = Ebean.json().toBean(EBasicJsonList.class, asJson); + assertEquals(found.getId(), fromJson.getId()); + assertEquals(found.getId(), fromJson.getId()); + assertEquals(found.getName(), fromJson.getName()); + assertThat(fromJson.getTags()).containsExactly("one", "two"); + } + + //@Test//(dependsOnMethods = "insert") + public void update_when_notDirty() { + + found.setName("mod"); + LoggedSqlCollector.start(); + Ebean.save(found); + List sql = LoggedSqlCollector.stop(); + + // we don't update the phone numbers (as they are not dirty) + assertThat(sql.get(0)).contains("update ebasic_json_list set name=?, version=? where"); + } + + public void update_when_dirty() { + + //found.setName("modAgain"); + found.getTags().add("three"); + + LoggedSqlCollector.start(); + Ebean.save(found); + List sql = LoggedSqlCollector.stop(); + + // we don't update the phone numbers (as they are not dirty) + assertThat(sql.get(0)).contains("update ebasic_json_list set tags=?, version=? where"); + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonList.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonList.java new file mode 100644 index 000000000..cf694cb23 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonList.java @@ -0,0 +1,57 @@ +package com.avaje.tests.model.json; + +import com.avaje.ebean.annotation.DbJson; +import com.avaje.ebean.annotation.DbJsonType; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; +import java.util.ArrayList; +import java.util.List; + +@Entity +public class EBasicJsonList { + + @Id + Long id; + + String name; + + @DbJson(length = 100, storage = DbJsonType.VARCHAR) + List tags = new ArrayList(); + + @Version + Long version; + + 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 List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } +} diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 5ecf16998..466673fd5 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -76,9 +76,9 @@ - - - + + +