diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java index 4f927043c5..4a1499138a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java @@ -35,7 +35,7 @@ * Description of a property of a bean. Includes its deployment information such * as database column mapping information. */ -public class DeployBeanProperty { +public class DeployBeanProperty implements DeployProperty { private static final int ID_ORDER = 1000000; private static final int UNIDIRECTIONAL_ORDER = 100000; @@ -226,6 +226,11 @@ public DeployBeanDescriptor getDesc() { return desc; } + @Override + public Class getOwnerType() { + return desc.getBeanType(); + } + /** * Return the DB column length for character columns. *

@@ -258,10 +263,12 @@ public void setJsonDeserialize(boolean jsonDeserialize) { this.jsonDeserialize = jsonDeserialize; } + @Override public MutationDetection getMutationDetection() { return mutationDetection; } + @Override public void setMutationDetection(MutationDetection dirtyDetection) { this.mutationDetection = dirtyDetection; } @@ -476,8 +483,9 @@ public void setGeneratedProperty(GeneratedProperty generatedValue) { } /** - * Return true if this property is mandatory. + * Return true if this property is not mandatory. */ + @Override public boolean isNullable() { return nullable; } @@ -848,6 +856,7 @@ public Class getPropertyType() { /** * Return the generic type for this property. */ + @Override public Type getGenericType() { return genericType; } @@ -1052,6 +1061,7 @@ public A getMetaAnnotation(Class annotationType) { return null; } + @Override @SuppressWarnings("unchecked") public List getMetaAnnotations(Class annotationType) { List result = new ArrayList<>(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployProperty.java new file mode 100644 index 0000000000..0fe3a4b8f1 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployProperty.java @@ -0,0 +1,53 @@ +package io.ebeaninternal.server.deploy.meta; + +import io.ebean.annotation.MutationDetection; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.List; + +/** + * Property, with basic type information (BeanProperty and DtoProperty). + */ +public interface DeployProperty { + + /** + * Return the name of the property. + */ + String getName(); + + /** + * Return the generic type for this property. + */ + Type getGenericType(); + + /** + * Return the property type. + */ + Class getPropertyType(); + + /** + * Returns the owner class of this property. + */ + Class getOwnerType(); + + /** + * Returns the annotations on this property. + */ + List getMetaAnnotations(Class annotationType); + + /** + * Returns the mutation detection setting of this property. + */ + MutationDetection getMutationDetection(); + + /** + * Sets the mutation detection setting of this property. + */ + void setMutationDetection(MutationDetection mutationDetection); + + /** + * Return true if this property is not mandatory. + */ + boolean isNullable(); +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java index ba7937dc68..9fe23c0b1c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java @@ -224,7 +224,7 @@ private void setDbJsonType(DeployBeanProperty prop, int dbType, int dbLength, Mu /** * Return the JDBC type for the JSON storage type. */ - private int dbJsonStorage(DbJsonType dbJsonType) { + public static int dbJsonStorage(DbJsonType dbJsonType) { switch (dbJsonType) { case JSONB: return DbPlatformType.JSONB; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java index 06929cefeb..bb6dde7fa3 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java @@ -1,5 +1,7 @@ package io.ebeaninternal.server.dto; +import io.ebean.annotation.DbJson; +import io.ebean.annotation.DbJsonB; import io.ebeaninternal.api.CoreLog; import io.ebeaninternal.server.type.TypeManager; @@ -21,10 +23,16 @@ final class DtoMetaBuilder { private final Class dtoType; private final List properties = new ArrayList<>(); private final Map constructorMap = new HashMap<>(); + private final Set> annotationFilter = new HashSet<>(); DtoMetaBuilder(Class dtoType, TypeManager typeManager) { this.dtoType = dtoType; this.typeManager = typeManager; + annotationFilter.add(DbJson.class); + annotationFilter.add(DbJsonB.class); + if (typeManager.jsonMarkerAnnotation() != null) { + annotationFilter.add(typeManager.jsonMarkerAnnotation()); + } } DtoMeta build() { @@ -38,7 +46,7 @@ private void readProperties() { if (includeMethod(method)) { try { final String name = propertyName(method.getName()); - properties.add(new DtoMetaProperty(typeManager, dtoType, method, name)); + properties.add(new DtoMetaProperty(typeManager, dtoType, method, name, annotationFilter)); } catch (Exception e) { CoreLog.log.log(DEBUG, "exclude on " + dtoType + " method " + method, e); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaDeployProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaDeployProperty.java new file mode 100644 index 0000000000..d4519fe509 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaDeployProperty.java @@ -0,0 +1,81 @@ +package io.ebeaninternal.server.dto; + +import io.ebean.annotation.MutationDetection; +import io.ebeaninternal.server.deploy.meta.DeployProperty; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * DeployProperty for Dto-Properties. + * + * @author Roland Praml, FOCONIS AG + */ +class DtoMetaDeployProperty implements DeployProperty { + private final String name; + private final Class ownerType; + private final Type genericType; + private final Class propertyType; + private final Set metaAnnotations; + private final boolean nullable; + private MutationDetection mutationDetection = MutationDetection.DEFAULT; + + DtoMetaDeployProperty(String name, Class ownerType, Type genericType, Class propertyType, Set metaAnnotations, Method method) { + this.name = name; + this.ownerType = ownerType; + this.genericType = genericType; + this.nullable = !propertyType.isPrimitive(); + this.propertyType = propertyType; + this.metaAnnotations = metaAnnotations; + } + + @Override + public String getName() { + return name; + } + + @Override + public Type getGenericType() { + return genericType; + } + + @Override + public Class getPropertyType() { + return propertyType; + } + + @Override + public Class getOwnerType() { + return ownerType; + } + + @Override + public List getMetaAnnotations(Class annotationType) { + List result = new ArrayList<>(); + for (Annotation ann : metaAnnotations) { + if (ann.annotationType() == annotationType) { + result.add((A) ann); + } + } + return result; + } + + @Override + public MutationDetection getMutationDetection() { + return mutationDetection; + } + + @Override + public void setMutationDetection(MutationDetection mutationDetection) { + this.mutationDetection = mutationDetection; + } + + @Override + public boolean isNullable() { + return nullable; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java index 680c0095b6..6a052fe05c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java @@ -1,15 +1,26 @@ package io.ebeaninternal.server.dto; +import io.ebean.annotation.DbJson; +import io.ebean.annotation.DbJsonB; +import io.ebean.config.dbplatform.DbPlatformType; import io.ebean.core.type.DataReader; import io.ebean.core.type.ScalarType; +import io.ebean.util.AnnotationUtil; +import io.ebeaninternal.server.deploy.meta.DeployProperty; +import io.ebeaninternal.server.deploy.parse.DeployUtil; import io.ebeaninternal.server.type.TypeManager; +import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.sql.SQLException; +import java.util.Collections; +import java.util.List; +import java.util.Set; final class DtoMetaProperty implements DtoReadSet { @@ -20,18 +31,74 @@ final class DtoMetaProperty implements DtoReadSet { private final MethodHandle setter; private final ScalarType scalarType; - DtoMetaProperty(TypeManager typeManager, Class dtoType, Method writeMethod, String name) throws IllegalAccessException, NoSuchMethodException { + DtoMetaProperty(TypeManager typeManager, Class dtoType, Method writeMethod, String name, Set> annotationFilter) + throws IllegalAccessException, NoSuchMethodException { this.dtoType = dtoType; this.name = name; if (writeMethod != null) { this.setter = lookupMethodHandle(dtoType, writeMethod); - this.scalarType = typeManager.type(propertyType(writeMethod), propertyClass(writeMethod)); + Field field = findField(dtoType, name); + DeployProperty deployProp = new DtoMetaDeployProperty(name, + dtoType, + propertyType(writeMethod), + propertyClass(writeMethod), + field == null ? Collections.emptySet() : AnnotationUtil.metaFindAllFor(field, annotationFilter), + writeMethod); + scalarType = getScalarType(typeManager, deployProp); } else { this.scalarType = null; this.setter = null; } } + private ScalarType getScalarType(TypeManager typeManager, DeployProperty deployProp) { + final ScalarType scalarType; + + List json = deployProp.getMetaAnnotations(DbJson.class); + if (!json.isEmpty()) { + return typeManager.dbJsonType(deployProp, DeployUtil.dbJsonStorage(json.get(0).storage()), json.get(0).length()); + } + List jsonB = deployProp.getMetaAnnotations(DbJsonB.class); + if (!jsonB.isEmpty()) { + return typeManager.dbJsonType(deployProp, DbPlatformType.JSONB, jsonB.get(0).length()); + } + if (typeManager.jsonMarkerAnnotation() != null + && !deployProp.getMetaAnnotations(typeManager.jsonMarkerAnnotation()).isEmpty()) { + return typeManager.dbJsonType(deployProp, DbPlatformType.JSON, 0); + } + return typeManager.type(deployProp); + + + } + + /** + * Find all annotations on fields and methods. + */ + private Set findMetaAnnotations(Class dtoType, Method writeMethod, String name, Set> annotationFilter) { + Field field = findField(dtoType, name); + if (field != null) { + Set metaAnnotations = AnnotationUtil.metaFindAllFor(field, annotationFilter); + metaAnnotations.addAll(AnnotationUtil.metaFindAllFor(writeMethod, annotationFilter)); + return metaAnnotations; + } else { + return AnnotationUtil.metaFindAllFor(writeMethod, annotationFilter); + } + } + + /** + * Find field in class with same name + */ + private Field findField(Class type, String name) { + while (type != Object.class && type != null) { + try { + return dtoType.getDeclaredField(name); + } catch (NoSuchFieldException e) { + type = type.getSuperclass(); + } + } + return null; + } + private static MethodHandle lookupMethodHandle(Class dtoType, Method method) throws NoSuchMethodException, IllegalAccessException { return LOOKUP.findVirtual(dtoType, method.getName(), MethodType.methodType(method.getReturnType(), method.getParameterTypes())); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java index fd50c145b2..141f81a020 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java @@ -18,10 +18,12 @@ import io.ebeaninternal.server.core.ServiceUtil; import io.ebeaninternal.server.core.bootup.BootupClasses; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; +import io.ebeaninternal.server.deploy.meta.DeployProperty; import javax.persistence.AttributeConverter; import javax.persistence.EnumType; import java.io.File; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; @@ -191,7 +193,8 @@ public ScalarType type(int jdbcType) { } @Override - public ScalarType type(Type propertyType, Class propertyClass) { + public ScalarType type(DeployProperty prop) { + Type propertyType = prop.getGenericType(); if (propertyType instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) propertyType; Type rawType = pt.getRawType(); @@ -199,7 +202,7 @@ public ScalarType type(Type propertyType, Class propertyClass) { return dbArrayType((Class) rawType, propertyType, true); } } - return type(propertyClass); + return type(prop.getPropertyType()); } /** @@ -297,8 +300,14 @@ private boolean isEnumType(Type valueType) { return TypeReflectHelper.isEnumType(valueType); } + @Override - public ScalarType dbJsonType(DeployBeanProperty prop, int dbType, int dbLength) { + public Class jsonMarkerAnnotation() { + return jsonMapper == null ? null : jsonMapper.markerAnnotation(); + } + + @Override + public ScalarType dbJsonType(DeployProperty prop, int dbType, int dbLength) { Class type = prop.getPropertyType(); if (type.equals(String.class)) { return ScalarTypeJsonString.typeFor(postgres, dbType); @@ -328,14 +337,14 @@ public ScalarType dbJsonType(DeployBeanProperty prop, int dbType, int dbLengt return createJsonObjectMapperType(prop, dbType, DocPropertyType.OBJECT); } - private boolean keepSource(DeployBeanProperty prop) { + private boolean keepSource(DeployProperty prop) { if (prop.getMutationDetection() == MutationDetection.DEFAULT) { prop.setMutationDetection(jsonManager.mutationDetection()); } return prop.getMutationDetection() == MutationDetection.SOURCE; } - private DocPropertyType docPropertyType(DeployBeanProperty prop, Class type) { + private DocPropertyType docPropertyType(DeployProperty prop, Class type) { return type.equals(List.class) || type.equals(Set.class) ? docType(prop.getGenericType()) : DocPropertyType.OBJECT; } @@ -369,14 +378,18 @@ private boolean isMapValueTypeObject(Type genericType) { return Object.class.equals(typeArgs[1]) || "?".equals(typeArgs[1].toString()); } - private ScalarType createJsonObjectMapperType(DeployBeanProperty prop, int dbType, DocPropertyType docType) { + private ScalarType createJsonObjectMapperType(DeployProperty prop, int dbType, DocPropertyType docType) { if (jsonMapper == null) { throw new IllegalArgumentException("Unsupported @DbJson mapping - Jackson ObjectMapper not present for " + prop); } if (MutationDetection.DEFAULT == prop.getMutationDetection()) { prop.setMutationDetection(jsonManager.mutationDetection()); } - var req = new ScalarJsonRequest(jsonManager, dbType, docType, prop.getDesc().getBeanType(), prop.getMutationDetection(), prop.getName()); + Class type = prop.getOwnerType(); + if (prop instanceof DeployBeanProperty) { + type = ((DeployBeanProperty) prop).getField().getDeclaringClass(); + } + var req = new ScalarJsonRequest(jsonManager, dbType, docType, type, prop.getMutationDetection(), prop.getName()); return jsonMapper.createType(req); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeManager.java index e683b43e61..828174a677 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeManager.java @@ -2,8 +2,10 @@ import io.ebean.core.type.ScalarType; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; +import io.ebeaninternal.server.deploy.meta.DeployProperty; import javax.persistence.EnumType; +import java.lang.annotation.Annotation; import java.lang.reflect.Type; /** @@ -40,20 +42,25 @@ public interface TypeManager { *

* For example Array based ScalarType for types like {@code List}. */ - ScalarType type(Type propertyType, Class type); + ScalarType type(DeployProperty property); /** * Create a ScalarType for an Enum using a mapping (rather than JPA Ordinal or String which has limitations). */ ScalarType enumType(Class> enumType, EnumType enumerated); + /** + * Returns the Json Marker annotation (e.g. JacksonAnnotation) + */ + Class jsonMarkerAnnotation(); + /** * Return the ScalarType used to handle JSON content. *

* Note that type expected to be JsonNode or Map. *

*/ - ScalarType dbJsonType(DeployBeanProperty prop, int dbType, int dbLength); + ScalarType dbJsonType(DeployProperty prop, int dbType, int dbLength); /** * Return the ScalarType used to handle DB ARRAY. diff --git a/ebean-test/src/test/java/org/tests/json/TestDbJson_Jackson.java b/ebean-test/src/test/java/org/tests/json/TestDbJson_Jackson.java index 47fbfdc582..4826e3873e 100644 --- a/ebean-test/src/test/java/org/tests/json/TestDbJson_Jackson.java +++ b/ebean-test/src/test/java/org/tests/json/TestDbJson_Jackson.java @@ -1,15 +1,25 @@ package org.tests.json; import com.fasterxml.jackson.databind.ObjectMapper; -import io.ebean.xtest.BaseTestCase; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import io.ebean.DB; +import io.ebean.annotation.DbJson; +import io.ebean.annotation.DbJsonB; +import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.Test; +import org.tests.model.json.BasicJacksonType; import org.tests.model.json.EBasicJsonJackson; import org.tests.model.json.EBasicJsonJackson2; import org.tests.model.json.LongJacksonType; import org.tests.model.json.StringJacksonType; import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; @@ -59,9 +69,56 @@ public void testJsonDeserializeAnnotation() throws IOException { assertThat(found.getValueMap()).containsEntry(1L, "one").containsEntry(2L, "two"); } + public static class DtoJackson { + @DbJson(length = 700) + Set> valueSet = new LinkedHashSet<>(); + + @DbJsonB + List> valueList = new ArrayList<>(); + + @DbJson(length = 700) + @JsonDeserialize(keyAs = Long.class) + Map> valueMap = new LinkedHashMap<>(); + + @DbJson(length = 500) + BasicJacksonType plainValue; + + public Set> getValueSet() { + return valueSet; + } + + public void setValueSet(Set> valueSet) { + this.valueSet = valueSet; + } + + public List> getValueList() { + return valueList; + } + + public void setValueList(List> valueList) { + this.valueList = valueList; + } + + public Map> getValueMap() { + return valueMap; + } + + public void setValueMap(Map> valueMap) { + this.valueMap = valueMap; + } + + public BasicJacksonType getPlainValue() { + return plainValue; + } + + public void setPlainValue(BasicJacksonType plainValue) { + this.plainValue = plainValue; + } + } + /** * This testcase verifies if polymorph objects will work in ebean. - * + *

* for BasicJacksonType there exists two types and has a @JsonTypeInfo * annotation. It is expected that this information is also honored by ebean. */ @@ -94,7 +151,8 @@ public void testPolymorph() throws IOException { assertThat(found.getPlainValue()).isInstanceOf(LongJacksonType.class); assertThat(found.getValueList()).hasSize(2); assertThat(found.getValueSet()).hasSize(2); - assertThat(found.getValueMap()).hasSize(2);; + assertThat(found.getValueMap()).hasSize(2); + ; DB.save(bean); @@ -103,7 +161,16 @@ public void testPolymorph() throws IOException { assertThat(found.getPlainValue()).isInstanceOf(LongJacksonType.class); assertThat(found.getValueList()).hasSize(2); assertThat(found.getValueSet()).hasSize(2); - assertThat(found.getValueMap()).hasSize(2);; + assertThat(found.getValueMap()).hasSize(2); + + + DtoJackson dto = DB.find(EBasicJsonJackson2.class).setId(bean.getId()) + .select("valueSet,valueList,valueMap,plainValue").asDto(DtoJackson.class).findOne(); + + assertThat(dto.getPlainValue()).isInstanceOf(LongJacksonType.class); + assertThat(dto.getValueList()).hasSize(2); + assertThat(dto.getValueSet()).hasSize(2); + assertThat(dto.getValueMap()).hasSize(2); }