diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java
index beefce9eb..6934a2145 100644
--- a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java
+++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java
@@ -32,6 +32,7 @@ import javax.persistence.Id;
import javax.persistence.Version;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import java.lang.reflect.Type;
import java.sql.Types;
/**
@@ -147,6 +148,8 @@ public class DeployBeanProperty {
*/
private final Class> propertyType;
+ private final Type genericType;
+
/**
* Set for Non-JDBC types to provide logical to db type conversion.
*/
@@ -208,10 +211,17 @@ public class DeployBeanProperty {
public DeployBeanProperty(DeployBeanDescriptor> desc, Class> propertyType, ScalarType> scalarType, ScalarTypeConverter, ?> typeConverter) {
this.desc = desc;
this.propertyType = propertyType;
+ this.genericType = null;
this.scalarType = wrapScalarType(propertyType, scalarType, typeConverter);
this.dbType = (scalarType == null) ? 0 : scalarType.getJdbcType();
}
+ public DeployBeanProperty(DeployBeanDescriptor> desc, Class> propertyType, Type genericType) {
+ this.desc = desc;
+ this.propertyType = propertyType;
+ this.genericType = genericType;
+ }
+
/**
* Wrap the ScalarType using a ScalarTypeConverter.
*/
@@ -810,6 +820,13 @@ public class DeployBeanProperty {
return propertyType;
}
+ /**
+ * Return the generic type for this property.
+ */
+ public Type getGenericType() {
+ return genericType;
+ }
+
/**
* Return true if this is included in the unique id.
*/
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 071c57fbb..8362fdcc5 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
@@ -230,7 +230,7 @@ public class DeployCreateProperties {
}
}
if (isSpecialScalarType(field)) {
- return new DeployBeanProperty(desc, propertyType, null, null);
+ return new DeployBeanProperty(desc, propertyType, field.getGenericType());
}
// check for Collection type (list, set or map)
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 52124a5f6..0d430f7e7 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
@@ -226,7 +226,7 @@ public class DeployUtil {
private void setDbJsonType(DeployBeanProperty prop, int dbType, int dbLength) {
Class> type = prop.getPropertyType();
- ScalarType> scalarType = typeManager.getJsonScalarType(type, dbType, dbLength);
+ ScalarType> scalarType = typeManager.getJsonScalarType(type, dbType, dbLength, prop.getGenericType());
if (scalarType == null) {
throw new RuntimeException("No ScalarType for JSON type [" + type + "] [" + dbType + "]");
}
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 503019e3d..3da28dbd3 100644
--- a/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java
+++ b/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java
@@ -33,6 +33,8 @@ import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.InetAddress;
@@ -144,7 +146,7 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
private final JsonConfig.DateTime jsonDateTime;
- private final boolean objectMapperPresent;
+ private final Object objectMapper;
private final boolean java7Present;
@@ -187,7 +189,8 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
this.typeMap = new ConcurrentHashMap, ScalarType>>();
this.nativeMap = new ConcurrentHashMap>();
- this.objectMapperPresent = config.getClassLoadConfig().isJacksonObjectMapperPresent();
+ boolean objectMapperPresent = config.getClassLoadConfig().isJacksonObjectMapperPresent();
+ this.objectMapper = (objectMapperPresent) ? initObjectMapper(config) : null;
this.extraTypeFactory = new DefaultTypeFactory(config);
this.postgres = isPostgres(config.getDatabasePlatform());
@@ -355,18 +358,30 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
}
@Override
- public ScalarType> getJsonScalarType(Class> type, int dbType, int dbLength) {
+ public ScalarType> getJsonScalarType(Class> type, int dbType, int dbLength, Type genericType) {
if (type.equals(List.class)) {
- return ScalarTypeJsonList.typeFor(postgres, dbType);
+ if (isValueTypeSimple(genericType)) {
+ return ScalarTypeJsonList.typeFor(postgres, dbType);
+ } else {
+ return createJsonObjectMapperType(type, genericType, dbType);
+ }
}
if (type.equals(Set.class)) {
- return ScalarTypeJsonSet.typeFor(postgres, dbType);
+ if (isValueTypeSimple(genericType)) {
+ return ScalarTypeJsonSet.typeFor(postgres, dbType);
+ } else {
+ return createJsonObjectMapperType(type, genericType, dbType);
+ }
}
if (type.equals(Map.class)) {
- return ScalarTypeJsonMap.typeFor(postgres, dbType);
+ if (isMapValueTypeObject(genericType)) {
+ return ScalarTypeJsonMap.typeFor(postgres, dbType);
+ } else {
+ return createJsonObjectMapperType(type, genericType, dbType);
+ }
}
if (type.equals(JsonNode.class)) {
@@ -381,7 +396,30 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
}
}
- throw new IllegalArgumentException("Type [" + type + "] unsupported for @DbJson mapping");
+ return createJsonObjectMapperType(type, type, dbType);
+ }
+
+ /**
+ * Return true if value parameter type of the map is Object.
+ */
+ private boolean isValueTypeSimple(Type genericType) {
+ Type[] typeArgs = ((ParameterizedType) genericType).getActualTypeArguments();
+ return String.class.equals(typeArgs[0]) || Long.class.equals(typeArgs[0]);
+ }
+
+ /**
+ * Return true if value parameter type of the map is Object.
+ */
+ private boolean isMapValueTypeObject(Type genericType) {
+ Type[] typeArgs = ((ParameterizedType) genericType).getActualTypeArguments();
+ return Object.class.equals(typeArgs[1]);
+ }
+
+ private ScalarType> createJsonObjectMapperType(Class> type, Type genericType, int dbType) {
+ if (objectMapper == null) {
+ throw new IllegalArgumentException("Type [" + type + "] unsupported for @DbJson mapping - Jackson ObjectMapper not present");
+ }
+ return ScalarTypeJsonObjectMapper.createTypeFor(postgres, type, (ObjectMapper) objectMapper, genericType, dbType);
}
/**
@@ -610,15 +648,13 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
try {
ScalarType> scalarType;
- if (!objectMapperPresent) {
+ if (objectMapper == null) {
scalarType = (ScalarType>) cls.newInstance();
-
} else {
try {
// first try objectMapper constructor
Constructor> constructor = cls.getConstructor(ObjectMapper.class);
- ObjectMapper objectMapper = getObjectMapper(serverConfig);
- scalarType = (ScalarType>)constructor.newInstance(objectMapper);
+ scalarType = (ScalarType>)constructor.newInstance((ObjectMapper)objectMapper);
} catch (NoSuchMethodException e) {
scalarType = (ScalarType>) cls.newInstance();
}
@@ -634,9 +670,9 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable {
}
}
- private ObjectMapper getObjectMapper(ServerConfig serverConfig) {
+ private Object initObjectMapper(ServerConfig serverConfig) {
- ObjectMapper objectMapper = (ObjectMapper)serverConfig.getObjectMapper();
+ Object objectMapper = serverConfig.getObjectMapper();
if (objectMapper == null) {
objectMapper = new ObjectMapper();
serverConfig.setObjectMapper(objectMapper);
diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareSet.java b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareSet.java
index 8bd1da4ae..fbfdff06b 100644
--- a/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareSet.java
+++ b/src/main/java/com/avaje/ebeaninternal/server/type/ModifyAwareSet.java
@@ -14,6 +14,13 @@ public class ModifyAwareSet extends ModifyAwareCollection implements Set s) {
+ super(new ModifyAwareFlag(), s);
+ }
+
@Override
public boolean isMarkedDirty() {
return owner.isMarkedDirty();
diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java
new file mode 100644
index 000000000..aba059ef7
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java
@@ -0,0 +1,268 @@
+package com.avaje.ebeaninternal.server.type;
+
+import com.avaje.ebean.config.dbplatform.DbType;
+import com.avaje.ebeanservice.docstore.api.mapping.DocPropertyType;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import javax.persistence.PersistenceException;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Supports @DbJson properties using Jackson ObjectMapper.
+ */
+public class ScalarTypeJsonObjectMapper {
+
+ /**
+ * Create and return the appropriate ScalarType.
+ *
+ * @param postgres
+ * @param type The field/property type
+ * @param objectMapper The Jackson ObjectMapper to use for marshalling
+ * @param genericType The generic type
+ * @param dbType The DB storage type to use
+ */
+ public static ScalarType> createTypeFor(boolean postgres, Class> type, ObjectMapper objectMapper, Type genericType, int dbType) {
+
+ String pgType = getPostgresType(postgres, dbType);
+ if (Set.class.equals(type)) {
+ return new OmSet(objectMapper, genericType, dbType, pgType);
+ }
+ if (List.class.equals(type)) {
+ return new OmList(objectMapper, genericType, dbType, pgType);
+ }
+ if (Map.class.equals(type)) {
+ return new OmMap(objectMapper, genericType, dbType, pgType);
+ }
+ return new GenericObject(objectMapper, genericType, dbType, pgType);
+ }
+
+ private static String getPostgresType(boolean postgres, int dbType) {
+ if (postgres) {
+ switch (dbType) {
+ case DbType.JSON : return PostgresHelper.JSON_TYPE;
+ case DbType.JSONB : return PostgresHelper.JSONB_TYPE;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Maps any type (Object) using Jackson ObjectMapper.
+ */
+ private static class GenericObject extends Base
*/
- ScalarType> getJsonScalarType(Class> type, int dbType, int dbLength);
+ ScalarType> getJsonScalarType(Class> type, int dbType, int dbLength, Type genericType);
}
diff --git a/src/test/java/com/avaje/tests/json/TestDbJson_List.java b/src/test/java/com/avaje/tests/json/TestDbJson_List.java
index 7e876027d..fc96b7f3f 100644
--- a/src/test/java/com/avaje/tests/json/TestDbJson_List.java
+++ b/src/test/java/com/avaje/tests/json/TestDbJson_List.java
@@ -3,13 +3,19 @@ package com.avaje.tests.json;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.tests.model.json.EBasicJsonList;
+import com.avaje.tests.model.json.PlainBean;
import org.avaje.ebeantest.LoggedSqlCollector;
import org.junit.Test;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class TestDbJson_List extends BaseTestCase {
@@ -30,6 +36,22 @@ public class TestDbJson_List extends BaseTestCase {
bean.getFlags().add(43L);
bean.getFlags().add(44L);
+ bean.setPlainBean(new PlainBean("plain", 52));
+
+ List beanList = new ArrayList();
+ beanList.add(new PlainBean("one", 1));
+ beanList.add(new PlainBean("two", 2));
+ bean.setBeanList(beanList);
+
+
+ Set beanSet = new LinkedHashSet();
+ beanSet.add(new PlainBean("A", 1));
+ beanSet.add(new PlainBean("B", 2));
+ bean.setBeanSet(beanSet);
+
+ bean.getBeanMap().put("key0", new PlainBean("k0", 90));
+ bean.getBeanMap().put("key1", new PlainBean("k1", 91));
+
Ebean.save(bean);
found = Ebean.find(EBasicJsonList.class, bean.getId());
@@ -38,11 +60,15 @@ public class TestDbJson_List extends BaseTestCase {
assertTrue(found.getFlags().contains(42L));
assertTrue(found.getFlags().contains(43L));
assertTrue(found.getFlags().contains(44L));
+ assertThat(found.getBeanList()).hasSize(2);
+ assertThat(found.getBeanSet()).hasSize(2);
+ assertThat(found.getBeanMap()).hasSize(2);
json_parse_format();
update_when_notDirty();
update_when_dirty();
update_when_dirty_flags();
+ update_when_dirty_SetListMap();
}
//@Test//(dependsOnMethods = "insert")
@@ -51,6 +77,10 @@ public class TestDbJson_List extends BaseTestCase {
String asJson = Ebean.json().toJson(found);
assertThat(asJson).contains("\"tags\":[\"one\",\"two\"]");
assertThat(asJson).contains("\"flags\":[42,43,44]");
+ assertThat(asJson).contains("\"plainBean\":{\"name\":\"plain\"");
+ assertThat(asJson).contains("\"beanSet\":[");
+ assertThat(asJson).contains("\"beanList\":[");
+ assertThat(asJson).contains("\"beanMap\":{");
assertThat(asJson).contains("\"id\":");
EBasicJsonList fromJson = Ebean.json().toBean(EBasicJsonList.class, asJson);
@@ -62,6 +92,9 @@ public class TestDbJson_List extends BaseTestCase {
assertTrue(fromJson.getFlags().contains(43L));
assertTrue(fromJson.getFlags().contains(44L));
+ assertThat(fromJson.getBeanSet()).hasSize(2);
+ assertThat(fromJson.getBeanList()).hasSize(2);
+ assertThat(fromJson.getBeanMap()).hasSize(2);
}
//@Test//(dependsOnMethods = "insert")
@@ -73,7 +106,7 @@ public class TestDbJson_List extends BaseTestCase {
List sql = LoggedSqlCollector.stop();
// we don't update the phone numbers (as they are not dirty)
- assertThat(sql.get(0)).contains("update ebasic_json_list set name=?, version=? where");
+ assertThat(sql.get(0)).contains("update ebasic_json_list set name=?, plain_bean=?, version=? where");
}
public void update_when_dirty() {
@@ -86,7 +119,7 @@ public class TestDbJson_List extends BaseTestCase {
List sql = LoggedSqlCollector.stop();
// we don't update the phone numbers (as they are not dirty)
- assertThat(sql.get(0)).contains("update ebasic_json_list set tags=?, version=? where");
+ assertThat(sql.get(0)).contains("update ebasic_json_list set plain_bean=?, tags=?, version=? where id=? and version=?");
}
public void update_when_dirty_flags() {
@@ -99,7 +132,40 @@ public class TestDbJson_List extends BaseTestCase {
List sql = LoggedSqlCollector.stop();
// we don't update the phone numbers (as they are not dirty)
- assertThat(sql.get(0)).contains("update ebasic_json_list set flags=?, version=? where");
+ assertThat(sql.get(0)).contains("update ebasic_json_list set plain_bean=?, flags=?, version=? where id=? and version=?;");
}
+ public void update_when_dirty_SetListMap() {
+
+ //found.setName("modAgain");
+ found.getBeanSet().clear();
+ found.getBeanList().clear();
+ found.getBeanMap().remove("key0");
+
+ LoggedSqlCollector.start();
+ Ebean.save(found);
+ List sql = LoggedSqlCollector.stop();
+
+ // we don't update the phone numbers (as they are not dirty)
+ assertThat(sql.get(0)).contains("update ebasic_json_list set bean_set=?, bean_list=?, bean_map=?, plain_bean=?, version=? where id=? and version=?");
+ }
+
+ @Test
+ public void insert_fetch_when_null() {
+
+ EBasicJsonList bean = new EBasicJsonList();
+ bean.setName("leave some nulls");
+ bean.setFlags(null);
+ bean.setTags(null);
+ bean.setBeanMap(null);
+
+ Ebean.save(bean);
+
+ EBasicJsonList found = Ebean.find(EBasicJsonList.class, bean.getId());
+
+ assertNull(found.getPlainBean());
+
+ String asJson = Ebean.json().toJson(found);
+ assertNotNull(asJson);
+ }
}
diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonList.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonList.java
index 9d6688a08..64d1256a5 100644
--- a/src/test/java/com/avaje/tests/model/json/EBasicJsonList.java
+++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonList.java
@@ -7,8 +7,10 @@ import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Version;
import java.util.ArrayList;
+import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
@Entity
@@ -19,6 +21,18 @@ public class EBasicJsonList {
String name;
+ @DbJson(length = 700)
+ Set beanSet;
+
+ @DbJson(length = 700)
+ List beanList;
+
+ @DbJson(length = 700)
+ Map beanMap = new LinkedHashMap();
+
+ @DbJson(length = 500)
+ PlainBean plainBean;
+
@DbJson(length = 50)
Set flags = new LinkedHashSet();
@@ -60,6 +74,38 @@ public class EBasicJsonList {
this.flags = flags;
}
+ public PlainBean getPlainBean() {
+ return plainBean;
+ }
+
+ public void setPlainBean(PlainBean plainBean) {
+ this.plainBean = plainBean;
+ }
+
+ public Set getBeanSet() {
+ return beanSet;
+ }
+
+ public void setBeanSet(Set beanSet) {
+ this.beanSet = beanSet;
+ }
+
+ public List getBeanList() {
+ return beanList;
+ }
+
+ public void setBeanList(List beanList) {
+ this.beanList = beanList;
+ }
+
+ public Map getBeanMap() {
+ return beanMap;
+ }
+
+ public void setBeanMap(Map beanMap) {
+ this.beanMap = beanMap;
+ }
+
public Long getVersion() {
return version;
}
diff --git a/src/test/java/com/avaje/tests/model/json/PlainBean.java b/src/test/java/com/avaje/tests/model/json/PlainBean.java
new file mode 100644
index 000000000..3e8c9d7c5
--- /dev/null
+++ b/src/test/java/com/avaje/tests/model/json/PlainBean.java
@@ -0,0 +1,55 @@
+package com.avaje.tests.model.json;
+
+import java.sql.Timestamp;
+
+/**
+ * Something for Jackson ObjectMapper to marshall.
+ */
+public class PlainBean {
+
+ String name;
+
+ long along;
+
+ Timestamp timestamp;
+
+ public PlainBean(String name, long along) {
+ this.name = name;
+ this.along = along;
+ this.timestamp = new Timestamp(System.currentTimeMillis());
+ }
+
+ /**
+ * A constructor for Jackson.
+ */
+ public PlainBean() {
+ }
+
+ public String toString() {
+ return "name:" + name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public long getAlong() {
+ return along;
+ }
+
+ public void setAlong(long along) {
+ this.along = along;
+ }
+
+ public Timestamp getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(Timestamp timestamp) {
+ this.timestamp = timestamp;
+ }
+}