mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
#542 - ENH: @DbJson fields are auto serialized and deserialized to specified classes.
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user