Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
import io.ebeaninternal.server.deploy.ManyType;
import io.ebeaninternal.server.deploy.meta.*;
import io.ebeaninternal.server.type.TypeManager;
import io.ebeaninternal.server.type.TypeReflectHelper;
import jakarta.persistence.Convert;
import jakarta.persistence.PersistenceException;
import jakarta.persistence.Transient;

import jakarta.persistence.*;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

import static java.lang.System.Logger.Level.*;
Expand All @@ -37,7 +39,11 @@ public DeployCreateProperties(TypeManager typeManager) {
* Create the appropriate properties for a bean.
*/
public void createProperties(DeployBeanDescriptor<?> desc) {
createProperties(desc, desc.getBeanType(), 0, new HashMap<>());
// Build the full type-variable map once for the entire hierarchy. TypeReflectHelper
// (via TypeResolver) walks superclasses and interfaces, composing mappings so that
// multi-level generic hierarchies (A extends B<T>, B<T> extends C<T>) resolve correctly.
Map<TypeVariable<?>, Type> typeMap = TypeReflectHelper.typeVariableMap(desc.getBeanType());
createProperties(desc, desc.getBeanType(), 0, typeMap);
desc.sortProperties();
}

Expand All @@ -63,10 +69,14 @@ private boolean ignoreField(Field field) {
}

/**
* properties the bean properties from Class. Some of these properties may not map to database
* columns.
* Create the bean properties from Class. Some of these properties may not map to database columns.
*/
private void createProperties(DeployBeanDescriptor<?> desc, Class<?> beanType, int level, Map<TypeVariable<?>, Class<?>> genericTypeMap) {
private void createProperties(
DeployBeanDescriptor<?> desc,
Class<?> beanType,
int level,
Map<TypeVariable<?>, Type> typeMap
) {
if (beanType.equals(Model.class)) {
// ignore all fields on model (_$dbName)
return;
Expand All @@ -76,7 +86,7 @@ private void createProperties(DeployBeanDescriptor<?> desc, Class<?> beanType, i
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
if (!ignoreField(field)) {
DeployBeanProperty prop = createProp(desc, field, beanType, genericTypeMap);
DeployBeanProperty prop = createProp(desc, field, beanType, typeMap);
if (prop != null) {
// set a order that gives priority to inherited properties
// push Id/EmbeddedId up and CreatedTimestamp/UpdatedTimestamp down
Expand All @@ -96,9 +106,10 @@ private void createProperties(DeployBeanDescriptor<?> desc, Class<?> beanType, i
Class<?> superClass = beanType.getSuperclass();
if (!superClass.equals(Object.class)) {
// recursively add any properties in the inheritance hierarchy
// up to the Object.class level...
createProperties(desc, superClass, level + 1, mapGenerics(beanType));
// up to the Object.class level - the same typeMap covers the full hierarchy
createProperties(desc, superClass, level + 1, typeMap);
}

} catch (PersistenceException ex) {
throw ex;
} catch (Exception ex) {
Expand All @@ -118,18 +129,28 @@ private DeployBeanProperty createManyType(DeployBeanDescriptor<?> desc, Class<?>
return new DeployBeanPropertyAssocMany<>(desc, targetType, manyType);
}

private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Map<TypeVariable<?>, Class<?>> genericTypeMap) {
Class<?> propertyType = field.getGenericType() instanceof TypeVariable<?>
? genericTypeMap.get(field.getGenericType())
: field.getType();
private DeployBeanProperty createProp(
DeployBeanDescriptor<?> desc,
Field field,
Map<TypeVariable<?>, Type> typeMap
) {
// Resolve the field's generic type through the accumulated type-variable map.
// This handles TypeVariables from generic superclasses at any depth in the hierarchy.
Type resolvedGenericType = TypeReflectHelper.resolveType(field.getGenericType(), typeMap);
Class<?> propertyType = TypeReflectHelper.resolveToClass(resolvedGenericType);
if (propertyType == null) {
propertyType = field.getType();
}

if (isSpecialScalarType(field)) {
return new DeployBeanProperty(desc, propertyType, field.getGenericType());
return new DeployBeanProperty(desc, propertyType, resolvedGenericType);
}

// check for Collection type (list, set or map)
ManyType manyType = determineManyType.manyType(propertyType);
if (manyType != null) {
// List, Set or Map based object
Class<?> targetType = determineTargetType(field);
Class<?> targetType = determineTargetType(resolvedGenericType);
if (targetType == null) {
if (AnnotationUtil.has(field, Transient.class)) {
// not supporting this field (generic type used)
Expand All @@ -139,20 +160,25 @@ private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field,
}
return createManyType(desc, targetType, manyType);
}

if (propertyType.isEnum() || propertyType.isPrimitive()) {
return new DeployBeanProperty(desc, propertyType, null, null);
}

ScalarType<?> scalarType = typeManager.type(propertyType);
if (scalarType != null) {
return new DeployBeanProperty(desc, propertyType, scalarType, null);
}

if (isTransientField(field)) {
// return with no ScalarType (still support JSON features)
return new DeployBeanProperty(desc, propertyType, null, null);
}

if (AnnotationUtil.has(field, Convert.class)) {
throw new IllegalStateException("No AttributeConverter registered for type " + propertyType + " at " + desc.getFullName() + "." + field.getName());
}

try {
return new DeployBeanPropertyAssocOne<>(desc, propertyType);
} catch (Exception e) {
Expand All @@ -176,8 +202,8 @@ private boolean isTransientField(Field field) {
return AnnotationUtil.has(field, Transient.class);
}

private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Class<?> beanType, Map<TypeVariable<?>, Class<?>> genericTypeMap) {
DeployBeanProperty prop = createProp(desc, field, genericTypeMap);
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Class<?> beanType, Map<TypeVariable<?>, Type> typeMap) {
DeployBeanProperty prop = createProp(desc, field, typeMap);
if (prop == null) {
// transient annotation on unsupported type
return null;
Expand All @@ -193,75 +219,21 @@ private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field,
* Determine the type of the List,Set or Map. Not been set explicitly so determine this from
* ParameterizedType.
*/
private Class<?> determineTargetType(Field field) {
Type genType = field.getGenericType();
private Class<?> determineTargetType(Type genType) {
if (genType instanceof ParameterizedType) {
ParameterizedType ptype = (ParameterizedType) genType;
Type[] typeArgs = ptype.getActualTypeArguments();
if (typeArgs.length == 1) {
// expecting set or list
if (typeArgs[0] instanceof Class<?>) {
return (Class<?>) typeArgs[0];
}
if (typeArgs[0] instanceof WildcardType) {
final Type[] upperBounds = ((WildcardType) typeArgs[0]).getUpperBounds();
if (upperBounds.length == 1 && upperBounds[0] instanceof Class<?>) {
// kotlin generated wildcard type
return (Class<?>) upperBounds[0];
}
}
// throw new RuntimeException("Unexpected Parameterised Type? "+typeArgs[0]);
return null;
return TypeReflectHelper.resolveCollectionTarget(typeArgs[0]);
}
if (typeArgs.length == 2) {
// this is probably a Map
if (typeArgs[1] instanceof ParameterizedType) {
// not supporting ParameterizedType on Map.
return null;
}
if (typeArgs[1] instanceof WildcardType) {
return Object.class;
}
return (Class<?>) typeArgs[1];
return TypeReflectHelper.resolveCollectionTarget(typeArgs[1]);
}
}
// if targetType is null, then must be set in annotations
return null;
}

private Map<TypeVariable<?>, Class<?>> mapGenerics(Class<?> clazz) {
Type genericSuperclass = clazz.getGenericSuperclass();
if (!(genericSuperclass instanceof ParameterizedType)) {
return new HashMap<>();
}

ParameterizedType parameterized = (ParameterizedType) genericSuperclass;
TypeVariable<?>[] typeVars = ((Class<?>) parameterized.getRawType()).getTypeParameters();
Type[] actualTypes = parameterized.getActualTypeArguments();

Map<TypeVariable<?>, Class<?>> typeMap = new HashMap<>();
for (int i = 0; i < typeVars.length; i++) {
Type actual = actualTypes[i];
Class<?> resolvedClass = resolveToClass(actual);
if (resolvedClass != null) {
typeMap.put(typeVars[i], resolvedClass);
} else {
// ignore
}
}
return typeMap;
}

private static Class<?> resolveToClass(Type type) {
if (type instanceof Class<?>) {
return (Class<?>) type;
} else if (type instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) type;
Type raw = pType.getRawType();
if (raw instanceof Class<?>) {
return (Class<?>) raw;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.Map;

public final class TypeReflectHelper {

/**
* Return the full map of TypeVariable to resolved Type for the given class and its entire
* superclass/interface hierarchy. Used to resolve generic field types in mapped superclasses.
*/
public static Map<TypeVariable<?>, Type> typeVariableMap(Class<?> targetType) {
return TypeResolver.getTypeVariableMap(targetType);
}

public static Class<?>[] getParams(Class<?> cls, Class<?> matchRawType) {
return TypeResolver.resolveRawArgs(matchRawType, cls);
}
Expand Down Expand Up @@ -53,6 +63,85 @@ public static Type getValueType(Type collectionType) {
return typeArgs[0];
}

/**
* Return the raw Class for a Type, handling Class, ParameterizedType, and WildcardType.
* Returns null when the raw class cannot be determined.
*/
public static Class<?> resolveToClass(Type type) {
if (type instanceof Class) {
return (Class<?>) type;
}
if (type instanceof ParameterizedType) {
Type raw = ((ParameterizedType) type).getRawType();
if (raw instanceof Class) {
return (Class<?>) raw;
}
}
if (type instanceof WildcardType) {
Type[] upperBounds = ((WildcardType) type).getUpperBounds();
if (upperBounds.length == 1) {
return resolveToClass(upperBounds[0]);
}
}
return null;
}

/**
* Return the element class for a collection or map type argument (handles Class,
* ParameterizedType raw, and Kotlin-generated wildcard upper bounds).
*/
public static Class<?> resolveCollectionTarget(Type typeArg) {
if (typeArg instanceof Class) {
return (Class<?>) typeArg;
}
if (typeArg instanceof ParameterizedType) {
Type rawType = ((ParameterizedType) typeArg).getRawType();
if (rawType instanceof Class) {
return (Class<?>) rawType;
}
return null;
}
if (typeArg instanceof WildcardType) {
// kotlin generated wildcard type
Type[] upperBounds = ((WildcardType) typeArg).getUpperBounds();
if (upperBounds.length == 1) {
return resolveToClass(upperBounds[0]);
}
}
return null;
}

/**
* Resolve a Type through the given type-variable map, recursively substituting any bound
* TypeVariables and rebuilding ParameterizedTypes whose arguments changed.
*/
public static Type resolveType(Type type, Map<TypeVariable<?>, Type> typeMap) {
if (type instanceof TypeVariable) {
TypeVariable<?> typeVariable = (TypeVariable<?>) type;
Type resolved = typeMap.get(typeVariable);
return resolved != null ? resolveType(resolved, typeMap) : typeVariable;
}
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Type[] actualArgs = parameterizedType.getActualTypeArguments();
Type[] resolvedArgs = new Type[actualArgs.length];
boolean changed = false;
for (int i = 0; i < actualArgs.length; i++) {
resolvedArgs[i] = resolveType(actualArgs[i], typeMap);
changed |= resolvedArgs[i] != actualArgs[i];
}
if (!changed) {
return parameterizedType;
}
return new ResolvedParameterizedType(
parameterizedType.getOwnerType(),
parameterizedType.getRawType(),
resolvedArgs
);
}
return type;
}

private static Class<?> getClass(Type type) {
while (true) {
if (type instanceof ParameterizedType) {
Expand All @@ -69,4 +158,31 @@ private static Class<?> getClass(Type type) {
return (Class<?>) type;
}
}

private static final class ResolvedParameterizedType implements ParameterizedType {
private final Type ownerType;
private final Type rawType;
private final Type[] actualTypeArguments;

ResolvedParameterizedType(Type ownerType, Type rawType, Type[] actualTypeArguments) {
this.ownerType = ownerType;
this.rawType = rawType;
this.actualTypeArguments = actualTypeArguments;
}

@Override
public Type[] getActualTypeArguments() {
return actualTypeArguments.clone();
}

@Override
public Type getRawType() {
return rawType;
}

@Override
public Type getOwnerType() {
return ownerType;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ private static Class<?> resolveRawClass(Type genericType, Class<?> subType) {
return genericType instanceof Class ? (Class<?>) genericType : Unknown.class;
}

private static Map<TypeVariable<?>, Type> getTypeVariableMap(final Class<?> targetType) {
static Map<TypeVariable<?>, Type> getTypeVariableMap(final Class<?> targetType) {
Map<TypeVariable<?>, Type> map = new HashMap<>();
// Populate interfaces
populateSuperTypeArgs(targetType.getGenericInterfaces(), map);
Expand Down
Loading
Loading