#542 - ENH: @DbJson fields are auto serialized and deserialized to specified classes.

This commit is contained in:
Robin Bygrave
2016-05-06 13:20:51 +12:00
parent deb6ce23d7
commit 13f7d37e76
10 changed files with 516 additions and 19 deletions
@@ -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.
*/
@@ -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)
@@ -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 + "]");
}
@@ -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<Class<?>, ScalarType<?>>();
this.nativeMap = new ConcurrentHashMap<Integer, ScalarType<?>>();
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);
@@ -14,6 +14,13 @@ public class ModifyAwareSet<E> extends ModifyAwareCollection<E> implements Set<E
super(owner, s);
}
/**
* Create as top level with it's own ModifyAwareOwner instance wrapping the given Set.
*/
public ModifyAwareSet(Set<E> s) {
super(new ModifyAwareFlag(), s);
}
@Override
public boolean isMarkedDirty() {
return owner.isMarkedDirty();
@@ -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<Object> {
public GenericObject(ObjectMapper objectMapper, Type type, int dbType, String pgType) {
super(Object.class, objectMapper, type, dbType, pgType);
}
}
/**
* Type for Sets wrapping the ObjectMapper Set as a ModifyAwareSet.
*/
private static class OmSet extends Base<Set> {
public OmSet(ObjectMapper objectMapper, Type type, int dbType, String pgType) {
super(Set.class, objectMapper, type, dbType, pgType);
}
@Override
@SuppressWarnings("unchecked")
public Set read(DataReader reader) throws SQLException {
Set value = super.read(reader);
return value == null ? null : new ModifyAwareSet(value);
}
}
/**
* Type for Lists wrapping the ObjectMapper List as a ModifyAwareList.
*/
private static class OmList extends Base<List> {
public OmList(ObjectMapper objectMapper, Type type, int dbType, String pgType) {
super(List.class, objectMapper, type, dbType, pgType);
}
@Override
@SuppressWarnings("unchecked")
public List read(DataReader reader) throws SQLException {
List value = super.read(reader);
return value == null ? null : new ModifyAwareList(value);
}
}
/**
* Type for Map wrapping the ObjectMapper Map as a ModifyAwareMap.
*/
private static class OmMap extends Base<Map> {
public OmMap(ObjectMapper objectMapper, Type type, int dbType, String pgType) {
super(Map.class, objectMapper, type, dbType, pgType);
}
@Override
@SuppressWarnings("unchecked")
public Map read(DataReader reader) throws SQLException {
Map value = super.read(reader);
return value == null ? null : new ModifyAwareMap(value);
}
}
/**
* ScalarType that uses Jackson ObjectMapper to marshall/unmarshall to/from JSON
* and storing them in one of JSON, JSONB, VARCHAR, CLOB or BLOB.
*/
private static abstract class Base<T> extends ScalarTypeBase<T> {
private final ObjectMapper objectMapper;
private final JavaType javaType;
private final String pgType;
/**
* Construct given the object mapper, property type and DB type for storage.
*
* @param objectMapper Jackson object mapper for JSON marshalling/unmarshalling
* @param type The property type (ie. type of field with @DbJson)
* @param dbType The DB type used for storage (JSON, JSONB, VARCHAR, CLOB or BLOB)
*/
public Base(Class<T> cls, ObjectMapper objectMapper, Type type, int dbType, String pgType) {
super(cls, false, dbType);
this.pgType = pgType;
this.objectMapper = objectMapper;
this.javaType = objectMapper.getTypeFactory().constructType(type);
}
/**
* Consider as a mutable type. Use the isDirty() method to check for dirty state.
*/
@Override
public boolean isMutable() {
return true;
}
/**
* Return true if the value should be considered dirty (and included in an update).
*/
@Override
public boolean isDirty(Object value) {
return !(value instanceof ModifyAwareOwner) || ((ModifyAwareOwner) value).isMarkedDirty();
}
@Override
public T read(DataReader reader) throws SQLException {
String json = reader.getString();
if (json == null || json.isEmpty()) {
return null;
}
try {
return objectMapper.readValue(json, javaType);
} catch (IOException e) {
throw new SQLException("Unable to convert JSON", e);
}
}
@Override
public void bind(DataBind bind, T value) throws SQLException {
if (pgType != null) {
String rawJson = (value == null) ? null : formatValue(value);
bind.setObject(PostgresHelper.asObject(pgType, rawJson));
} else {
if (value == null) {
bind.setNull(jdbcType);
} else {
try {
String json = objectMapper.writeValueAsString(value);
bind.setString(json);
} catch (JsonProcessingException e) {
throw new SQLException("Unable to create JSON", e);
}
}
}
}
@Override
public Object toJdbcType(Object value) {
// no type conversion supported
return value;
}
@Override
public T toBeanType(Object value) {
// no type conversion supported
return (T) value;
}
@Override
public String formatValue(T value) {
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
throw new PersistenceException("Unable to create JSON", e);
}
}
@Override
public T parse(String value) {
try {
return objectMapper.readValue(value, javaType);
} catch (IOException e) {
throw new PersistenceException("Unable to convert JSON", e);
}
}
@Override
public DocPropertyType getDocType() {
return DocPropertyType.OBJECT;
}
@Override
public boolean isDateTimeCapable() {
return false;
}
@Override
public T convertFromMillis(long dateTime) {
throw new IllegalStateException("Not supported");
}
@Override
public T jsonRead(JsonParser parser) throws IOException {
return objectMapper.readValue(parser, javaType);
}
@Override
public void jsonWrite(JsonGenerator writer, T value) throws IOException {
objectMapper.writeValue(writer, value);
}
@Override
public T readData(DataInput dataInput) throws IOException {
if (!dataInput.readBoolean()) {
return null;
} else {
return parse(dataInput.readUTF());
}
}
@Override
public void writeData(DataOutput dataOutput, T value) throws IOException {
if (value == null) {
dataOutput.writeBoolean(false);
} else {
ScalarHelp.writeUTF(dataOutput, format(value));
}
}
}
}
@@ -2,6 +2,8 @@ package com.avaje.ebeaninternal.server.type;
import com.avaje.ebeaninternal.server.type.reflect.CheckImmutableResponse;
import java.lang.reflect.Type;
/**
* Convert an Object to the required type.
*/
@@ -63,5 +65,5 @@ public interface TypeManager {
* Note that type expected to be JsonNode or Map.
* </p>
*/
ScalarType<?> getJsonScalarType(Class<?> type, int dbType, int dbLength);
ScalarType<?> getJsonScalarType(Class<?> type, int dbType, int dbLength, Type genericType);
}
@@ -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<PlainBean> beanList = new ArrayList<PlainBean>();
beanList.add(new PlainBean("one", 1));
beanList.add(new PlainBean("two", 2));
bean.setBeanList(beanList);
Set<PlainBean> beanSet = new LinkedHashSet<PlainBean>();
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<String> 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<String> 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<String> 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<String> 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);
}
}
@@ -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<PlainBean> beanSet;
@DbJson(length = 700)
List<PlainBean> beanList;
@DbJson(length = 700)
Map<String,PlainBean> beanMap = new LinkedHashMap<String, PlainBean>();
@DbJson(length = 500)
PlainBean plainBean;
@DbJson(length = 50)
Set<Long> flags = new LinkedHashSet<Long>();
@@ -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<PlainBean> getBeanSet() {
return beanSet;
}
public void setBeanSet(Set<PlainBean> beanSet) {
this.beanSet = beanSet;
}
public List<PlainBean> getBeanList() {
return beanList;
}
public void setBeanList(List<PlainBean> beanList) {
this.beanList = beanList;
}
public Map<String, PlainBean> getBeanMap() {
return beanMap;
}
public void setBeanMap(Map<String, PlainBean> beanMap) {
this.beanMap = beanMap;
}
public Long getVersion() {
return version;
}
@@ -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;
}
}