#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);
}