diff --git a/pom.xml b/pom.xml index e82a15113..d80270f84 100644 --- a/pom.xml +++ b/pom.xml @@ -57,10 +57,18 @@ com.fasterxml.jackson.core - jackson-core - 2.4.3 + jackson-databind + 2.5.3 - + + + + com.fasterxml.jackson.core + jackson-core + 2.5.3 + provided + + javax.transaction jta @@ -68,6 +76,7 @@ provided + javax.validation validation-api @@ -89,6 +98,14 @@ provided + + + org.postgresql + postgresql + 9.4-1201-jdbc41 + provided + + com.squareup.okhttp okhttp @@ -124,13 +141,6 @@ test - - postgresql - postgresql - 8.4-701.jdbc4 - test - - org.hsqldb hsqldb @@ -254,8 +264,6 @@ Ebean 4 src/main/java/com/avaje/ebean/overview.html - - 1.8 org.avaje.doclet.PygmentsDoclet com.avaje.ebeaninternal.*:com.avaje.ebean.util diff --git a/src/main/java/com/avaje/ebean/annotation/DbJson.java b/src/main/java/com/avaje/ebean/annotation/DbJson.java new file mode 100644 index 000000000..d5bf2b059 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/DbJson.java @@ -0,0 +1,44 @@ +package com.avaje.ebean.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specify a property holding JSON content. + * + * By default the content will be stored in a DB Clob except on Postgres where DB JSON type is used. + * + * Example: + * {@code + * + * // Store as JSON on Postgres or Clob on other databases + * @DbJson + * Map content; + * + * } + * + * Example with JSONB storage + * {@code + * + * // Store as JSONB on Postgres or Clob on other databases + * @DbJson(storage = DbJsonType.JSONB) + * Map content; + * + * } + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface DbJson { + + /** + * Specify the database type used to store the JSON content. + */ + DbJsonType storage() default DbJsonType.JSON; + + /** + * For VARCHAR storage specify the column length (defaults to 3000). + */ + int length() default 0; +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/annotation/DbJsonB.java b/src/main/java/com/avaje/ebean/annotation/DbJsonB.java new file mode 100644 index 000000000..1982c8db7 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/DbJsonB.java @@ -0,0 +1,39 @@ +package com.avaje.ebean.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specify a property holding JSON content. + * + * The content will be stored on Postgres using it's JSONB type and as Clob for other databases. + * + * + * This is equivalent to using @DbJson(storage = DbJsonType.JSONB) + * + * + * Example: + * {@code + * + * // Store as JSONB on Postgres or Clob on other databases + * @DbJsonB + * Map content; + * + * } + * + * Equivalent to: + * {@code + * + * // Store as JSONB on Postgres or Clob on other databases + * @DbJson(storage = DbJsonType.JSONB) + * Map content; + * + * } + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface DbJsonB { + +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/annotation/DbJsonType.java b/src/main/java/com/avaje/ebean/annotation/DbJsonType.java new file mode 100644 index 000000000..75ee1d930 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/DbJsonType.java @@ -0,0 +1,32 @@ +package com.avaje.ebean.annotation; + +/** + * Specify the DB storage type used to store JSON content. + */ +public enum DbJsonType { + + /** + * Store as JSON on Postgres and for other databases store as CLOB. + */ + JSON, + + /** + * Store as JSONB on Postgres and for other databases store as CLOB. + */ + JSONB, + + /** + * Store as database VARCHAR. + */ + VARCHAR, + + /** + * Store as database CLOB. + */ + CLOB, + + /** + * Store as database BLOB. + */ + BLOB +} diff --git a/src/main/java/com/avaje/ebean/config/ServerConfig.java b/src/main/java/com/avaje/ebean/config/ServerConfig.java index d6362251e..20f0ea183 100644 --- a/src/main/java/com/avaje/ebean/config/ServerConfig.java +++ b/src/main/java/com/avaje/ebean/config/ServerConfig.java @@ -291,6 +291,7 @@ public class ServerConfig { private int queryCacheMaxSize = 1000; private int queryCacheMaxIdleTime = 600; private int queryCacheMaxTimeToLive = 60*60*6; + private Object objectMapper; /** * Construct a Server Configuration for programmatically creating an EbeanServer. @@ -1864,4 +1865,12 @@ public class ServerConfig { return databasePlatform.isDisallowBatchOnCascade() ? PersistBatch.NONE : persistBatchOnCascade; } + + public Object getObjectMapper() { + return objectMapper; + } + + public void setObjectMapper(Object objectMapper) { + this.objectMapper = objectMapper; + } } 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 7b31b43bb..91caa245e 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/DbType.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbType.java @@ -8,6 +8,36 @@ package com.avaje.ebean.config.dbplatform; */ public class DbType { + /** + * Type to map Map content to Postgres HSTORE. + */ + public static final int HSTORE = 5000; + + /** + * Type to map JSON content to Clob or Postgres JSON type. + */ + public static final int JSON = 5001; + + /** + * Type to map JSON content to Clob or Postgres JSONB type. + */ + public static final int JSONB = 5002; + + /** + * Type to map JSON content to VARCHAR. + */ + public static final int JSONVarchar = 5003; + + /** + * Type to map JSON content to Clob. + */ + public static final int JSONClob = 5004; + + /** + * Type to map JSON content to Blob. + */ + public static final int JSONBlob = 5005; + /** * The data type name (VARCHAR, INTEGER ...) */ 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 7cc04c2d0..ca345fe4a 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java @@ -36,6 +36,13 @@ public class DbTypeMap { put(Types.BLOB, new DbType("blob")); put(Types.CLOB, new DbType("clob")); + + put(DbType.JSON, new DbType("clob")); // Postgres maps this to JSON + put(DbType.JSONB, new DbType("clob")); // Postgres maps this to JSONB + put(DbType.JSONClob, new DbType("clob")); + put(DbType.JSONBlob, new DbType("blob")); + put(DbType.JSONVarchar, new DbType("varchar", 1000)); + put(Types.LONGVARBINARY, new DbType("longvarbinary")); put(Types.LONGVARCHAR, new DbType("lonvarchar")); put(Types.VARBINARY, new DbType("varbinary", 255)); 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 021e30bb4..d03d4e690 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java @@ -13,11 +13,6 @@ import java.sql.Types; */ public class PostgresPlatform extends DatabasePlatform { - /** - * Unique jdbc type id defined for hstore type. - */ - public static final int TYPE_HSTORE = 4001; - public PostgresPlatform() { super(); this.name = "postgres"; @@ -43,8 +38,10 @@ public class PostgresPlatform extends DatabasePlatform { this.openQuote = "\""; this.closeQuote = "\""; - dbTypeMap.put(TYPE_HSTORE, new DbType("hstore")); - + dbTypeMap.put(DbType.HSTORE, new DbType("hstore")); + dbTypeMap.put(DbType.JSON, new DbType("json")); + dbTypeMap.put(DbType.JSONB, new DbType("jsonb")); + dbTypeMap.put(Types.INTEGER, new DbType("integer", false)); dbTypeMap.put(Types.DOUBLE, new DbType("float")); dbTypeMap.put(Types.TINYINT, new DbType("smallint")); 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 b624cf41b..947dbbf1e 100644 --- a/src/main/java/com/avaje/ebean/text/json/EJson.java +++ b/src/main/java/com/avaje/ebean/text/json/EJson.java @@ -35,14 +35,29 @@ public class EJson { public static void write(Object object, JsonGenerator jsonGenerator) throws IOException { EJsonWriter.write(object, jsonGenerator); } - + + /** + * Parse the json and return as a Map additionally specifying if the returned map should + * be modify aware meaning that it can detect when it has been modified. + */ + public static Map parseObject(String json, boolean modifyAware) throws IOException { + return EJsonReader.parseObject(json, modifyAware); + } + /** * Parse the json and return as a Map. */ public static Map parseObject(String json) throws IOException { return EJsonReader.parseObject(json); } - + + /** + * Parse the json and return as a Map taking a reader. + */ + public static Map parseObject(Reader reader, boolean modifyAware) throws IOException { + return EJsonReader.parseObject(reader, modifyAware); + } + /** * Parse the json and return as a Map taking a reader. */ 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 67f57a921..7aaa28a6d 100644 --- a/src/main/java/com/avaje/ebean/text/json/EJsonReader.java +++ b/src/main/java/com/avaje/ebean/text/json/EJsonReader.java @@ -9,6 +9,10 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import com.avaje.ebeaninternal.server.type.ModifyAwareFlag; +import com.avaje.ebeaninternal.server.type.ModifyAwareList; +import com.avaje.ebeaninternal.server.type.ModifyAwareMap; +import com.avaje.ebeaninternal.server.type.ModifyAwareOwner; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; @@ -16,7 +20,12 @@ import com.fasterxml.jackson.core.JsonToken; class EJsonReader { static JsonFactory json = new JsonFactory(); - + + @SuppressWarnings("unchecked") + static Map parseObject(String json, boolean modifyAware) throws IOException { + return (Map) parse(json, modifyAware); + } + @SuppressWarnings("unchecked") static Map parseObject(String json) throws IOException { return (Map) parse(json); @@ -26,7 +35,12 @@ class EJsonReader { static Map parseObject(Reader reader) throws IOException { return (Map) parse(reader); } - + + @SuppressWarnings("unchecked") + static Map parseObject(Reader reader, boolean modifyAware) throws IOException { + return (Map) parse(reader, modifyAware); + } + @SuppressWarnings("unchecked") static Map parseObject(JsonParser parser) throws IOException { return (Map) parse(parser); @@ -34,7 +48,7 @@ class EJsonReader { @SuppressWarnings("unchecked") static Map parseObject(JsonParser parser, JsonToken token) throws IOException { - return (Map)parse(parser, token); + return (Map)parse(parser, token, false); } @SuppressWarnings("unchecked") @@ -56,40 +70,58 @@ class EJsonReader { return parse(new StringReader(json)); } + static Object parse(String json, boolean modifyAware) throws IOException { + return parse(new StringReader(json), modifyAware); + } + static Object parse(Reader reader) throws IOException { return parse(json.createParser(reader)); } - static Object parse(JsonParser parser) throws IOException { - return parse(parser, null); + static Object parse(Reader reader, boolean modifyAware) throws IOException { + return parse(json.createParser(reader), modifyAware); } - static Object parse(JsonParser parser, JsonToken token) throws IOException { - return new EJsonReader(parser).parseJson(token); + static Object parse(JsonParser parser) throws IOException { + return parse(parser, null, false); + } + + static Object parse(JsonParser parser, boolean modifyAware) throws IOException { + return parse(parser, null, modifyAware); + } + + static Object parse(JsonParser parser, JsonToken token, boolean modifyAware) throws IOException { + return new EJsonReader(parser, modifyAware).parseJson(token); } private final JsonParser parser; + private final boolean modifyAware; + + private final ModifyAwareFlag modifyAwareOwner; + private int depth; private Stack stack; private Context currentContext; - EJsonReader(JsonParser parser) { + EJsonReader(JsonParser parser, boolean modifyAware) { this.parser = parser; + this.modifyAware = modifyAware; + this.modifyAwareOwner = (modifyAware) ? new ModifyAwareFlag() : null; } private void startArray() { depth++; stack.push(currentContext); - currentContext = new ArrayContext(); + currentContext = modifyAware ? new ArrayContext(modifyAwareOwner) : new ArrayContext(); } private void startObject() { depth++; stack.push(currentContext); - currentContext = new ObjectContext(); + currentContext = modifyAware ? new ObjectContext(modifyAwareOwner) : new ObjectContext(); } private void endArray() { @@ -105,6 +137,9 @@ class EJsonReader { if (!stack.isEmpty()) { currentContext = stack.pop(currentContext); } + if (modifyAwareOwner != null) { + modifyAwareOwner.resetMarkedDirty(); + } } private void setValue(Object value) { @@ -235,10 +270,18 @@ class EJsonReader { private static class ObjectContext extends Context { - private final Map map = new LinkedHashMap(); + private final Map map; private String key; + ObjectContext() { + map = new LinkedHashMap(); + } + + ObjectContext(ModifyAwareOwner owner) { + map = new ModifyAwareMap(owner, new LinkedHashMap()); + } + public void popContext(Context temp) { setValue(temp.getValue()); } @@ -262,7 +305,15 @@ class EJsonReader { private static class ArrayContext extends Context { - private final List values = new ArrayList(); + private final List values; + + ArrayContext() { + values = new ArrayList(); + } + + ArrayContext(ModifyAwareOwner owner) { + values = new ModifyAwareList(owner, new ArrayList()); + } public void popContext(Context temp) { values.add(temp.getValue()); diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java index 56571b3d8..55c9e3c28 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java @@ -97,7 +97,7 @@ public class DeployBeanPropertyLists { discDeployProp.setDbColumn(discriminatorColumn); // create the discriminator BeanProperty and only register it in the propertyMap - BeanProperty dprop = new BeanProperty(owner, desc, discDeployProp); + BeanProperty dprop = new BeanProperty(desc, discDeployProp); propertyMap.put(dprop.getName(), dprop); } @@ -382,6 +382,6 @@ public class DeployBeanPropertyLists { return new BeanPropertyCompound(owner, desc, (DeployBeanPropertyCompound) deployProp); } - return new BeanProperty(owner, desc, deployProp); + return new BeanProperty(desc, deployProp); } } 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 027849d0b..64c9d2dbe 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 @@ -135,6 +135,18 @@ public class AnnotationFields extends AnnotationParser { util.setLobType(prop); } + DbJson dbJson = get(prop, DbJson.class); + if (dbJson != null) { + util.setDbJsonType(prop, dbJson); + } else { + if (get(prop, DbJsonB.class) != null) { + util.setDbJsonBType(prop); + } + } + if (get(prop, ColumnHstore.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 5f320313a..470b7cb7c 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 @@ -1,33 +1,22 @@ package com.avaje.ebeaninternal.server.deploy.parse; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; +import com.avaje.ebean.annotation.ColumnHstore; +import com.avaje.ebean.annotation.DbJson; +import com.avaje.ebean.annotation.DbJsonB; +import com.avaje.ebeaninternal.server.deploy.DetermineManyType; +import com.avaje.ebeaninternal.server.deploy.ManyType; +import com.avaje.ebeaninternal.server.deploy.meta.*; +import com.avaje.ebeaninternal.server.type.CtCompoundType; +import com.avaje.ebeaninternal.server.type.ScalarType; +import com.avaje.ebeaninternal.server.type.TypeManager; +import com.avaje.ebeaninternal.server.type.reflect.CheckImmutableResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.persistence.ManyToOne; import javax.persistence.PersistenceException; import javax.persistence.Transient; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.avaje.ebean.annotation.ColumnHstore; -import com.avaje.ebeaninternal.server.core.Message; -import com.avaje.ebeaninternal.server.deploy.DetermineManyType; -import com.avaje.ebeaninternal.server.deploy.ManyType; -import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanDescriptor; -import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty; -import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany; -import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocOne; -import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyCompound; -import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertySimpleCollection; -import com.avaje.ebeaninternal.server.type.CtCompoundType; -import com.avaje.ebeaninternal.server.type.ScalarType; -import com.avaje.ebeaninternal.server.type.ScalarTypePostgresHstore; -import com.avaje.ebeaninternal.server.type.TypeManager; -import com.avaje.ebeaninternal.server.type.reflect.CheckImmutableResponse; +import java.lang.reflect.*; /** * Create the properties for a bean. @@ -95,15 +84,13 @@ public class DeployCreateProperties { Field field = fields[i]; if (Modifier.isStatic(field.getModifiers())) { // not interested in static fields + logger.trace("Skipping static field {} in {}", field.getName(), beanType.getName()); } else if (Modifier.isTransient(field.getModifiers())) { // not interested in transient fields - logger.trace("Skipping transient field " + field.getName() + " in " + beanType.getName()); + logger.trace("Skipping transient field {} in {}", field.getName(), beanType.getName()); - } else if (ignoreFieldByName(field.getName())) { - // not interested this field (ebean or aspectJ field) - - } else { + } else if (!ignoreFieldByName(field.getName())) { String fieldName = getFieldName(field, beanType); String initFieldName = initCap(fieldName); @@ -118,14 +105,10 @@ public class DeployCreateProperties { prop.setSortOrder((level * 10000 + 100 - i + sortOverride)); DeployBeanProperty replaced = desc.addBeanProperty(prop); - if (replaced != null) { - if (replaced.isTransient()) { - // expected for inheritance... - } else { - String msg = "Huh??? property " + prop.getFullBeanName() + " being defined twice"; - msg += " but replaced property was not transient? This is not expected?"; - logger.warn(msg); - } + if (replaced != null && !replaced.isTransient()) { + String msg = "Huh??? property " + prop.getFullBeanName() + " being defined twice"; + msg += " but replaced property was not transient? This is not expected?"; + logger.warn(msg); } } } @@ -210,33 +193,6 @@ public class DeployCreateProperties { return null; } - /** - * Find a public non-static setter method that matches this field (according to bean-spec rules). - */ - private Method findSetter(Field field, String initFieldName, Method[] declaredMethods, boolean scalaObject) { - - String methSetName = "set" + initFieldName; - String scalaSetName = field.getName() + "_$eq"; - - for (int i = 0; i < declaredMethods.length; i++) { - Method m = declaredMethods[i]; - - if ((scalaObject && m.getName().equals(scalaSetName)) || m.getName().equals(methSetName)) { - - Class>[] params = m.getParameterTypes(); - if (params.length == 1 && field.getType().equals(params[0])) { - if (void.class.equals(m.getReturnType())) { - int modifiers = m.getModifiers(); - if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers)) { - return m; - } - } - } - } - } - return null; - } - @SuppressWarnings({ "unchecked", "rawtypes" }) private DeployBeanProperty createManyType(DeployBeanDescriptor> desc, Class> targetType, ManyType manyType) { @@ -266,16 +222,8 @@ public class DeployCreateProperties { propertyType = tt; } } - Class> innerType = propertyType; - - String specialTypeKey = getSpecialScalarType(field); - if (specialTypeKey != null) { - ScalarType> scalarType = typeManager.getScalarTypeFromKey(specialTypeKey); - if (scalarType == null) { - logger.error("Could not find ScalarType to match key ["+specialTypeKey+"]"); - } else { - return new DeployBeanProperty(desc, propertyType, scalarType, null); - } + if (isMappedType(field)) { + return new DeployBeanProperty(desc, propertyType, null, null); } // check for Collection type (list, set or map) @@ -295,16 +243,16 @@ public class DeployCreateProperties { return createManyType(desc, targetType, manyType); } - if (innerType.isEnum() || innerType.isPrimitive()) { + if (propertyType.isEnum() || propertyType.isPrimitive()) { return new DeployBeanProperty(desc, propertyType, null, null); } - ScalarType> scalarType = typeManager.getScalarType(innerType); + ScalarType> scalarType = typeManager.getScalarType(propertyType); if (scalarType != null) { return new DeployBeanProperty(desc, propertyType, scalarType, null); } - CtCompoundType> compoundType = typeManager.getCompoundType(innerType); + CtCompoundType> compoundType = typeManager.getCompoundType(propertyType); if (compoundType != null) { return new DeployBeanPropertyCompound(desc, propertyType, compoundType, null); } @@ -313,19 +261,19 @@ public class DeployCreateProperties { return null; } try { - CheckImmutableResponse checkImmutable = typeManager.checkImmutable(innerType); + CheckImmutableResponse checkImmutable = typeManager.checkImmutable(propertyType); if (checkImmutable.isImmutable()) { if (checkImmutable.isCompoundType()) { // use reflection to support compound immutable value objects - typeManager.recursiveCreateScalarDataReader(innerType); - compoundType = typeManager.getCompoundType(innerType); + typeManager.recursiveCreateScalarDataReader(propertyType); + compoundType = typeManager.getCompoundType(propertyType); if (compoundType != null) { return new DeployBeanPropertyCompound(desc, propertyType, compoundType, null); } } else { // use reflection to support simple immutable value objects - scalarType = typeManager.recursiveCreateScalarTypes(innerType); + scalarType = typeManager.recursiveCreateScalarTypes(propertyType); return new DeployBeanProperty(desc, propertyType, scalarType, null); } } @@ -338,13 +286,13 @@ public class DeployCreateProperties { } } - private String getSpecialScalarType(Field field) { - - if (field.getAnnotation(ColumnHstore.class) != null) { - return ScalarTypePostgresHstore.KEY; - } - - return null; + /** + * Return true if the field has one of the special mappings. + */ + private boolean isMappedType(Field field) { + return (field.getAnnotation(DbJson.class) != null) + || (field.getAnnotation(DbJsonB.class) != null) + || (field.getAnnotation(ColumnHstore.class) != null); } private boolean isTransientField(Field field) { 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 10471ad7e..d300218af 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 @@ -5,6 +5,8 @@ import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.PersistenceException; +import com.avaje.ebean.annotation.DbJson; +import com.avaje.ebean.annotation.DbJsonType; import com.avaje.ebean.config.EncryptDeploy; import com.avaje.ebean.config.EncryptDeployManager; import com.avaje.ebean.config.EncryptKeyManager; @@ -13,6 +15,7 @@ import com.avaje.ebean.config.NamingConvention; import com.avaje.ebean.config.ServerConfig; import com.avaje.ebean.config.TableName; import com.avaje.ebean.config.dbplatform.DatabasePlatform; +import com.avaje.ebean.config.dbplatform.DbType; import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty; import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyCompound; import com.avaje.ebeaninternal.server.type.DataEncryptSupport; @@ -41,6 +44,8 @@ public class DeployUtil { */ private static final int dbBLOBType = Types.BLOB; + private static final int DEFAULT_JSON_VARCHAR_LENGTH = 3000; + private final NamingConvention namingConvention; private final TypeManager typeManager; @@ -184,6 +189,72 @@ 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() + "]"); + } + prop.setDbType(DbType.HSTORE); + prop.setScalarType(scalarType); + } + + /** + * This property is marked as a Lob object. + */ + public void setDbJsonType(DeployBeanProperty prop, DbJson dbJsonType) { + + int dbType = getDbJsonStorage(dbJsonType.storage()); + setDbJsonType(prop, dbType, dbJsonType.length()); + } + + public void setDbJsonBType(DeployBeanProperty prop) { + setDbJsonType(prop, DbType.JSONB, 0); + } + + private void setDbJsonType(DeployBeanProperty prop, int dbType, int dbLength) { + + Class> type = prop.getPropertyType(); + + ScalarType> scalarType = typeManager.getJsonScalarType(type, dbType); + 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) { + // determine the db column size + int columnLength = (dbLength > 0) ? dbLength : DEFAULT_JSON_VARCHAR_LENGTH; + prop.setDbLength(columnLength); + } + } + + /** + * Return the JDBC type for the JSON storage type. + */ + private int getDbJsonStorage(DbJsonType dbJsonType) { + + switch (dbJsonType) { + case JSON: + return DbType.JSON; + case JSONB: + return DbType.JSONB; + case VARCHAR: + return Types.VARCHAR; + case CLOB: + return Types.CLOB; + case BLOB: + return Types.BLOB; + default: + return DbType.JSON; + } + } + /** * This property is marked as a Lob object. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/DataReader.java b/src/main/java/com/avaje/ebeaninternal/server/type/DataReader.java index 71911a53d..e17fa936f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/DataReader.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/DataReader.java @@ -1,6 +1,7 @@ package com.avaje.ebeaninternal.server.type; import java.io.InputStream; +import java.io.Reader; import java.math.BigDecimal; import java.sql.Array; import java.sql.SQLException; @@ -19,10 +20,14 @@ public interface DataReader { byte[] getBlobBytes() throws SQLException; + InputStream getBlobInputStream() throws SQLException; + String getStringFromStream() throws SQLException; String getStringClob() throws SQLException; + Reader getClobReader() throws SQLException; + String getString() throws SQLException; Boolean getBoolean() throws SQLException; 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 6a94fcefe..8b27cab90 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java @@ -1,5 +1,22 @@ package com.avaje.ebeaninternal.server.type; +import com.avaje.ebean.annotation.EnumMapping; +import com.avaje.ebean.annotation.EnumValue; +import com.avaje.ebean.config.*; +import com.avaje.ebean.config.dbplatform.DatabasePlatform; +import com.avaje.ebean.config.dbplatform.DbType; +import com.avaje.ebeaninternal.api.ClassUtil; +import com.avaje.ebeaninternal.server.core.BootupClasses; +import com.avaje.ebeaninternal.server.lib.util.StringHelper; +import com.avaje.ebeaninternal.server.type.reflect.*; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.joda.time.*; +import org.joda.time.LocalDate; +import org.joda.time.LocalDateTime; +import org.joda.time.LocalTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -13,42 +30,12 @@ import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; import java.time.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Currency; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.TimeZone; -import java.util.UUID; +import java.time.Duration; +import java.time.Instant; +import java.time.Period; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import com.avaje.ebean.config.*; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.joda.time.DateMidnight; -import org.joda.time.DateTime; -import org.joda.time.LocalDate; -import org.joda.time.LocalDateTime; -import org.joda.time.LocalTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.avaje.ebean.annotation.EnumMapping; -import com.avaje.ebean.annotation.EnumValue; -import com.avaje.ebeaninternal.api.ClassUtil; -import com.avaje.ebeaninternal.server.core.BootupClasses; -import com.avaje.ebeaninternal.server.lib.util.StringHelper; -import com.avaje.ebeaninternal.server.type.reflect.CheckImmutable; -import com.avaje.ebeaninternal.server.type.reflect.CheckImmutableResponse; -import com.avaje.ebeaninternal.server.type.reflect.ImmutableMeta; -import com.avaje.ebeaninternal.server.type.reflect.ImmutableMetaFactory; -import com.avaje.ebeaninternal.server.type.reflect.KnownImmutable; -import com.avaje.ebeaninternal.server.type.reflect.ReflectionBasedCompoundType; -import com.avaje.ebeaninternal.server.type.reflect.ReflectionBasedCompoundTypeProperty; -import com.avaje.ebeaninternal.server.type.reflect.ReflectionBasedTypeBuilder; - /** * Default implementation of TypeManager. * @@ -65,10 +52,16 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { private final ConcurrentHashMap> nativeMap; - private final ConcurrentHashMap> customTypeMap; - private final DefaultTypeFactory extraTypeFactory; + private final ScalarTypeJsonMap JSON_MAP_CLOB = new ScalarTypeJsonMap.Clob(); + + private final ScalarTypeJsonMap jsonMapClob = JSON_MAP_CLOB; + private final ScalarTypeJsonMap jsonMapBlob = new ScalarTypeJsonMap.Blob(); + private final ScalarTypeJsonMap jsonMapVarchar = new ScalarTypeJsonMap.Varchar(); + private final ScalarTypeJsonMap jsonMapJson; + private final ScalarTypeJsonMap jsonMapJsonb; + private final ScalarTypeFile fileType = new ScalarTypeFile(); private final ScalarType> charType = new ScalarTypeChar(); @@ -134,9 +127,6 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { */ public DefaultTypeManager(ServerConfig config, BootupClasses bootupClasses) { - int clobType = config.getDatabasePlatform().getClobDbType(); - int blobType = config.getDatabasePlatform().getBlobDbType(); - this.jsonDateTime = config.getJsonDateTime(); this.checkImmutable = new CheckImmutable(this); this.reflectScalarBuilder = new ReflectionBasedTypeBuilder(this); @@ -144,18 +134,24 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { this.compoundTypeMap = new ConcurrentHashMap, CtCompoundType>>(); this.typeMap = new ConcurrentHashMap, ScalarType>>(); this.nativeMap = new ConcurrentHashMap>(); - this.customTypeMap = new ConcurrentHashMap>(); - - this.customTypeMap.put(ScalarTypePostgresHstore.KEY, new ScalarTypePostgresHstore()); this.objectMapperPresent = ClassUtil.isPresent("com.fasterxml.jackson.databind.ObjectMapper", this.getClass()); this.extraTypeFactory = new DefaultTypeFactory(config); - initialiseStandard(jsonDateTime, clobType, blobType, config.isUuidStoreAsBinary()); + initialiseStandard(jsonDateTime, config); initialiseJavaTimeTypes(jsonDateTime, config); initialiseJodaTypes(jsonDateTime); + if (isPostgres(config.getDatabasePlatform())) { + // Postgres has special DB types for JSON/JSONB + this.jsonMapJson = new ScalarTypeJsonMapPostgres.JSON(); + this.jsonMapJsonb = new ScalarTypeJsonMapPostgres.JSONB(); + } else { + this.jsonMapJson = JSON_MAP_CLOB; + this.jsonMapJsonb = JSON_MAP_CLOB; + } + if (bootupClasses != null) { initialiseCustomScalarTypes(jsonDateTime, bootupClasses, config); initialiseScalarConverters(bootupClasses); @@ -163,12 +159,8 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { } } - /** - * Lookup a special or custom scalar type by key. - */ - @Override - public ScalarType> getScalarTypeFromKey(String specialTypeKey) { - return customTypeMap.get(specialTypeKey); + private boolean isPostgres(DatabasePlatform databasePlatform) { + return databasePlatform.getName().toLowerCase().startsWith("postgre"); } public boolean isKnownImmutable(Class> cls) { @@ -295,6 +287,24 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { return reader; } + @Override + public ScalarType> getJsonScalarType(Class> type, int dbType) { + + if (type.equals(Map.class)) { + switch (dbType) { + case Types.VARCHAR : return jsonMapVarchar; + case Types.BLOB: return jsonMapBlob; + case Types.CLOB : return jsonMapClob; + case DbType.JSONB: return jsonMapJsonb; + case DbType.JSON: return jsonMapJson; + default: + return jsonMapJson; + } + } + + throw new IllegalArgumentException("Type [" + type + "] unsupported for @DbJson mapping"); + } + /** * Return a ScalarType for a given class. * @@ -370,8 +380,8 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { return value; } + @SuppressWarnings("ResultOfMethodCallIgnored") private boolean isIntegerType(String s) { - try { Integer.parseInt(s); return true; @@ -685,7 +695,14 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { * Register all the standard types supported. This is the standard JDBC types * plus some other common types such as java.util.Date and java.util.Calendar. */ - protected void initialiseStandard(JsonConfig.DateTime mode, int platformClobType, int platformBlobType, boolean binaryUUID) { + protected void initialiseStandard(JsonConfig.DateTime mode, ServerConfig config) { + + boolean binaryUUID = config.isUuidStoreAsBinary(); + DatabasePlatform databasePlatform = config.getDatabasePlatform(); + int platformClobType = databasePlatform.getClobDbType(); + int platformBlobType = databasePlatform.getBlobDbType(); + + nativeMap.put(DbType.HSTORE, new ScalarTypePostgresHstore()); ScalarType> utilDateType = extraTypeFactory.createUtilDate(mode); typeMap.put(java.util.Date.class, utilDateType); diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareFlag.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareFlag.java new file mode 100644 index 000000000..31081879f --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareFlag.java @@ -0,0 +1,26 @@ +package com.avaje.ebeaninternal.server.type; + +/** + * Detects when content has been modified and as such needs to be persisted (included in an update). + */ +public class ModifyAwareFlag implements ModifyAwareOwner { + + boolean dirty; + + @Override + public boolean isMarkedDirty() { + if (!dirty) return false; + dirty = false; + return true; + } + + @Override + public void markAsModified() { + dirty = true; + } + + @Override + public void resetMarkedDirty() { + dirty = false; + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareList.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareList.java new file mode 100644 index 000000000..1c5096146 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareList.java @@ -0,0 +1,166 @@ +package com.avaje.ebeaninternal.server.type; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +/** + * Modify aware wrapper of a list. + */ +public class ModifyAwareList implements List, ModifyAwareOwner { + + final List list; + + final ModifyAwareOwner owner; + + public ModifyAwareList(List list) { + this.list = list; + this.owner = new ModifyAwareFlag(); + } + + public ModifyAwareList(ModifyAwareOwner owner, List list) { + this.list = list; + this.owner = owner; + } + + @Override + public boolean isMarkedDirty() { + return owner.isMarkedDirty(); + } + + @Override + public void markAsModified() { + owner.markAsModified(); + } + + @Override + public void resetMarkedDirty() { + owner.resetMarkedDirty(); + } + + @Override + public int size() { + return list.size(); + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return list.contains(o); + } + + @Override + public Iterator iterator() { + return new ModifyAwareIterator(owner, list.iterator()); + } + + @Override + public Object[] toArray() { + return list.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return list.toArray(a); + } + + @Override + public boolean add(E e) { + owner.markAsModified(); + return list.add(e); + } + + @Override + public boolean remove(Object o) { + owner.markAsModified(); + return list.remove(o); + } + + @Override + public boolean containsAll(Collection> c) { + return list.containsAll(c); + } + + @Override + public boolean addAll(Collection extends E> c) { + owner.markAsModified(); + return list.addAll(c); + } + + @Override + public boolean addAll(int index, Collection extends E> c) { + owner.markAsModified(); + return list.addAll(index, c); + } + + @Override + public boolean removeAll(Collection> c) { + owner.markAsModified(); + return list.removeAll(c); + } + + @Override + public boolean retainAll(Collection> c) { + owner.markAsModified(); + return list.retainAll(c); + } + + @Override + public void clear() { + owner.markAsModified(); + list.clear(); + } + + @Override + public E get(int index) { + return list.get(index); + } + + @Override + public E set(int index, E element) { + owner.markAsModified(); + return list.set(index, element); + } + + @Override + public void add(int index, E element) { + owner.markAsModified(); + list.add(index, element); + } + + @Override + public E remove(int index) { + owner.markAsModified(); + return list.remove(index); + } + + @Override + public int indexOf(Object o) { + return list.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return list.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return new ModifyAwareListIterator(owner, list.listIterator()); + } + + @Override + public ListIterator listIterator(int index) { + return new ModifyAwareListIterator(owner, list.listIterator(index)); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return new ModifyAwareList(owner,list.subList(fromIndex, toIndex)); + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareListIterator.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareListIterator.java new file mode 100644 index 000000000..2b5bdc362 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareListIterator.java @@ -0,0 +1,66 @@ +package com.avaje.ebeaninternal.server.type; + +import java.util.ListIterator; + +/** + * Modify aware wrapper of a ListIterator. + */ +public class ModifyAwareListIterator implements ListIterator { + + final ModifyAwareOwner owner; + + final ListIterator iterator; + + public ModifyAwareListIterator(ModifyAwareOwner owner, ListIterator iterator) { + this.owner = owner; + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public E next() { + return iterator.next(); + } + + @Override + public boolean hasPrevious() { + return iterator.hasPrevious(); + } + + @Override + public E previous() { + return iterator.previous(); + } + + @Override + public int nextIndex() { + return iterator.nextIndex(); + } + + @Override + public int previousIndex() { + return iterator.previousIndex(); + } + + @Override + public void remove() { + owner.markAsModified(); + iterator.remove(); + } + + @Override + public void set(E e) { + owner.markAsModified(); + iterator.set(e); + } + + @Override + public void add(E e) { + owner.markAsModified(); + iterator.add(e); + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareMap.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareMap.java index 234c231bd..724dde7e7 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareMap.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareMap.java @@ -9,18 +9,21 @@ import java.util.Set; */ public class ModifyAwareMap implements Map, ModifyAwareOwner { - /** - * Dirty flag set when the map has been modified. - */ - private boolean dirty; + final ModifyAwareOwner owner; /** * The underlying map. */ - private Map map; + final Map map; public ModifyAwareMap(Map underyling) { this.map = underyling; + this.owner = new ModifyAwareFlag(); + } + + public ModifyAwareMap(ModifyAwareOwner owner, Map underyling) { + this.owner = owner; + this.map = underyling; } public String toString() { @@ -29,12 +32,17 @@ public class ModifyAwareMap implements Map, ModifyAwareOwner { @Override public boolean isMarkedDirty() { - return dirty; + return owner.isMarkedDirty(); } @Override public void markAsModified() { - dirty = true; + owner.markAsModified(); + } + + @Override + public void resetMarkedDirty() { + owner.resetMarkedDirty(); } @Override diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareOwner.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareOwner.java index 542278c9f..fe2d6f85a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareOwner.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareOwner.java @@ -7,6 +7,7 @@ public interface ModifyAwareOwner { /** * Return true if the value is considered dirty. + * Note that this resets the dirty status back to clean. */ boolean isMarkedDirty(); @@ -14,4 +15,10 @@ public interface ModifyAwareOwner { * Marks the object as modified. */ void markAsModified(); + + /** + * Reset the dirty state to clean. + */ + void resetMarkedDirty(); + } diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/RsetDataReader.java b/src/main/java/com/avaje/ebeaninternal/server/type/RsetDataReader.java index 851a45935..54965a6b7 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/RsetDataReader.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/RsetDataReader.java @@ -168,6 +168,15 @@ public class RsetDataReader implements DataReader { return readStringLob(reader); } + @Override + public Reader getClobReader() throws SQLException { + Clob clob = rset.getClob(pos()); + if (clob == null) { + return null; + } + return clob.getCharacterStream(); + } + public String getStringClob() throws SQLException { Clob clob = rset.getClob(pos()); @@ -212,6 +221,14 @@ public class RsetDataReader implements DataReader { return getBinaryLob(in); } + public InputStream getBlobInputStream() throws SQLException { + Blob blob = rset.getBlob(pos()); + if (blob == null) { + return null; + } + return blob.getBinaryStream(); + } + protected byte[] getBinaryLob(InputStream in) throws SQLException { try { diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMap.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMap.java new file mode 100644 index 000000000..3f80ce5c2 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMap.java @@ -0,0 +1,214 @@ +package com.avaje.ebeaninternal.server.type; + +import com.avaje.ebean.text.TextException; +import com.avaje.ebean.text.json.EJson; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Map; + +/** + * Type which maps Map to various DB types (Clob, Varchar, Blob) in JSON format. + */ +public abstract class ScalarTypeJsonMap extends ScalarTypeBase { + + public static class Clob extends ScalarTypeJsonMap { + + public Clob() { + super(Types.CLOB); + } + + @Override + public Map read(DataReader dataReader) throws SQLException { + + Reader reader = dataReader.getClobReader(); + if (reader == null) { + return null; + } + try { + Map map = parse(reader); + reader.close(); + return map; + } catch (IOException e) { + throw new SQLException("Error reading Clob stream from DB", e); + } + } + } + + public static class Varchar extends ScalarTypeJsonMap { + + public Varchar() { + super(Types.VARCHAR); + } + } + + public static class Blob extends ScalarTypeJsonMap { + public Blob() { + super(Types.BLOB); + } + + @Override + public Map read(DataReader dataReader) throws SQLException { + + InputStream is = dataReader.getBlobInputStream(); + if (is == null) { + return null; + } + try { + InputStreamReader reader = new InputStreamReader(is); + Map map = parse(reader); + reader.close(); + return map; + } catch (IOException e) { + throw new SQLException("Error reading Blob stream from DB", e); + } + } + + @Override + public void bind(DataBind b, Map value) throws SQLException { + + if (value == null) { + b.setNull(Types.BLOB); + } else { + String rawJson = formatValue(value); + InputStream stream = new ByteArrayInputStream(rawJson.getBytes(StandardCharsets.UTF_8)); + b.setBlob(stream); + } + } + } + + public ScalarTypeJsonMap(int jdbcType) { + super(Map.class, false, jdbcType); + } + + /** + * Map is 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 Map read(DataReader dataReader) throws SQLException { + + String rawJson = dataReader.getString(); + if (rawJson == null) { + return null; + } + return parse(rawJson); + } + + @Override + public void bind(DataBind b, Map value) throws SQLException { + + if (value == null) { + b.setNull(Types.VARCHAR); + } else { + String rawJson = formatValue(value); + b.setString(rawJson); + } + } + + @Override + public Object toJdbcType(Object value) { + return value; + } + + @Override + public Map toBeanType(Object value) { + return (Map) value; + } + + @Override + public String formatValue(Map v) { + try { + return EJson.write(v); + } catch (IOException e) { + throw new TextException(e); + } + } + + @Override + public Map parse(String value) { + try { + // return a modify aware map + return EJson.parseObject(value, true); + } catch (IOException e) { + throw new TextException(e); + } + } + + public Map parse(Reader reader) { + try { + // return a modify aware map + return EJson.parseObject(reader, true); + } catch (IOException e) { + throw new TextException(e); + } + } + + @Override + public Map convertFromMillis(long dateTime) { + throw new RuntimeException("Should never be called"); + } + + @Override + public boolean isDateTimeCapable() { + return false; + } + + @Override + public Map readData(DataInput dataInput) throws IOException { + if (!dataInput.readBoolean()) { + return null; + } else { + String json = dataInput.readUTF(); + return parse(json); + } + } + + @Override + public void writeData(DataOutput dataOutput, Map v) throws IOException { + if (v == null) { + dataOutput.writeBoolean(false); + } else { + dataOutput.writeBoolean(true); + String json = format(v); + dataOutput.writeUTF(json); + } + } + + @Override + public void jsonWrite(JsonGenerator ctx, String name, Map value) throws IOException { + // write the field name followed by the Map/JSON Object + if (value == null) { + ctx.writeNullField(name); + } else { + ctx.writeFieldName(name); + EJson.write(value, ctx); + } + } + + @Override + public Map jsonRead(JsonParser ctx, JsonToken event) throws IOException { + // at this point the BeanProperty has read the START_OBJECT token + // to check for a null value. Pass the START_OBJECT token through to + // the EJson parsing so that it knows the first token has been read + return EJson.parseObject(ctx, event); + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java new file mode 100644 index 000000000..388826545 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java @@ -0,0 +1,55 @@ +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; + +/** + * Support for the Postgres DB types JSON and JSONB. + */ +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) { + super(jdbcType); + this.postgresType = postgresType; + } + + @Override + public void bind(DataBind b, Map value) throws SQLException { + + String rawJson = (value == null) ? null : formatValue(value); + + PGobject pgo = new PGobject(); + pgo.setType(postgresType); + pgo.setValue(rawJson); + b.setObject(pgo); + } + + /** + * ScalarType mapping java Map type to Postgres JSON database type. + */ + public static class JSON extends ScalarTypeJsonMapPostgres { + + public JSON() { + super(DbType.JSON, POSTGRES_TYPE_JSON); + } + } + + /** + * ScalarType mapping java Map type to Postgres JSONB database type. + */ + public static class JSONB extends ScalarTypeJsonMapPostgres { + + public JSONB() { + super(DbType.JSONB, POSTGRES_TYPE_JSONB); + } + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java index e8866c327..d91def549 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java @@ -1,18 +1,18 @@ package com.avaje.ebeaninternal.server.type; +import com.avaje.ebean.config.dbplatform.DbType; +import com.avaje.ebean.text.TextException; +import com.avaje.ebean.text.json.EJson; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; + import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.sql.SQLException; import java.util.Map; -import com.avaje.ebean.config.dbplatform.PostgresPlatform; -import com.avaje.ebean.text.json.EJson; -import com.avaje.ebean.text.TextException; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; - /** * Postgres Hstore type which maps Map to a single 'HStore column' in the DB. */ @@ -21,10 +21,8 @@ public class ScalarTypePostgresHstore extends ScalarTypeBase { public static final String KEY = "hstore"; - public static final int HSTORE_TYPE = PostgresPlatform.TYPE_HSTORE; - public ScalarTypePostgresHstore() { - super(Map.class, false, HSTORE_TYPE); + super(Map.class, false, DbType.HSTORE); } @Override 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 efaf2b3b4..71cd5db12 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java @@ -58,7 +58,10 @@ public interface TypeManager { ScalarType> createEnumScalarType(Class> enumType); /** - * Find a scalarType using a custom type key. Used for Hstore and similar special types. + * Return the ScalarType used to handle JSON content. + * + * Note that type expected to be JsonNode or Map. + * */ - ScalarType> getScalarTypeFromKey(String specialTypeKey); + ScalarType> getJsonScalarType(Class> type, int dbType); } diff --git a/src/test/java/com/avaje/ebean/json/EJsonTests.java b/src/test/java/com/avaje/ebean/json/EJsonTests.java index 434db49eb..366831bf1 100644 --- a/src/test/java/com/avaje/ebean/json/EJsonTests.java +++ b/src/test/java/com/avaje/ebean/json/EJsonTests.java @@ -1,6 +1,7 @@ package com.avaje.ebean.json; import com.avaje.ebean.text.json.EJson; +import com.avaje.ebeaninternal.server.type.ModifyAwareMap; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import org.junit.Assert; @@ -203,4 +204,46 @@ public class EJsonTests { } + @Test + public void test_map_nested_modifyAware() throws IOException { + + String jsonInput = "{\"name\":\"rob\",\"age\":12,\"org\":{\"name\":\"superorg\",\"rating\":4},\"nums\":[1,2,3]}"; + ModifyAwareMap map = (ModifyAwareMap) EJson.parseObject(jsonInput, true); + + Assert.assertFalse(map.isMarkedDirty()); + Assert.assertEquals(4, map.size()); + + map.put("name", "jim"); + Assert.assertTrue(map.isMarkedDirty()); + + } + + @SuppressWarnings("unchecked") + @Test + public void test_map_nested_modifyAwareNestedList() throws IOException { + + String jsonInput = "{\"name\":\"rob\",\"age\":12,\"org\":{\"name\":\"superorg\",\"rating\":4},\"nums\":[1,2,3]}"; + ModifyAwareMap map = (ModifyAwareMap) EJson.parseObject(jsonInput, true); + Assert.assertFalse(map.isMarkedDirty()); + + List nums = (List) map.get("nums"); + nums.add(4); + Assert.assertTrue(map.isMarkedDirty()); + + } + + @SuppressWarnings("unchecked") + @Test + public void test_map_nested_modifyAwareNestedObject() throws IOException { + + String jsonInput = "{\"name\":\"rob\",\"age\":12,\"org\":{\"name\":\"superorg\",\"rating\":4},\"nums\":[1,2,3]}"; + ModifyAwareMap map = (ModifyAwareMap) EJson.parseObject(jsonInput, true); + Assert.assertFalse(map.isMarkedDirty()); + + Map org = (Map) map.get("org"); + org.put("extra","foo"); + Assert.assertTrue(map.isMarkedDirty()); + + } + } diff --git a/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareListTest.java b/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareListTest.java new file mode 100644 index 000000000..3c194c83c --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareListTest.java @@ -0,0 +1,228 @@ +package com.avaje.ebeaninternal.server.type; + +import org.junit.Test; + +import java.util.*; + +import static org.junit.Assert.*; + + +public class ModifyAwareListTest { + + private ModifyAwareList createList() { + ArrayList list = new ArrayList(); + list.addAll(Arrays.asList("A","B","C","D","E")); + return new ModifyAwareList(list); + } + private ModifyAwareList createEmptyList() { + return new ModifyAwareList(new ArrayList()); + } + + @Test + public void testSize() throws Exception { + + assertEquals(5, createList().size()); + } + + @Test + public void testIsEmpty() throws Exception { + + assertFalse(createList().isEmpty()); + assertTrue(createEmptyList().isEmpty()); + } + + @Test + public void testContains() throws Exception { + + assertTrue(createList().contains("B")); + assertFalse(createList().contains("Z")); + } + + @Test + public void testIterator() throws Exception { + + ModifyAwareList list = createList(); + Iterator iterator = list.iterator(); + assertTrue(iterator.hasNext()); + assertEquals("A", iterator.next()); + assertFalse(list.isMarkedDirty()); + + iterator.remove(); + assertTrue(list.isMarkedDirty()); + } + + @Test + public void testToArray() throws Exception { + + Object[] objects = createList().toArray(); + assertEquals(5, objects.length); + assertEquals("A", objects[0]); + assertEquals("E", objects[4]); + } + + @Test + public void testToArray1() throws Exception { + + String[] objects = createList().toArray(new String[5]); + assertEquals(5, objects.length); + assertEquals("A", objects[0]); + assertEquals("E", objects[4]); + } + + @Test + public void testAdd() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + list.add("F"); + assertTrue(list.isMarkedDirty()); + } + + @Test + public void testRemove() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + list.remove("A"); + assertTrue(list.isMarkedDirty()); + } + + @Test + public void testContainsAll() throws Exception { + + ModifyAwareList list = createList(); + + assertTrue(list.containsAll(Arrays.asList("A", "B"))); + assertFalse(list.containsAll(Arrays.asList("A", "B", "Z"))); + } + + @Test + public void testAddAll() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + assertTrue(list.addAll(Arrays.asList("F", "G"))); + assertTrue(list.isMarkedDirty()); + } + + + @Test + public void testRemoveAll() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + assertTrue(list.removeAll(Arrays.asList("A", "G"))); + assertTrue(list.isMarkedDirty()); + } + + @Test + public void testRetainAll() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + assertTrue(list.retainAll(Arrays.asList("A", "B"))); + assertTrue(list.isMarkedDirty()); + assertEquals(2, list.size()); + } + + @Test + public void testClear() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + list.clear(); + assertTrue(list.isMarkedDirty()); + assertEquals(0, list.size()); + + } + + @Test + public void testGet() throws Exception { + + ModifyAwareList list = createList(); + + assertEquals("A", list.get(0)); + assertEquals("B", list.get(1)); + assertEquals("E", list.get(4)); + } + + @Test + public void testSet() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + list.set(0, "Z"); + assertTrue(list.isMarkedDirty()); + assertEquals(5, list.size()); + } + + @Test + public void testIndexOf() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + assertEquals(2, list.indexOf("C")); + assertEquals(-1, list.indexOf("Z")); + assertFalse(list.isMarkedDirty()); + } + + @Test + public void testLastIndexOf() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + assertEquals(2, list.lastIndexOf("C")); + assertEquals(-1, list.lastIndexOf("Z")); + assertFalse(list.isMarkedDirty()); + } + + @Test + public void testListIterator() throws Exception { + + ModifyAwareList list = createList(); + ListIterator iterator = list.listIterator(); + assertTrue(iterator.hasNext()); + assertEquals("A", iterator.next()); + assertFalse(list.isMarkedDirty()); + + iterator.remove(); + assertTrue(list.isMarkedDirty()); + } + + @Test + public void testListIterator1() throws Exception { + + ModifyAwareList list = createList(); + ListIterator iterator = list.listIterator(2); + assertTrue(iterator.hasNext()); + assertEquals("C", iterator.next()); + assertFalse(list.isMarkedDirty()); + + iterator.remove(); + assertTrue(list.isMarkedDirty()); + + } + + @Test + public void testSubList() throws Exception { + + ModifyAwareList list = createList(); + List sub = list.subList(1, 3); + assertEquals("B", sub.get(0)); + assertEquals("C", sub.get(1)); + + assertFalse(list.isMarkedDirty()); + + sub.remove("C"); + assertTrue(list.isMarkedDirty()); + } +} \ No newline at end of file diff --git a/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareMapTest.java b/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareMapTest.java new file mode 100644 index 000000000..1f9a09b12 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareMapTest.java @@ -0,0 +1,180 @@ +package com.avaje.ebeaninternal.server.type; + +import org.junit.Test; + +import java.util.*; + +import static org.junit.Assert.*; + +public class ModifyAwareMapTest { + + private ModifyAwareMap createMap() { + LinkedHashMap map = new LinkedHashMap(); + map.put("A", "one"); + map.put("B", "two"); + map.put("C", "three"); + map.put("D", "four"); + map.put("E", "five"); + return new ModifyAwareMap(map); + } + + private ModifyAwareMap createEmptyMap() { + LinkedHashMap map = new LinkedHashMap(); + return new ModifyAwareMap(map); + } + + @Test + public void testToString() throws Exception { + + ModifyAwareMap map = createMap(); + assertEquals(map.map.toString(), map.toString()); + } + + @Test + public void testIsMarkedDirty() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + map.put("A", "change"); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testMarkAsModified() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + map.markAsModified(); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testSize() throws Exception { + + ModifyAwareMap map = createMap(); + assertEquals(5, map.size()); + } + + @Test + public void testIsEmpty() throws Exception { + + assertFalse(createMap().isEmpty()); + assertTrue(createEmptyMap().isEmpty()); + } + + @Test + public void testContainsKey() throws Exception { + + ModifyAwareMap map = createMap(); + assertTrue(map.containsKey("A")); + assertFalse(map.containsKey("Z")); + } + + @Test + public void testContainsValue() throws Exception { + ModifyAwareMap map = createMap(); + assertTrue(map.containsValue("one")); + assertFalse(map.containsValue("junk")); + } + + @Test + public void testGet() throws Exception { + + ModifyAwareMap map = createMap(); + + assertEquals("two", map.get("B")); + assertNull(map.get("Z")); + assertFalse(map.isMarkedDirty()); + } + + @Test + public void testPut() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + map.put("A", "mod"); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testRemove() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + map.remove("A"); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testPutAllWithEmpty() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + Map other = new HashMap(); + map.putAll(other); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testPutAll() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + Map other = new HashMap(); + other.put("A", "one"); + map.putAll(other); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testClear() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + map.clear(); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testKeySet() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + Set keys = map.keySet(); + assertEquals(map.size(), keys.size()); + assertTrue(keys.contains("A")); + assertFalse(map.isMarkedDirty()); + } + + @Test + public void testValues() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + Collection values = map.values(); + assertEquals(map.size(), values.size()); + assertTrue(values.contains("one")); + assertFalse(map.isMarkedDirty()); + } + + @Test + public void testEntrySet() throws Exception { + + ModifyAwareMap map = createMap(); + Set> entries = map.entrySet(); + + assertFalse(map.isMarkedDirty()); + + assertEquals(map.size(), entries.size()); + assertFalse(map.isMarkedDirty()); + } +} \ No newline at end of file diff --git a/src/test/java/com/avaje/tests/json/TestJsonMapBasic.java b/src/test/java/com/avaje/tests/json/TestJsonMapBasic.java new file mode 100644 index 000000000..4681860c5 --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonMapBasic.java @@ -0,0 +1,67 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.text.json.EJson; +import com.avaje.tests.model.json.EBasicJsonMap; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class TestJsonMapBasic extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + //String s1 = "{\"docId\":19,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + Map content = EJson.parseObject(s0); + + EBasicJsonMap bean = new EBasicJsonMap(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonMap bean1 = Ebean.find(EBasicJsonMap.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + assertEquals(bean.getContent().get("contentType"), bean1.getContent().get("contentType")); + assertEquals(18L, bean1.getContent().get("docId")); + + bean1.setName("just change name"); + Ebean.save(bean1); + + // content changes detected - dirty state so included in update + Map content1 = bean1.getContent(); + content1.put("additional", "newValue"); + content1.put("docId", 99L); + bean1.setName("two"); + Ebean.save(bean1); + + EBasicJsonMap bean2 = Ebean.find(EBasicJsonMap.class, bean.getId()); + + // name changed and docId changed + assertEquals("two", bean2.getName()); + assertEquals(99L, bean2.getContent().get("docId")); + assertEquals("newValue", bean2.getContent().get("additional")); + + content1.put("additional", "modValue"); + bean1.setName("three"); + bean1.setContent(content1); + Ebean.save(bean1); + + EBasicJsonMap bean3 = Ebean.find(EBasicJsonMap.class, bean.getId()); + + assertEquals("three", bean3.getName()); + assertEquals(99L, bean3.getContent().get("docId")); + assertEquals("modValue", bean3.getContent().get("additional")); + } +} diff --git a/src/test/java/com/avaje/tests/json/TestJsonMapBlob.java b/src/test/java/com/avaje/tests/json/TestJsonMapBlob.java new file mode 100644 index 000000000..6b081a5ea --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonMapBlob.java @@ -0,0 +1,40 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.text.json.EJson; +import com.avaje.tests.model.json.EBasicJsonMapBlob; +import com.avaje.tests.model.json.EBasicJsonMapVarchar; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class TestJsonMapBlob extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + Map content = EJson.parseObject(s0); + + EBasicJsonMapBlob bean = new EBasicJsonMapBlob(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonMapBlob bean1 = Ebean.find(EBasicJsonMapBlob.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + assertEquals(bean.getContent().get("contentType"), bean1.getContent().get("contentType")); + assertEquals(18L, bean1.getContent().get("docId")); + + bean1.setName("just change name"); + Ebean.save(bean1); + } +} diff --git a/src/test/java/com/avaje/tests/json/TestJsonMapJsonB.java b/src/test/java/com/avaje/tests/json/TestJsonMapJsonB.java new file mode 100644 index 000000000..f3b250cc6 --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonMapJsonB.java @@ -0,0 +1,40 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.text.json.EJson; +import com.avaje.tests.model.json.EBasicJsonMapJsonB; +import com.avaje.tests.model.json.EBasicJsonMapJsonB; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class TestJsonMapJsonB extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + Map content = EJson.parseObject(s0); + + EBasicJsonMapJsonB bean = new EBasicJsonMapJsonB(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonMapJsonB bean1 = Ebean.find(EBasicJsonMapJsonB.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + assertEquals(bean.getContent().get("contentType"), bean1.getContent().get("contentType")); + assertEquals(18L, bean1.getContent().get("docId")); + + bean1.setName("just change name"); + Ebean.save(bean1); + } +} diff --git a/src/test/java/com/avaje/tests/json/TestJsonMapVarchar.java b/src/test/java/com/avaje/tests/json/TestJsonMapVarchar.java new file mode 100644 index 000000000..b8ae74021 --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonMapVarchar.java @@ -0,0 +1,40 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.text.json.EJson; +import com.avaje.tests.model.json.EBasicJsonMap; +import com.avaje.tests.model.json.EBasicJsonMapVarchar; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class TestJsonMapVarchar extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + Map content = EJson.parseObject(s0); + + EBasicJsonMapVarchar bean = new EBasicJsonMapVarchar(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonMapVarchar bean1 = Ebean.find(EBasicJsonMapVarchar.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + assertEquals(bean.getContent().get("contentType"), bean1.getContent().get("contentType")); + assertEquals(18L, bean1.getContent().get("docId")); + + bean1.setName("just change name"); + Ebean.save(bean1); + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonMap.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonMap.java new file mode 100644 index 000000000..6a68998e7 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonMap.java @@ -0,0 +1,55 @@ +package com.avaje.tests.model.json; + +import com.avaje.ebean.annotation.DbJson; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; +import java.util.Map; + +@Entity +public class EBasicJsonMap { + + @Id + Long id; + + @Version + Long version; + + String name; + + @DbJson + Map content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getContent() { + return content; + } + + public void setContent(Map content) { + this.content = content; + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonMapBlob.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapBlob.java new file mode 100644 index 000000000..b01873056 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapBlob.java @@ -0,0 +1,56 @@ +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.Map; + +@Entity +public class EBasicJsonMapBlob { + + @Id + Long id; + + @Version + Long version; + + String name; + + @DbJson(storage = DbJsonType.BLOB) + Map content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getContent() { + return content; + } + + public void setContent(Map content) { + this.content = content; + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonMapJsonB.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapJsonB.java new file mode 100644 index 000000000..c8ece036f --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapJsonB.java @@ -0,0 +1,55 @@ +package com.avaje.tests.model.json; + +import com.avaje.ebean.annotation.DbJsonB; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; +import java.util.Map; + +@Entity +public class EBasicJsonMapJsonB { + + @Id + Long id; + + @Version + Long version; + + String name; + + @DbJsonB + Map content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getContent() { + return content; + } + + public void setContent(Map content) { + this.content = content; + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonMapVarchar.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapVarchar.java new file mode 100644 index 000000000..648574201 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapVarchar.java @@ -0,0 +1,56 @@ +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.Map; + +@Entity +public class EBasicJsonMapVarchar { + + @Id + Long id; + + @Version + Long version; + + String name; + + @DbJson(storage = DbJsonType.VARCHAR)//, length = 2200) + Map content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getContent() { + return content; + } + + public void setContent(Map content) { + this.content = content; + } +}
+ * By default the content will be stored in a DB Clob except on Postgres where DB JSON type is used. + *
{@code + * + * // Store as JSON on Postgres or Clob on other databases + * @DbJson + * Map content; + * + * }
{@code + * + * // Store as JSONB on Postgres or Clob on other databases + * @DbJson(storage = DbJsonType.JSONB) + * Map content; + * + * }
+ * The content will be stored on Postgres using it's JSONB type and as Clob for other databases. + *
+ * This is equivalent to using @DbJson(storage = DbJsonType.JSONB) + *
@DbJson(storage = DbJsonType.JSONB)
{@code + * + * // Store as JSONB on Postgres or Clob on other databases + * @DbJsonB + * Map content; + * + * }
@@ -65,10 +52,16 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { private final ConcurrentHashMap> nativeMap; - private final ConcurrentHashMap> customTypeMap; - private final DefaultTypeFactory extraTypeFactory; + private final ScalarTypeJsonMap JSON_MAP_CLOB = new ScalarTypeJsonMap.Clob(); + + private final ScalarTypeJsonMap jsonMapClob = JSON_MAP_CLOB; + private final ScalarTypeJsonMap jsonMapBlob = new ScalarTypeJsonMap.Blob(); + private final ScalarTypeJsonMap jsonMapVarchar = new ScalarTypeJsonMap.Varchar(); + private final ScalarTypeJsonMap jsonMapJson; + private final ScalarTypeJsonMap jsonMapJsonb; + private final ScalarTypeFile fileType = new ScalarTypeFile(); private final ScalarType> charType = new ScalarTypeChar(); @@ -134,9 +127,6 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { */ public DefaultTypeManager(ServerConfig config, BootupClasses bootupClasses) { - int clobType = config.getDatabasePlatform().getClobDbType(); - int blobType = config.getDatabasePlatform().getBlobDbType(); - this.jsonDateTime = config.getJsonDateTime(); this.checkImmutable = new CheckImmutable(this); this.reflectScalarBuilder = new ReflectionBasedTypeBuilder(this); @@ -144,18 +134,24 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { this.compoundTypeMap = new ConcurrentHashMap, CtCompoundType>>(); this.typeMap = new ConcurrentHashMap, ScalarType>>(); this.nativeMap = new ConcurrentHashMap>(); - this.customTypeMap = new ConcurrentHashMap>(); - - this.customTypeMap.put(ScalarTypePostgresHstore.KEY, new ScalarTypePostgresHstore()); this.objectMapperPresent = ClassUtil.isPresent("com.fasterxml.jackson.databind.ObjectMapper", this.getClass()); this.extraTypeFactory = new DefaultTypeFactory(config); - initialiseStandard(jsonDateTime, clobType, blobType, config.isUuidStoreAsBinary()); + initialiseStandard(jsonDateTime, config); initialiseJavaTimeTypes(jsonDateTime, config); initialiseJodaTypes(jsonDateTime); + if (isPostgres(config.getDatabasePlatform())) { + // Postgres has special DB types for JSON/JSONB + this.jsonMapJson = new ScalarTypeJsonMapPostgres.JSON(); + this.jsonMapJsonb = new ScalarTypeJsonMapPostgres.JSONB(); + } else { + this.jsonMapJson = JSON_MAP_CLOB; + this.jsonMapJsonb = JSON_MAP_CLOB; + } + if (bootupClasses != null) { initialiseCustomScalarTypes(jsonDateTime, bootupClasses, config); initialiseScalarConverters(bootupClasses); @@ -163,12 +159,8 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { } } - /** - * Lookup a special or custom scalar type by key. - */ - @Override - public ScalarType> getScalarTypeFromKey(String specialTypeKey) { - return customTypeMap.get(specialTypeKey); + private boolean isPostgres(DatabasePlatform databasePlatform) { + return databasePlatform.getName().toLowerCase().startsWith("postgre"); } public boolean isKnownImmutable(Class> cls) { @@ -295,6 +287,24 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { return reader; } + @Override + public ScalarType> getJsonScalarType(Class> type, int dbType) { + + if (type.equals(Map.class)) { + switch (dbType) { + case Types.VARCHAR : return jsonMapVarchar; + case Types.BLOB: return jsonMapBlob; + case Types.CLOB : return jsonMapClob; + case DbType.JSONB: return jsonMapJsonb; + case DbType.JSON: return jsonMapJson; + default: + return jsonMapJson; + } + } + + throw new IllegalArgumentException("Type [" + type + "] unsupported for @DbJson mapping"); + } + /** * Return a ScalarType for a given class. * @@ -370,8 +380,8 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { return value; } + @SuppressWarnings("ResultOfMethodCallIgnored") private boolean isIntegerType(String s) { - try { Integer.parseInt(s); return true; @@ -685,7 +695,14 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { * Register all the standard types supported. This is the standard JDBC types * plus some other common types such as java.util.Date and java.util.Calendar. */ - protected void initialiseStandard(JsonConfig.DateTime mode, int platformClobType, int platformBlobType, boolean binaryUUID) { + protected void initialiseStandard(JsonConfig.DateTime mode, ServerConfig config) { + + boolean binaryUUID = config.isUuidStoreAsBinary(); + DatabasePlatform databasePlatform = config.getDatabasePlatform(); + int platformClobType = databasePlatform.getClobDbType(); + int platformBlobType = databasePlatform.getBlobDbType(); + + nativeMap.put(DbType.HSTORE, new ScalarTypePostgresHstore()); ScalarType> utilDateType = extraTypeFactory.createUtilDate(mode); typeMap.put(java.util.Date.class, utilDateType); diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareFlag.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareFlag.java new file mode 100644 index 000000000..31081879f --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareFlag.java @@ -0,0 +1,26 @@ +package com.avaje.ebeaninternal.server.type; + +/** + * Detects when content has been modified and as such needs to be persisted (included in an update). + */ +public class ModifyAwareFlag implements ModifyAwareOwner { + + boolean dirty; + + @Override + public boolean isMarkedDirty() { + if (!dirty) return false; + dirty = false; + return true; + } + + @Override + public void markAsModified() { + dirty = true; + } + + @Override + public void resetMarkedDirty() { + dirty = false; + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareList.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareList.java new file mode 100644 index 000000000..1c5096146 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareList.java @@ -0,0 +1,166 @@ +package com.avaje.ebeaninternal.server.type; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +/** + * Modify aware wrapper of a list. + */ +public class ModifyAwareList implements List, ModifyAwareOwner { + + final List list; + + final ModifyAwareOwner owner; + + public ModifyAwareList(List list) { + this.list = list; + this.owner = new ModifyAwareFlag(); + } + + public ModifyAwareList(ModifyAwareOwner owner, List list) { + this.list = list; + this.owner = owner; + } + + @Override + public boolean isMarkedDirty() { + return owner.isMarkedDirty(); + } + + @Override + public void markAsModified() { + owner.markAsModified(); + } + + @Override + public void resetMarkedDirty() { + owner.resetMarkedDirty(); + } + + @Override + public int size() { + return list.size(); + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return list.contains(o); + } + + @Override + public Iterator iterator() { + return new ModifyAwareIterator(owner, list.iterator()); + } + + @Override + public Object[] toArray() { + return list.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return list.toArray(a); + } + + @Override + public boolean add(E e) { + owner.markAsModified(); + return list.add(e); + } + + @Override + public boolean remove(Object o) { + owner.markAsModified(); + return list.remove(o); + } + + @Override + public boolean containsAll(Collection> c) { + return list.containsAll(c); + } + + @Override + public boolean addAll(Collection extends E> c) { + owner.markAsModified(); + return list.addAll(c); + } + + @Override + public boolean addAll(int index, Collection extends E> c) { + owner.markAsModified(); + return list.addAll(index, c); + } + + @Override + public boolean removeAll(Collection> c) { + owner.markAsModified(); + return list.removeAll(c); + } + + @Override + public boolean retainAll(Collection> c) { + owner.markAsModified(); + return list.retainAll(c); + } + + @Override + public void clear() { + owner.markAsModified(); + list.clear(); + } + + @Override + public E get(int index) { + return list.get(index); + } + + @Override + public E set(int index, E element) { + owner.markAsModified(); + return list.set(index, element); + } + + @Override + public void add(int index, E element) { + owner.markAsModified(); + list.add(index, element); + } + + @Override + public E remove(int index) { + owner.markAsModified(); + return list.remove(index); + } + + @Override + public int indexOf(Object o) { + return list.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return list.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return new ModifyAwareListIterator(owner, list.listIterator()); + } + + @Override + public ListIterator listIterator(int index) { + return new ModifyAwareListIterator(owner, list.listIterator(index)); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return new ModifyAwareList(owner,list.subList(fromIndex, toIndex)); + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareListIterator.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareListIterator.java new file mode 100644 index 000000000..2b5bdc362 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareListIterator.java @@ -0,0 +1,66 @@ +package com.avaje.ebeaninternal.server.type; + +import java.util.ListIterator; + +/** + * Modify aware wrapper of a ListIterator. + */ +public class ModifyAwareListIterator implements ListIterator { + + final ModifyAwareOwner owner; + + final ListIterator iterator; + + public ModifyAwareListIterator(ModifyAwareOwner owner, ListIterator iterator) { + this.owner = owner; + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public E next() { + return iterator.next(); + } + + @Override + public boolean hasPrevious() { + return iterator.hasPrevious(); + } + + @Override + public E previous() { + return iterator.previous(); + } + + @Override + public int nextIndex() { + return iterator.nextIndex(); + } + + @Override + public int previousIndex() { + return iterator.previousIndex(); + } + + @Override + public void remove() { + owner.markAsModified(); + iterator.remove(); + } + + @Override + public void set(E e) { + owner.markAsModified(); + iterator.set(e); + } + + @Override + public void add(E e) { + owner.markAsModified(); + iterator.add(e); + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareMap.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareMap.java index 234c231bd..724dde7e7 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareMap.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareMap.java @@ -9,18 +9,21 @@ import java.util.Set; */ public class ModifyAwareMap implements Map, ModifyAwareOwner { - /** - * Dirty flag set when the map has been modified. - */ - private boolean dirty; + final ModifyAwareOwner owner; /** * The underlying map. */ - private Map map; + final Map map; public ModifyAwareMap(Map underyling) { this.map = underyling; + this.owner = new ModifyAwareFlag(); + } + + public ModifyAwareMap(ModifyAwareOwner owner, Map underyling) { + this.owner = owner; + this.map = underyling; } public String toString() { @@ -29,12 +32,17 @@ public class ModifyAwareMap implements Map, ModifyAwareOwner { @Override public boolean isMarkedDirty() { - return dirty; + return owner.isMarkedDirty(); } @Override public void markAsModified() { - dirty = true; + owner.markAsModified(); + } + + @Override + public void resetMarkedDirty() { + owner.resetMarkedDirty(); } @Override diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareOwner.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareOwner.java index 542278c9f..fe2d6f85a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareOwner.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareOwner.java @@ -7,6 +7,7 @@ public interface ModifyAwareOwner { /** * Return true if the value is considered dirty. + * Note that this resets the dirty status back to clean. */ boolean isMarkedDirty(); @@ -14,4 +15,10 @@ public interface ModifyAwareOwner { * Marks the object as modified. */ void markAsModified(); + + /** + * Reset the dirty state to clean. + */ + void resetMarkedDirty(); + } diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/RsetDataReader.java b/src/main/java/com/avaje/ebeaninternal/server/type/RsetDataReader.java index 851a45935..54965a6b7 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/RsetDataReader.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/RsetDataReader.java @@ -168,6 +168,15 @@ public class RsetDataReader implements DataReader { return readStringLob(reader); } + @Override + public Reader getClobReader() throws SQLException { + Clob clob = rset.getClob(pos()); + if (clob == null) { + return null; + } + return clob.getCharacterStream(); + } + public String getStringClob() throws SQLException { Clob clob = rset.getClob(pos()); @@ -212,6 +221,14 @@ public class RsetDataReader implements DataReader { return getBinaryLob(in); } + public InputStream getBlobInputStream() throws SQLException { + Blob blob = rset.getBlob(pos()); + if (blob == null) { + return null; + } + return blob.getBinaryStream(); + } + protected byte[] getBinaryLob(InputStream in) throws SQLException { try { diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMap.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMap.java new file mode 100644 index 000000000..3f80ce5c2 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMap.java @@ -0,0 +1,214 @@ +package com.avaje.ebeaninternal.server.type; + +import com.avaje.ebean.text.TextException; +import com.avaje.ebean.text.json.EJson; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Map; + +/** + * Type which maps Map to various DB types (Clob, Varchar, Blob) in JSON format. + */ +public abstract class ScalarTypeJsonMap extends ScalarTypeBase { + + public static class Clob extends ScalarTypeJsonMap { + + public Clob() { + super(Types.CLOB); + } + + @Override + public Map read(DataReader dataReader) throws SQLException { + + Reader reader = dataReader.getClobReader(); + if (reader == null) { + return null; + } + try { + Map map = parse(reader); + reader.close(); + return map; + } catch (IOException e) { + throw new SQLException("Error reading Clob stream from DB", e); + } + } + } + + public static class Varchar extends ScalarTypeJsonMap { + + public Varchar() { + super(Types.VARCHAR); + } + } + + public static class Blob extends ScalarTypeJsonMap { + public Blob() { + super(Types.BLOB); + } + + @Override + public Map read(DataReader dataReader) throws SQLException { + + InputStream is = dataReader.getBlobInputStream(); + if (is == null) { + return null; + } + try { + InputStreamReader reader = new InputStreamReader(is); + Map map = parse(reader); + reader.close(); + return map; + } catch (IOException e) { + throw new SQLException("Error reading Blob stream from DB", e); + } + } + + @Override + public void bind(DataBind b, Map value) throws SQLException { + + if (value == null) { + b.setNull(Types.BLOB); + } else { + String rawJson = formatValue(value); + InputStream stream = new ByteArrayInputStream(rawJson.getBytes(StandardCharsets.UTF_8)); + b.setBlob(stream); + } + } + } + + public ScalarTypeJsonMap(int jdbcType) { + super(Map.class, false, jdbcType); + } + + /** + * Map is 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 Map read(DataReader dataReader) throws SQLException { + + String rawJson = dataReader.getString(); + if (rawJson == null) { + return null; + } + return parse(rawJson); + } + + @Override + public void bind(DataBind b, Map value) throws SQLException { + + if (value == null) { + b.setNull(Types.VARCHAR); + } else { + String rawJson = formatValue(value); + b.setString(rawJson); + } + } + + @Override + public Object toJdbcType(Object value) { + return value; + } + + @Override + public Map toBeanType(Object value) { + return (Map) value; + } + + @Override + public String formatValue(Map v) { + try { + return EJson.write(v); + } catch (IOException e) { + throw new TextException(e); + } + } + + @Override + public Map parse(String value) { + try { + // return a modify aware map + return EJson.parseObject(value, true); + } catch (IOException e) { + throw new TextException(e); + } + } + + public Map parse(Reader reader) { + try { + // return a modify aware map + return EJson.parseObject(reader, true); + } catch (IOException e) { + throw new TextException(e); + } + } + + @Override + public Map convertFromMillis(long dateTime) { + throw new RuntimeException("Should never be called"); + } + + @Override + public boolean isDateTimeCapable() { + return false; + } + + @Override + public Map readData(DataInput dataInput) throws IOException { + if (!dataInput.readBoolean()) { + return null; + } else { + String json = dataInput.readUTF(); + return parse(json); + } + } + + @Override + public void writeData(DataOutput dataOutput, Map v) throws IOException { + if (v == null) { + dataOutput.writeBoolean(false); + } else { + dataOutput.writeBoolean(true); + String json = format(v); + dataOutput.writeUTF(json); + } + } + + @Override + public void jsonWrite(JsonGenerator ctx, String name, Map value) throws IOException { + // write the field name followed by the Map/JSON Object + if (value == null) { + ctx.writeNullField(name); + } else { + ctx.writeFieldName(name); + EJson.write(value, ctx); + } + } + + @Override + public Map jsonRead(JsonParser ctx, JsonToken event) throws IOException { + // at this point the BeanProperty has read the START_OBJECT token + // to check for a null value. Pass the START_OBJECT token through to + // the EJson parsing so that it knows the first token has been read + return EJson.parseObject(ctx, event); + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java new file mode 100644 index 000000000..388826545 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java @@ -0,0 +1,55 @@ +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; + +/** + * Support for the Postgres DB types JSON and JSONB. + */ +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) { + super(jdbcType); + this.postgresType = postgresType; + } + + @Override + public void bind(DataBind b, Map value) throws SQLException { + + String rawJson = (value == null) ? null : formatValue(value); + + PGobject pgo = new PGobject(); + pgo.setType(postgresType); + pgo.setValue(rawJson); + b.setObject(pgo); + } + + /** + * ScalarType mapping java Map type to Postgres JSON database type. + */ + public static class JSON extends ScalarTypeJsonMapPostgres { + + public JSON() { + super(DbType.JSON, POSTGRES_TYPE_JSON); + } + } + + /** + * ScalarType mapping java Map type to Postgres JSONB database type. + */ + public static class JSONB extends ScalarTypeJsonMapPostgres { + + public JSONB() { + super(DbType.JSONB, POSTGRES_TYPE_JSONB); + } + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java index e8866c327..d91def549 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java @@ -1,18 +1,18 @@ package com.avaje.ebeaninternal.server.type; +import com.avaje.ebean.config.dbplatform.DbType; +import com.avaje.ebean.text.TextException; +import com.avaje.ebean.text.json.EJson; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; + import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.sql.SQLException; import java.util.Map; -import com.avaje.ebean.config.dbplatform.PostgresPlatform; -import com.avaje.ebean.text.json.EJson; -import com.avaje.ebean.text.TextException; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; - /** * Postgres Hstore type which maps Map to a single 'HStore column' in the DB. */ @@ -21,10 +21,8 @@ public class ScalarTypePostgresHstore extends ScalarTypeBase { public static final String KEY = "hstore"; - public static final int HSTORE_TYPE = PostgresPlatform.TYPE_HSTORE; - public ScalarTypePostgresHstore() { - super(Map.class, false, HSTORE_TYPE); + super(Map.class, false, DbType.HSTORE); } @Override 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 efaf2b3b4..71cd5db12 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java @@ -58,7 +58,10 @@ public interface TypeManager { ScalarType> createEnumScalarType(Class> enumType); /** - * Find a scalarType using a custom type key. Used for Hstore and similar special types. + * Return the ScalarType used to handle JSON content. + * + * Note that type expected to be JsonNode or Map. + * */ - ScalarType> getScalarTypeFromKey(String specialTypeKey); + ScalarType> getJsonScalarType(Class> type, int dbType); } diff --git a/src/test/java/com/avaje/ebean/json/EJsonTests.java b/src/test/java/com/avaje/ebean/json/EJsonTests.java index 434db49eb..366831bf1 100644 --- a/src/test/java/com/avaje/ebean/json/EJsonTests.java +++ b/src/test/java/com/avaje/ebean/json/EJsonTests.java @@ -1,6 +1,7 @@ package com.avaje.ebean.json; import com.avaje.ebean.text.json.EJson; +import com.avaje.ebeaninternal.server.type.ModifyAwareMap; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import org.junit.Assert; @@ -203,4 +204,46 @@ public class EJsonTests { } + @Test + public void test_map_nested_modifyAware() throws IOException { + + String jsonInput = "{\"name\":\"rob\",\"age\":12,\"org\":{\"name\":\"superorg\",\"rating\":4},\"nums\":[1,2,3]}"; + ModifyAwareMap map = (ModifyAwareMap) EJson.parseObject(jsonInput, true); + + Assert.assertFalse(map.isMarkedDirty()); + Assert.assertEquals(4, map.size()); + + map.put("name", "jim"); + Assert.assertTrue(map.isMarkedDirty()); + + } + + @SuppressWarnings("unchecked") + @Test + public void test_map_nested_modifyAwareNestedList() throws IOException { + + String jsonInput = "{\"name\":\"rob\",\"age\":12,\"org\":{\"name\":\"superorg\",\"rating\":4},\"nums\":[1,2,3]}"; + ModifyAwareMap map = (ModifyAwareMap) EJson.parseObject(jsonInput, true); + Assert.assertFalse(map.isMarkedDirty()); + + List nums = (List) map.get("nums"); + nums.add(4); + Assert.assertTrue(map.isMarkedDirty()); + + } + + @SuppressWarnings("unchecked") + @Test + public void test_map_nested_modifyAwareNestedObject() throws IOException { + + String jsonInput = "{\"name\":\"rob\",\"age\":12,\"org\":{\"name\":\"superorg\",\"rating\":4},\"nums\":[1,2,3]}"; + ModifyAwareMap map = (ModifyAwareMap) EJson.parseObject(jsonInput, true); + Assert.assertFalse(map.isMarkedDirty()); + + Map org = (Map) map.get("org"); + org.put("extra","foo"); + Assert.assertTrue(map.isMarkedDirty()); + + } + } diff --git a/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareListTest.java b/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareListTest.java new file mode 100644 index 000000000..3c194c83c --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareListTest.java @@ -0,0 +1,228 @@ +package com.avaje.ebeaninternal.server.type; + +import org.junit.Test; + +import java.util.*; + +import static org.junit.Assert.*; + + +public class ModifyAwareListTest { + + private ModifyAwareList createList() { + ArrayList list = new ArrayList(); + list.addAll(Arrays.asList("A","B","C","D","E")); + return new ModifyAwareList(list); + } + private ModifyAwareList createEmptyList() { + return new ModifyAwareList(new ArrayList()); + } + + @Test + public void testSize() throws Exception { + + assertEquals(5, createList().size()); + } + + @Test + public void testIsEmpty() throws Exception { + + assertFalse(createList().isEmpty()); + assertTrue(createEmptyList().isEmpty()); + } + + @Test + public void testContains() throws Exception { + + assertTrue(createList().contains("B")); + assertFalse(createList().contains("Z")); + } + + @Test + public void testIterator() throws Exception { + + ModifyAwareList list = createList(); + Iterator iterator = list.iterator(); + assertTrue(iterator.hasNext()); + assertEquals("A", iterator.next()); + assertFalse(list.isMarkedDirty()); + + iterator.remove(); + assertTrue(list.isMarkedDirty()); + } + + @Test + public void testToArray() throws Exception { + + Object[] objects = createList().toArray(); + assertEquals(5, objects.length); + assertEquals("A", objects[0]); + assertEquals("E", objects[4]); + } + + @Test + public void testToArray1() throws Exception { + + String[] objects = createList().toArray(new String[5]); + assertEquals(5, objects.length); + assertEquals("A", objects[0]); + assertEquals("E", objects[4]); + } + + @Test + public void testAdd() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + list.add("F"); + assertTrue(list.isMarkedDirty()); + } + + @Test + public void testRemove() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + list.remove("A"); + assertTrue(list.isMarkedDirty()); + } + + @Test + public void testContainsAll() throws Exception { + + ModifyAwareList list = createList(); + + assertTrue(list.containsAll(Arrays.asList("A", "B"))); + assertFalse(list.containsAll(Arrays.asList("A", "B", "Z"))); + } + + @Test + public void testAddAll() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + assertTrue(list.addAll(Arrays.asList("F", "G"))); + assertTrue(list.isMarkedDirty()); + } + + + @Test + public void testRemoveAll() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + assertTrue(list.removeAll(Arrays.asList("A", "G"))); + assertTrue(list.isMarkedDirty()); + } + + @Test + public void testRetainAll() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + assertTrue(list.retainAll(Arrays.asList("A", "B"))); + assertTrue(list.isMarkedDirty()); + assertEquals(2, list.size()); + } + + @Test + public void testClear() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + list.clear(); + assertTrue(list.isMarkedDirty()); + assertEquals(0, list.size()); + + } + + @Test + public void testGet() throws Exception { + + ModifyAwareList list = createList(); + + assertEquals("A", list.get(0)); + assertEquals("B", list.get(1)); + assertEquals("E", list.get(4)); + } + + @Test + public void testSet() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + list.set(0, "Z"); + assertTrue(list.isMarkedDirty()); + assertEquals(5, list.size()); + } + + @Test + public void testIndexOf() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + assertEquals(2, list.indexOf("C")); + assertEquals(-1, list.indexOf("Z")); + assertFalse(list.isMarkedDirty()); + } + + @Test + public void testLastIndexOf() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + assertEquals(2, list.lastIndexOf("C")); + assertEquals(-1, list.lastIndexOf("Z")); + assertFalse(list.isMarkedDirty()); + } + + @Test + public void testListIterator() throws Exception { + + ModifyAwareList list = createList(); + ListIterator iterator = list.listIterator(); + assertTrue(iterator.hasNext()); + assertEquals("A", iterator.next()); + assertFalse(list.isMarkedDirty()); + + iterator.remove(); + assertTrue(list.isMarkedDirty()); + } + + @Test + public void testListIterator1() throws Exception { + + ModifyAwareList list = createList(); + ListIterator iterator = list.listIterator(2); + assertTrue(iterator.hasNext()); + assertEquals("C", iterator.next()); + assertFalse(list.isMarkedDirty()); + + iterator.remove(); + assertTrue(list.isMarkedDirty()); + + } + + @Test + public void testSubList() throws Exception { + + ModifyAwareList list = createList(); + List sub = list.subList(1, 3); + assertEquals("B", sub.get(0)); + assertEquals("C", sub.get(1)); + + assertFalse(list.isMarkedDirty()); + + sub.remove("C"); + assertTrue(list.isMarkedDirty()); + } +} \ No newline at end of file diff --git a/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareMapTest.java b/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareMapTest.java new file mode 100644 index 000000000..1f9a09b12 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareMapTest.java @@ -0,0 +1,180 @@ +package com.avaje.ebeaninternal.server.type; + +import org.junit.Test; + +import java.util.*; + +import static org.junit.Assert.*; + +public class ModifyAwareMapTest { + + private ModifyAwareMap createMap() { + LinkedHashMap map = new LinkedHashMap(); + map.put("A", "one"); + map.put("B", "two"); + map.put("C", "three"); + map.put("D", "four"); + map.put("E", "five"); + return new ModifyAwareMap(map); + } + + private ModifyAwareMap createEmptyMap() { + LinkedHashMap map = new LinkedHashMap(); + return new ModifyAwareMap(map); + } + + @Test + public void testToString() throws Exception { + + ModifyAwareMap map = createMap(); + assertEquals(map.map.toString(), map.toString()); + } + + @Test + public void testIsMarkedDirty() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + map.put("A", "change"); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testMarkAsModified() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + map.markAsModified(); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testSize() throws Exception { + + ModifyAwareMap map = createMap(); + assertEquals(5, map.size()); + } + + @Test + public void testIsEmpty() throws Exception { + + assertFalse(createMap().isEmpty()); + assertTrue(createEmptyMap().isEmpty()); + } + + @Test + public void testContainsKey() throws Exception { + + ModifyAwareMap map = createMap(); + assertTrue(map.containsKey("A")); + assertFalse(map.containsKey("Z")); + } + + @Test + public void testContainsValue() throws Exception { + ModifyAwareMap map = createMap(); + assertTrue(map.containsValue("one")); + assertFalse(map.containsValue("junk")); + } + + @Test + public void testGet() throws Exception { + + ModifyAwareMap map = createMap(); + + assertEquals("two", map.get("B")); + assertNull(map.get("Z")); + assertFalse(map.isMarkedDirty()); + } + + @Test + public void testPut() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + map.put("A", "mod"); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testRemove() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + map.remove("A"); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testPutAllWithEmpty() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + Map other = new HashMap(); + map.putAll(other); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testPutAll() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + Map other = new HashMap(); + other.put("A", "one"); + map.putAll(other); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testClear() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + map.clear(); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testKeySet() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + Set keys = map.keySet(); + assertEquals(map.size(), keys.size()); + assertTrue(keys.contains("A")); + assertFalse(map.isMarkedDirty()); + } + + @Test + public void testValues() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + Collection values = map.values(); + assertEquals(map.size(), values.size()); + assertTrue(values.contains("one")); + assertFalse(map.isMarkedDirty()); + } + + @Test + public void testEntrySet() throws Exception { + + ModifyAwareMap map = createMap(); + Set> entries = map.entrySet(); + + assertFalse(map.isMarkedDirty()); + + assertEquals(map.size(), entries.size()); + assertFalse(map.isMarkedDirty()); + } +} \ No newline at end of file diff --git a/src/test/java/com/avaje/tests/json/TestJsonMapBasic.java b/src/test/java/com/avaje/tests/json/TestJsonMapBasic.java new file mode 100644 index 000000000..4681860c5 --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonMapBasic.java @@ -0,0 +1,67 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.text.json.EJson; +import com.avaje.tests.model.json.EBasicJsonMap; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class TestJsonMapBasic extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + //String s1 = "{\"docId\":19,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + Map content = EJson.parseObject(s0); + + EBasicJsonMap bean = new EBasicJsonMap(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonMap bean1 = Ebean.find(EBasicJsonMap.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + assertEquals(bean.getContent().get("contentType"), bean1.getContent().get("contentType")); + assertEquals(18L, bean1.getContent().get("docId")); + + bean1.setName("just change name"); + Ebean.save(bean1); + + // content changes detected - dirty state so included in update + Map content1 = bean1.getContent(); + content1.put("additional", "newValue"); + content1.put("docId", 99L); + bean1.setName("two"); + Ebean.save(bean1); + + EBasicJsonMap bean2 = Ebean.find(EBasicJsonMap.class, bean.getId()); + + // name changed and docId changed + assertEquals("two", bean2.getName()); + assertEquals(99L, bean2.getContent().get("docId")); + assertEquals("newValue", bean2.getContent().get("additional")); + + content1.put("additional", "modValue"); + bean1.setName("three"); + bean1.setContent(content1); + Ebean.save(bean1); + + EBasicJsonMap bean3 = Ebean.find(EBasicJsonMap.class, bean.getId()); + + assertEquals("three", bean3.getName()); + assertEquals(99L, bean3.getContent().get("docId")); + assertEquals("modValue", bean3.getContent().get("additional")); + } +} diff --git a/src/test/java/com/avaje/tests/json/TestJsonMapBlob.java b/src/test/java/com/avaje/tests/json/TestJsonMapBlob.java new file mode 100644 index 000000000..6b081a5ea --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonMapBlob.java @@ -0,0 +1,40 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.text.json.EJson; +import com.avaje.tests.model.json.EBasicJsonMapBlob; +import com.avaje.tests.model.json.EBasicJsonMapVarchar; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class TestJsonMapBlob extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + Map content = EJson.parseObject(s0); + + EBasicJsonMapBlob bean = new EBasicJsonMapBlob(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonMapBlob bean1 = Ebean.find(EBasicJsonMapBlob.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + assertEquals(bean.getContent().get("contentType"), bean1.getContent().get("contentType")); + assertEquals(18L, bean1.getContent().get("docId")); + + bean1.setName("just change name"); + Ebean.save(bean1); + } +} diff --git a/src/test/java/com/avaje/tests/json/TestJsonMapJsonB.java b/src/test/java/com/avaje/tests/json/TestJsonMapJsonB.java new file mode 100644 index 000000000..f3b250cc6 --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonMapJsonB.java @@ -0,0 +1,40 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.text.json.EJson; +import com.avaje.tests.model.json.EBasicJsonMapJsonB; +import com.avaje.tests.model.json.EBasicJsonMapJsonB; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class TestJsonMapJsonB extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + Map content = EJson.parseObject(s0); + + EBasicJsonMapJsonB bean = new EBasicJsonMapJsonB(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonMapJsonB bean1 = Ebean.find(EBasicJsonMapJsonB.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + assertEquals(bean.getContent().get("contentType"), bean1.getContent().get("contentType")); + assertEquals(18L, bean1.getContent().get("docId")); + + bean1.setName("just change name"); + Ebean.save(bean1); + } +} diff --git a/src/test/java/com/avaje/tests/json/TestJsonMapVarchar.java b/src/test/java/com/avaje/tests/json/TestJsonMapVarchar.java new file mode 100644 index 000000000..b8ae74021 --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonMapVarchar.java @@ -0,0 +1,40 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.text.json.EJson; +import com.avaje.tests.model.json.EBasicJsonMap; +import com.avaje.tests.model.json.EBasicJsonMapVarchar; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class TestJsonMapVarchar extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + Map content = EJson.parseObject(s0); + + EBasicJsonMapVarchar bean = new EBasicJsonMapVarchar(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonMapVarchar bean1 = Ebean.find(EBasicJsonMapVarchar.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + assertEquals(bean.getContent().get("contentType"), bean1.getContent().get("contentType")); + assertEquals(18L, bean1.getContent().get("docId")); + + bean1.setName("just change name"); + Ebean.save(bean1); + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonMap.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonMap.java new file mode 100644 index 000000000..6a68998e7 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonMap.java @@ -0,0 +1,55 @@ +package com.avaje.tests.model.json; + +import com.avaje.ebean.annotation.DbJson; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; +import java.util.Map; + +@Entity +public class EBasicJsonMap { + + @Id + Long id; + + @Version + Long version; + + String name; + + @DbJson + Map content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getContent() { + return content; + } + + public void setContent(Map content) { + this.content = content; + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonMapBlob.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapBlob.java new file mode 100644 index 000000000..b01873056 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapBlob.java @@ -0,0 +1,56 @@ +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.Map; + +@Entity +public class EBasicJsonMapBlob { + + @Id + Long id; + + @Version + Long version; + + String name; + + @DbJson(storage = DbJsonType.BLOB) + Map content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getContent() { + return content; + } + + public void setContent(Map content) { + this.content = content; + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonMapJsonB.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapJsonB.java new file mode 100644 index 000000000..c8ece036f --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapJsonB.java @@ -0,0 +1,55 @@ +package com.avaje.tests.model.json; + +import com.avaje.ebean.annotation.DbJsonB; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; +import java.util.Map; + +@Entity +public class EBasicJsonMapJsonB { + + @Id + Long id; + + @Version + Long version; + + String name; + + @DbJsonB + Map content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getContent() { + return content; + } + + public void setContent(Map content) { + this.content = content; + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonMapVarchar.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapVarchar.java new file mode 100644 index 000000000..648574201 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapVarchar.java @@ -0,0 +1,56 @@ +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.Map; + +@Entity +public class EBasicJsonMapVarchar { + + @Id + Long id; + + @Version + Long version; + + String name; + + @DbJson(storage = DbJsonType.VARCHAR)//, length = 2200) + Map content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getContent() { + return content; + } + + public void setContent(Map content) { + this.content = content; + } +}
@@ -370,8 +380,8 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { return value; } + @SuppressWarnings("ResultOfMethodCallIgnored") private boolean isIntegerType(String s) { - try { Integer.parseInt(s); return true; @@ -685,7 +695,14 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { * Register all the standard types supported. This is the standard JDBC types * plus some other common types such as java.util.Date and java.util.Calendar. */ - protected void initialiseStandard(JsonConfig.DateTime mode, int platformClobType, int platformBlobType, boolean binaryUUID) { + protected void initialiseStandard(JsonConfig.DateTime mode, ServerConfig config) { + + boolean binaryUUID = config.isUuidStoreAsBinary(); + DatabasePlatform databasePlatform = config.getDatabasePlatform(); + int platformClobType = databasePlatform.getClobDbType(); + int platformBlobType = databasePlatform.getBlobDbType(); + + nativeMap.put(DbType.HSTORE, new ScalarTypePostgresHstore()); ScalarType> utilDateType = extraTypeFactory.createUtilDate(mode); typeMap.put(java.util.Date.class, utilDateType); diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareFlag.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareFlag.java new file mode 100644 index 000000000..31081879f --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareFlag.java @@ -0,0 +1,26 @@ +package com.avaje.ebeaninternal.server.type; + +/** + * Detects when content has been modified and as such needs to be persisted (included in an update). + */ +public class ModifyAwareFlag implements ModifyAwareOwner { + + boolean dirty; + + @Override + public boolean isMarkedDirty() { + if (!dirty) return false; + dirty = false; + return true; + } + + @Override + public void markAsModified() { + dirty = true; + } + + @Override + public void resetMarkedDirty() { + dirty = false; + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareList.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareList.java new file mode 100644 index 000000000..1c5096146 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareList.java @@ -0,0 +1,166 @@ +package com.avaje.ebeaninternal.server.type; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +/** + * Modify aware wrapper of a list. + */ +public class ModifyAwareList implements List, ModifyAwareOwner { + + final List list; + + final ModifyAwareOwner owner; + + public ModifyAwareList(List list) { + this.list = list; + this.owner = new ModifyAwareFlag(); + } + + public ModifyAwareList(ModifyAwareOwner owner, List list) { + this.list = list; + this.owner = owner; + } + + @Override + public boolean isMarkedDirty() { + return owner.isMarkedDirty(); + } + + @Override + public void markAsModified() { + owner.markAsModified(); + } + + @Override + public void resetMarkedDirty() { + owner.resetMarkedDirty(); + } + + @Override + public int size() { + return list.size(); + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return list.contains(o); + } + + @Override + public Iterator iterator() { + return new ModifyAwareIterator(owner, list.iterator()); + } + + @Override + public Object[] toArray() { + return list.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return list.toArray(a); + } + + @Override + public boolean add(E e) { + owner.markAsModified(); + return list.add(e); + } + + @Override + public boolean remove(Object o) { + owner.markAsModified(); + return list.remove(o); + } + + @Override + public boolean containsAll(Collection> c) { + return list.containsAll(c); + } + + @Override + public boolean addAll(Collection extends E> c) { + owner.markAsModified(); + return list.addAll(c); + } + + @Override + public boolean addAll(int index, Collection extends E> c) { + owner.markAsModified(); + return list.addAll(index, c); + } + + @Override + public boolean removeAll(Collection> c) { + owner.markAsModified(); + return list.removeAll(c); + } + + @Override + public boolean retainAll(Collection> c) { + owner.markAsModified(); + return list.retainAll(c); + } + + @Override + public void clear() { + owner.markAsModified(); + list.clear(); + } + + @Override + public E get(int index) { + return list.get(index); + } + + @Override + public E set(int index, E element) { + owner.markAsModified(); + return list.set(index, element); + } + + @Override + public void add(int index, E element) { + owner.markAsModified(); + list.add(index, element); + } + + @Override + public E remove(int index) { + owner.markAsModified(); + return list.remove(index); + } + + @Override + public int indexOf(Object o) { + return list.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return list.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return new ModifyAwareListIterator(owner, list.listIterator()); + } + + @Override + public ListIterator listIterator(int index) { + return new ModifyAwareListIterator(owner, list.listIterator(index)); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return new ModifyAwareList(owner,list.subList(fromIndex, toIndex)); + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareListIterator.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareListIterator.java new file mode 100644 index 000000000..2b5bdc362 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareListIterator.java @@ -0,0 +1,66 @@ +package com.avaje.ebeaninternal.server.type; + +import java.util.ListIterator; + +/** + * Modify aware wrapper of a ListIterator. + */ +public class ModifyAwareListIterator implements ListIterator { + + final ModifyAwareOwner owner; + + final ListIterator iterator; + + public ModifyAwareListIterator(ModifyAwareOwner owner, ListIterator iterator) { + this.owner = owner; + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public E next() { + return iterator.next(); + } + + @Override + public boolean hasPrevious() { + return iterator.hasPrevious(); + } + + @Override + public E previous() { + return iterator.previous(); + } + + @Override + public int nextIndex() { + return iterator.nextIndex(); + } + + @Override + public int previousIndex() { + return iterator.previousIndex(); + } + + @Override + public void remove() { + owner.markAsModified(); + iterator.remove(); + } + + @Override + public void set(E e) { + owner.markAsModified(); + iterator.set(e); + } + + @Override + public void add(E e) { + owner.markAsModified(); + iterator.add(e); + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareMap.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareMap.java index 234c231bd..724dde7e7 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareMap.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareMap.java @@ -9,18 +9,21 @@ import java.util.Set; */ public class ModifyAwareMap implements Map, ModifyAwareOwner { - /** - * Dirty flag set when the map has been modified. - */ - private boolean dirty; + final ModifyAwareOwner owner; /** * The underlying map. */ - private Map map; + final Map map; public ModifyAwareMap(Map underyling) { this.map = underyling; + this.owner = new ModifyAwareFlag(); + } + + public ModifyAwareMap(ModifyAwareOwner owner, Map underyling) { + this.owner = owner; + this.map = underyling; } public String toString() { @@ -29,12 +32,17 @@ public class ModifyAwareMap implements Map, ModifyAwareOwner { @Override public boolean isMarkedDirty() { - return dirty; + return owner.isMarkedDirty(); } @Override public void markAsModified() { - dirty = true; + owner.markAsModified(); + } + + @Override + public void resetMarkedDirty() { + owner.resetMarkedDirty(); } @Override diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareOwner.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareOwner.java index 542278c9f..fe2d6f85a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareOwner.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareOwner.java @@ -7,6 +7,7 @@ public interface ModifyAwareOwner { /** * Return true if the value is considered dirty. + * Note that this resets the dirty status back to clean. */ boolean isMarkedDirty(); @@ -14,4 +15,10 @@ public interface ModifyAwareOwner { * Marks the object as modified. */ void markAsModified(); + + /** + * Reset the dirty state to clean. + */ + void resetMarkedDirty(); + } diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/RsetDataReader.java b/src/main/java/com/avaje/ebeaninternal/server/type/RsetDataReader.java index 851a45935..54965a6b7 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/RsetDataReader.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/RsetDataReader.java @@ -168,6 +168,15 @@ public class RsetDataReader implements DataReader { return readStringLob(reader); } + @Override + public Reader getClobReader() throws SQLException { + Clob clob = rset.getClob(pos()); + if (clob == null) { + return null; + } + return clob.getCharacterStream(); + } + public String getStringClob() throws SQLException { Clob clob = rset.getClob(pos()); @@ -212,6 +221,14 @@ public class RsetDataReader implements DataReader { return getBinaryLob(in); } + public InputStream getBlobInputStream() throws SQLException { + Blob blob = rset.getBlob(pos()); + if (blob == null) { + return null; + } + return blob.getBinaryStream(); + } + protected byte[] getBinaryLob(InputStream in) throws SQLException { try { diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMap.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMap.java new file mode 100644 index 000000000..3f80ce5c2 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMap.java @@ -0,0 +1,214 @@ +package com.avaje.ebeaninternal.server.type; + +import com.avaje.ebean.text.TextException; +import com.avaje.ebean.text.json.EJson; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Map; + +/** + * Type which maps Map to various DB types (Clob, Varchar, Blob) in JSON format. + */ +public abstract class ScalarTypeJsonMap extends ScalarTypeBase { + + public static class Clob extends ScalarTypeJsonMap { + + public Clob() { + super(Types.CLOB); + } + + @Override + public Map read(DataReader dataReader) throws SQLException { + + Reader reader = dataReader.getClobReader(); + if (reader == null) { + return null; + } + try { + Map map = parse(reader); + reader.close(); + return map; + } catch (IOException e) { + throw new SQLException("Error reading Clob stream from DB", e); + } + } + } + + public static class Varchar extends ScalarTypeJsonMap { + + public Varchar() { + super(Types.VARCHAR); + } + } + + public static class Blob extends ScalarTypeJsonMap { + public Blob() { + super(Types.BLOB); + } + + @Override + public Map read(DataReader dataReader) throws SQLException { + + InputStream is = dataReader.getBlobInputStream(); + if (is == null) { + return null; + } + try { + InputStreamReader reader = new InputStreamReader(is); + Map map = parse(reader); + reader.close(); + return map; + } catch (IOException e) { + throw new SQLException("Error reading Blob stream from DB", e); + } + } + + @Override + public void bind(DataBind b, Map value) throws SQLException { + + if (value == null) { + b.setNull(Types.BLOB); + } else { + String rawJson = formatValue(value); + InputStream stream = new ByteArrayInputStream(rawJson.getBytes(StandardCharsets.UTF_8)); + b.setBlob(stream); + } + } + } + + public ScalarTypeJsonMap(int jdbcType) { + super(Map.class, false, jdbcType); + } + + /** + * Map is 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 Map read(DataReader dataReader) throws SQLException { + + String rawJson = dataReader.getString(); + if (rawJson == null) { + return null; + } + return parse(rawJson); + } + + @Override + public void bind(DataBind b, Map value) throws SQLException { + + if (value == null) { + b.setNull(Types.VARCHAR); + } else { + String rawJson = formatValue(value); + b.setString(rawJson); + } + } + + @Override + public Object toJdbcType(Object value) { + return value; + } + + @Override + public Map toBeanType(Object value) { + return (Map) value; + } + + @Override + public String formatValue(Map v) { + try { + return EJson.write(v); + } catch (IOException e) { + throw new TextException(e); + } + } + + @Override + public Map parse(String value) { + try { + // return a modify aware map + return EJson.parseObject(value, true); + } catch (IOException e) { + throw new TextException(e); + } + } + + public Map parse(Reader reader) { + try { + // return a modify aware map + return EJson.parseObject(reader, true); + } catch (IOException e) { + throw new TextException(e); + } + } + + @Override + public Map convertFromMillis(long dateTime) { + throw new RuntimeException("Should never be called"); + } + + @Override + public boolean isDateTimeCapable() { + return false; + } + + @Override + public Map readData(DataInput dataInput) throws IOException { + if (!dataInput.readBoolean()) { + return null; + } else { + String json = dataInput.readUTF(); + return parse(json); + } + } + + @Override + public void writeData(DataOutput dataOutput, Map v) throws IOException { + if (v == null) { + dataOutput.writeBoolean(false); + } else { + dataOutput.writeBoolean(true); + String json = format(v); + dataOutput.writeUTF(json); + } + } + + @Override + public void jsonWrite(JsonGenerator ctx, String name, Map value) throws IOException { + // write the field name followed by the Map/JSON Object + if (value == null) { + ctx.writeNullField(name); + } else { + ctx.writeFieldName(name); + EJson.write(value, ctx); + } + } + + @Override + public Map jsonRead(JsonParser ctx, JsonToken event) throws IOException { + // at this point the BeanProperty has read the START_OBJECT token + // to check for a null value. Pass the START_OBJECT token through to + // the EJson parsing so that it knows the first token has been read + return EJson.parseObject(ctx, event); + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java new file mode 100644 index 000000000..388826545 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonMapPostgres.java @@ -0,0 +1,55 @@ +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; + +/** + * Support for the Postgres DB types JSON and JSONB. + */ +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) { + super(jdbcType); + this.postgresType = postgresType; + } + + @Override + public void bind(DataBind b, Map value) throws SQLException { + + String rawJson = (value == null) ? null : formatValue(value); + + PGobject pgo = new PGobject(); + pgo.setType(postgresType); + pgo.setValue(rawJson); + b.setObject(pgo); + } + + /** + * ScalarType mapping java Map type to Postgres JSON database type. + */ + public static class JSON extends ScalarTypeJsonMapPostgres { + + public JSON() { + super(DbType.JSON, POSTGRES_TYPE_JSON); + } + } + + /** + * ScalarType mapping java Map type to Postgres JSONB database type. + */ + public static class JSONB extends ScalarTypeJsonMapPostgres { + + public JSONB() { + super(DbType.JSONB, POSTGRES_TYPE_JSONB); + } + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java index e8866c327..d91def549 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java @@ -1,18 +1,18 @@ package com.avaje.ebeaninternal.server.type; +import com.avaje.ebean.config.dbplatform.DbType; +import com.avaje.ebean.text.TextException; +import com.avaje.ebean.text.json.EJson; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; + import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.sql.SQLException; import java.util.Map; -import com.avaje.ebean.config.dbplatform.PostgresPlatform; -import com.avaje.ebean.text.json.EJson; -import com.avaje.ebean.text.TextException; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; - /** * Postgres Hstore type which maps Map to a single 'HStore column' in the DB. */ @@ -21,10 +21,8 @@ public class ScalarTypePostgresHstore extends ScalarTypeBase { public static final String KEY = "hstore"; - public static final int HSTORE_TYPE = PostgresPlatform.TYPE_HSTORE; - public ScalarTypePostgresHstore() { - super(Map.class, false, HSTORE_TYPE); + super(Map.class, false, DbType.HSTORE); } @Override 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 efaf2b3b4..71cd5db12 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java @@ -58,7 +58,10 @@ public interface TypeManager { ScalarType> createEnumScalarType(Class> enumType); /** - * Find a scalarType using a custom type key. Used for Hstore and similar special types. + * Return the ScalarType used to handle JSON content. + * + * Note that type expected to be JsonNode or Map. + * */ - ScalarType> getScalarTypeFromKey(String specialTypeKey); + ScalarType> getJsonScalarType(Class> type, int dbType); } diff --git a/src/test/java/com/avaje/ebean/json/EJsonTests.java b/src/test/java/com/avaje/ebean/json/EJsonTests.java index 434db49eb..366831bf1 100644 --- a/src/test/java/com/avaje/ebean/json/EJsonTests.java +++ b/src/test/java/com/avaje/ebean/json/EJsonTests.java @@ -1,6 +1,7 @@ package com.avaje.ebean.json; import com.avaje.ebean.text.json.EJson; +import com.avaje.ebeaninternal.server.type.ModifyAwareMap; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import org.junit.Assert; @@ -203,4 +204,46 @@ public class EJsonTests { } + @Test + public void test_map_nested_modifyAware() throws IOException { + + String jsonInput = "{\"name\":\"rob\",\"age\":12,\"org\":{\"name\":\"superorg\",\"rating\":4},\"nums\":[1,2,3]}"; + ModifyAwareMap map = (ModifyAwareMap) EJson.parseObject(jsonInput, true); + + Assert.assertFalse(map.isMarkedDirty()); + Assert.assertEquals(4, map.size()); + + map.put("name", "jim"); + Assert.assertTrue(map.isMarkedDirty()); + + } + + @SuppressWarnings("unchecked") + @Test + public void test_map_nested_modifyAwareNestedList() throws IOException { + + String jsonInput = "{\"name\":\"rob\",\"age\":12,\"org\":{\"name\":\"superorg\",\"rating\":4},\"nums\":[1,2,3]}"; + ModifyAwareMap map = (ModifyAwareMap) EJson.parseObject(jsonInput, true); + Assert.assertFalse(map.isMarkedDirty()); + + List nums = (List) map.get("nums"); + nums.add(4); + Assert.assertTrue(map.isMarkedDirty()); + + } + + @SuppressWarnings("unchecked") + @Test + public void test_map_nested_modifyAwareNestedObject() throws IOException { + + String jsonInput = "{\"name\":\"rob\",\"age\":12,\"org\":{\"name\":\"superorg\",\"rating\":4},\"nums\":[1,2,3]}"; + ModifyAwareMap map = (ModifyAwareMap) EJson.parseObject(jsonInput, true); + Assert.assertFalse(map.isMarkedDirty()); + + Map org = (Map) map.get("org"); + org.put("extra","foo"); + Assert.assertTrue(map.isMarkedDirty()); + + } + } diff --git a/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareListTest.java b/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareListTest.java new file mode 100644 index 000000000..3c194c83c --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareListTest.java @@ -0,0 +1,228 @@ +package com.avaje.ebeaninternal.server.type; + +import org.junit.Test; + +import java.util.*; + +import static org.junit.Assert.*; + + +public class ModifyAwareListTest { + + private ModifyAwareList createList() { + ArrayList list = new ArrayList(); + list.addAll(Arrays.asList("A","B","C","D","E")); + return new ModifyAwareList(list); + } + private ModifyAwareList createEmptyList() { + return new ModifyAwareList(new ArrayList()); + } + + @Test + public void testSize() throws Exception { + + assertEquals(5, createList().size()); + } + + @Test + public void testIsEmpty() throws Exception { + + assertFalse(createList().isEmpty()); + assertTrue(createEmptyList().isEmpty()); + } + + @Test + public void testContains() throws Exception { + + assertTrue(createList().contains("B")); + assertFalse(createList().contains("Z")); + } + + @Test + public void testIterator() throws Exception { + + ModifyAwareList list = createList(); + Iterator iterator = list.iterator(); + assertTrue(iterator.hasNext()); + assertEquals("A", iterator.next()); + assertFalse(list.isMarkedDirty()); + + iterator.remove(); + assertTrue(list.isMarkedDirty()); + } + + @Test + public void testToArray() throws Exception { + + Object[] objects = createList().toArray(); + assertEquals(5, objects.length); + assertEquals("A", objects[0]); + assertEquals("E", objects[4]); + } + + @Test + public void testToArray1() throws Exception { + + String[] objects = createList().toArray(new String[5]); + assertEquals(5, objects.length); + assertEquals("A", objects[0]); + assertEquals("E", objects[4]); + } + + @Test + public void testAdd() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + list.add("F"); + assertTrue(list.isMarkedDirty()); + } + + @Test + public void testRemove() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + list.remove("A"); + assertTrue(list.isMarkedDirty()); + } + + @Test + public void testContainsAll() throws Exception { + + ModifyAwareList list = createList(); + + assertTrue(list.containsAll(Arrays.asList("A", "B"))); + assertFalse(list.containsAll(Arrays.asList("A", "B", "Z"))); + } + + @Test + public void testAddAll() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + assertTrue(list.addAll(Arrays.asList("F", "G"))); + assertTrue(list.isMarkedDirty()); + } + + + @Test + public void testRemoveAll() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + assertTrue(list.removeAll(Arrays.asList("A", "G"))); + assertTrue(list.isMarkedDirty()); + } + + @Test + public void testRetainAll() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + assertTrue(list.retainAll(Arrays.asList("A", "B"))); + assertTrue(list.isMarkedDirty()); + assertEquals(2, list.size()); + } + + @Test + public void testClear() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + list.clear(); + assertTrue(list.isMarkedDirty()); + assertEquals(0, list.size()); + + } + + @Test + public void testGet() throws Exception { + + ModifyAwareList list = createList(); + + assertEquals("A", list.get(0)); + assertEquals("B", list.get(1)); + assertEquals("E", list.get(4)); + } + + @Test + public void testSet() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + list.set(0, "Z"); + assertTrue(list.isMarkedDirty()); + assertEquals(5, list.size()); + } + + @Test + public void testIndexOf() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + assertEquals(2, list.indexOf("C")); + assertEquals(-1, list.indexOf("Z")); + assertFalse(list.isMarkedDirty()); + } + + @Test + public void testLastIndexOf() throws Exception { + + ModifyAwareList list = createList(); + assertFalse(list.isMarkedDirty()); + + assertEquals(2, list.lastIndexOf("C")); + assertEquals(-1, list.lastIndexOf("Z")); + assertFalse(list.isMarkedDirty()); + } + + @Test + public void testListIterator() throws Exception { + + ModifyAwareList list = createList(); + ListIterator iterator = list.listIterator(); + assertTrue(iterator.hasNext()); + assertEquals("A", iterator.next()); + assertFalse(list.isMarkedDirty()); + + iterator.remove(); + assertTrue(list.isMarkedDirty()); + } + + @Test + public void testListIterator1() throws Exception { + + ModifyAwareList list = createList(); + ListIterator iterator = list.listIterator(2); + assertTrue(iterator.hasNext()); + assertEquals("C", iterator.next()); + assertFalse(list.isMarkedDirty()); + + iterator.remove(); + assertTrue(list.isMarkedDirty()); + + } + + @Test + public void testSubList() throws Exception { + + ModifyAwareList list = createList(); + List sub = list.subList(1, 3); + assertEquals("B", sub.get(0)); + assertEquals("C", sub.get(1)); + + assertFalse(list.isMarkedDirty()); + + sub.remove("C"); + assertTrue(list.isMarkedDirty()); + } +} \ No newline at end of file diff --git a/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareMapTest.java b/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareMapTest.java new file mode 100644 index 000000000..1f9a09b12 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/type/ModifyAwareMapTest.java @@ -0,0 +1,180 @@ +package com.avaje.ebeaninternal.server.type; + +import org.junit.Test; + +import java.util.*; + +import static org.junit.Assert.*; + +public class ModifyAwareMapTest { + + private ModifyAwareMap createMap() { + LinkedHashMap map = new LinkedHashMap(); + map.put("A", "one"); + map.put("B", "two"); + map.put("C", "three"); + map.put("D", "four"); + map.put("E", "five"); + return new ModifyAwareMap(map); + } + + private ModifyAwareMap createEmptyMap() { + LinkedHashMap map = new LinkedHashMap(); + return new ModifyAwareMap(map); + } + + @Test + public void testToString() throws Exception { + + ModifyAwareMap map = createMap(); + assertEquals(map.map.toString(), map.toString()); + } + + @Test + public void testIsMarkedDirty() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + map.put("A", "change"); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testMarkAsModified() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + map.markAsModified(); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testSize() throws Exception { + + ModifyAwareMap map = createMap(); + assertEquals(5, map.size()); + } + + @Test + public void testIsEmpty() throws Exception { + + assertFalse(createMap().isEmpty()); + assertTrue(createEmptyMap().isEmpty()); + } + + @Test + public void testContainsKey() throws Exception { + + ModifyAwareMap map = createMap(); + assertTrue(map.containsKey("A")); + assertFalse(map.containsKey("Z")); + } + + @Test + public void testContainsValue() throws Exception { + ModifyAwareMap map = createMap(); + assertTrue(map.containsValue("one")); + assertFalse(map.containsValue("junk")); + } + + @Test + public void testGet() throws Exception { + + ModifyAwareMap map = createMap(); + + assertEquals("two", map.get("B")); + assertNull(map.get("Z")); + assertFalse(map.isMarkedDirty()); + } + + @Test + public void testPut() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + map.put("A", "mod"); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testRemove() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + map.remove("A"); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testPutAllWithEmpty() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + Map other = new HashMap(); + map.putAll(other); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testPutAll() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + Map other = new HashMap(); + other.put("A", "one"); + map.putAll(other); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testClear() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + map.clear(); + assertTrue(map.isMarkedDirty()); + } + + @Test + public void testKeySet() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + Set keys = map.keySet(); + assertEquals(map.size(), keys.size()); + assertTrue(keys.contains("A")); + assertFalse(map.isMarkedDirty()); + } + + @Test + public void testValues() throws Exception { + + ModifyAwareMap map = createMap(); + assertFalse(map.isMarkedDirty()); + + Collection values = map.values(); + assertEquals(map.size(), values.size()); + assertTrue(values.contains("one")); + assertFalse(map.isMarkedDirty()); + } + + @Test + public void testEntrySet() throws Exception { + + ModifyAwareMap map = createMap(); + Set> entries = map.entrySet(); + + assertFalse(map.isMarkedDirty()); + + assertEquals(map.size(), entries.size()); + assertFalse(map.isMarkedDirty()); + } +} \ No newline at end of file diff --git a/src/test/java/com/avaje/tests/json/TestJsonMapBasic.java b/src/test/java/com/avaje/tests/json/TestJsonMapBasic.java new file mode 100644 index 000000000..4681860c5 --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonMapBasic.java @@ -0,0 +1,67 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.text.json.EJson; +import com.avaje.tests.model.json.EBasicJsonMap; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class TestJsonMapBasic extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + //String s1 = "{\"docId\":19,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + Map content = EJson.parseObject(s0); + + EBasicJsonMap bean = new EBasicJsonMap(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonMap bean1 = Ebean.find(EBasicJsonMap.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + assertEquals(bean.getContent().get("contentType"), bean1.getContent().get("contentType")); + assertEquals(18L, bean1.getContent().get("docId")); + + bean1.setName("just change name"); + Ebean.save(bean1); + + // content changes detected - dirty state so included in update + Map content1 = bean1.getContent(); + content1.put("additional", "newValue"); + content1.put("docId", 99L); + bean1.setName("two"); + Ebean.save(bean1); + + EBasicJsonMap bean2 = Ebean.find(EBasicJsonMap.class, bean.getId()); + + // name changed and docId changed + assertEquals("two", bean2.getName()); + assertEquals(99L, bean2.getContent().get("docId")); + assertEquals("newValue", bean2.getContent().get("additional")); + + content1.put("additional", "modValue"); + bean1.setName("three"); + bean1.setContent(content1); + Ebean.save(bean1); + + EBasicJsonMap bean3 = Ebean.find(EBasicJsonMap.class, bean.getId()); + + assertEquals("three", bean3.getName()); + assertEquals(99L, bean3.getContent().get("docId")); + assertEquals("modValue", bean3.getContent().get("additional")); + } +} diff --git a/src/test/java/com/avaje/tests/json/TestJsonMapBlob.java b/src/test/java/com/avaje/tests/json/TestJsonMapBlob.java new file mode 100644 index 000000000..6b081a5ea --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonMapBlob.java @@ -0,0 +1,40 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.text.json.EJson; +import com.avaje.tests.model.json.EBasicJsonMapBlob; +import com.avaje.tests.model.json.EBasicJsonMapVarchar; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class TestJsonMapBlob extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + Map content = EJson.parseObject(s0); + + EBasicJsonMapBlob bean = new EBasicJsonMapBlob(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonMapBlob bean1 = Ebean.find(EBasicJsonMapBlob.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + assertEquals(bean.getContent().get("contentType"), bean1.getContent().get("contentType")); + assertEquals(18L, bean1.getContent().get("docId")); + + bean1.setName("just change name"); + Ebean.save(bean1); + } +} diff --git a/src/test/java/com/avaje/tests/json/TestJsonMapJsonB.java b/src/test/java/com/avaje/tests/json/TestJsonMapJsonB.java new file mode 100644 index 000000000..f3b250cc6 --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonMapJsonB.java @@ -0,0 +1,40 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.text.json.EJson; +import com.avaje.tests.model.json.EBasicJsonMapJsonB; +import com.avaje.tests.model.json.EBasicJsonMapJsonB; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class TestJsonMapJsonB extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + Map content = EJson.parseObject(s0); + + EBasicJsonMapJsonB bean = new EBasicJsonMapJsonB(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonMapJsonB bean1 = Ebean.find(EBasicJsonMapJsonB.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + assertEquals(bean.getContent().get("contentType"), bean1.getContent().get("contentType")); + assertEquals(18L, bean1.getContent().get("docId")); + + bean1.setName("just change name"); + Ebean.save(bean1); + } +} diff --git a/src/test/java/com/avaje/tests/json/TestJsonMapVarchar.java b/src/test/java/com/avaje/tests/json/TestJsonMapVarchar.java new file mode 100644 index 000000000..b8ae74021 --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonMapVarchar.java @@ -0,0 +1,40 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.text.json.EJson; +import com.avaje.tests.model.json.EBasicJsonMap; +import com.avaje.tests.model.json.EBasicJsonMapVarchar; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class TestJsonMapVarchar extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + Map content = EJson.parseObject(s0); + + EBasicJsonMapVarchar bean = new EBasicJsonMapVarchar(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonMapVarchar bean1 = Ebean.find(EBasicJsonMapVarchar.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + assertEquals(bean.getContent().get("contentType"), bean1.getContent().get("contentType")); + assertEquals(18L, bean1.getContent().get("docId")); + + bean1.setName("just change name"); + Ebean.save(bean1); + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonMap.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonMap.java new file mode 100644 index 000000000..6a68998e7 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonMap.java @@ -0,0 +1,55 @@ +package com.avaje.tests.model.json; + +import com.avaje.ebean.annotation.DbJson; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; +import java.util.Map; + +@Entity +public class EBasicJsonMap { + + @Id + Long id; + + @Version + Long version; + + String name; + + @DbJson + Map content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getContent() { + return content; + } + + public void setContent(Map content) { + this.content = content; + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonMapBlob.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapBlob.java new file mode 100644 index 000000000..b01873056 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapBlob.java @@ -0,0 +1,56 @@ +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.Map; + +@Entity +public class EBasicJsonMapBlob { + + @Id + Long id; + + @Version + Long version; + + String name; + + @DbJson(storage = DbJsonType.BLOB) + Map content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getContent() { + return content; + } + + public void setContent(Map content) { + this.content = content; + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonMapJsonB.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapJsonB.java new file mode 100644 index 000000000..c8ece036f --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapJsonB.java @@ -0,0 +1,55 @@ +package com.avaje.tests.model.json; + +import com.avaje.ebean.annotation.DbJsonB; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; +import java.util.Map; + +@Entity +public class EBasicJsonMapJsonB { + + @Id + Long id; + + @Version + Long version; + + String name; + + @DbJsonB + Map content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getContent() { + return content; + } + + public void setContent(Map content) { + this.content = content; + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonMapVarchar.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapVarchar.java new file mode 100644 index 000000000..648574201 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonMapVarchar.java @@ -0,0 +1,56 @@ +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.Map; + +@Entity +public class EBasicJsonMapVarchar { + + @Id + Long id; + + @Version + Long version; + + String name; + + @DbJson(storage = DbJsonType.VARCHAR)//, length = 2200) + Map content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getContent() { + return content; + } + + public void setContent(Map content) { + this.content = content; + } +}
+ * Note that type expected to be JsonNode or Map. + *