From d977e749ff6e1c126a9f2d5881b3026adc6fc942 Mon Sep 17 00:00:00 2001 From: dferreira Date: Fri, 10 Oct 2025 15:54:43 +0100 Subject: [PATCH 1/2] creates support for generics in mapped superclasses --- .../generator/ProcessingContext.java | 199 ++++++++++++++++-- 1 file changed, 186 insertions(+), 13 deletions(-) diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java index 112f6737f1..c5612ed4bd 100644 --- a/querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java @@ -11,10 +11,12 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; @@ -28,6 +30,7 @@ import java.io.Reader; import java.nio.file.NoSuchFileException; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -88,6 +91,11 @@ class ProcessingContext implements Constants { */ private String factoryPackage; + /** + * Cache of resolved generic type parameters for mapped superclasses. + */ + private final Map> genericTypeCache = new HashMap<>(); + ProcessingContext(ProcessingEnvironment processingEnv) { this.processingEnv = processingEnv; this.typeUtils = processingEnv.getTypeUtils(); @@ -122,29 +130,70 @@ TypeElement componentAnnotation() { */ List allFields(Element element) { List list = new ArrayList<>(); - gatherProperties(list, element); + gatherProperties(list, element, null); return list; } /** * Recursively gather all the fields (properties) for the given bean element. */ - private void gatherProperties(List fields, Element element) { + private void gatherProperties(List fields, Element element, Map typeParameterMap) { TypeElement typeElement = (TypeElement) element; TypeMirror superclass = typeElement.getSuperclass(); Element mappedSuper = typeUtils.asElement(superclass); if (isMappedSuperOrInheritance(mappedSuper)) { - gatherProperties(fields, mappedSuper); + // Resolve generic type parameters for the superclass + Map superTypeParameterMap = resolveGenericTypes(superclass, typeParameterMap); + gatherProperties(fields, mappedSuper, superTypeParameterMap); } List allFields = ElementFilter.fieldsIn(element.getEnclosedElements()); for (VariableElement field : allFields) { if (!ignoreField(field)) { - fields.add(field); + // Create a wrapper that holds both the field and its resolved type context + if (typeParameterMap != null && !typeParameterMap.isEmpty()) { + fields.add(new ResolvedVariableElement(field, typeParameterMap)); + } else { + fields.add(field); + } } } } + /** + * Wrapper class to hold a VariableElement along with its type parameter resolution context. + * This allows us to pass resolved generic type information along with field elements. + */ + private static class ResolvedVariableElement implements VariableElement { + private final VariableElement delegate; + private final Map typeParameterMap; + + ResolvedVariableElement(VariableElement delegate, Map typeParameterMap) { + this.delegate = delegate; + this.typeParameterMap = new HashMap<>(typeParameterMap); + } + + public Map getTypeParameterMap() { + return typeParameterMap; + } + + // Delegate all VariableElement methods to the original element + @Override public TypeMirror asType() { return delegate.asType(); } + @Override public ElementKind getKind() { return delegate.getKind(); } + @Override public Set getModifiers() { return delegate.getModifiers(); } + @Override public javax.lang.model.element.Name getSimpleName() { return delegate.getSimpleName(); } + @Override public Element getEnclosingElement() { return delegate.getEnclosingElement(); } + @Override public List getEnclosedElements() { return delegate.getEnclosedElements(); } + @Override public List getAnnotationMirrors() { return delegate.getAnnotationMirrors(); } + @Override public A getAnnotation(Class annotationType) { return delegate.getAnnotation(annotationType); } + @Override public A[] getAnnotationsByType(Class annotationType) { return delegate.getAnnotationsByType(annotationType); } + @Override public Object getConstantValue() { return delegate.getConstantValue(); } + @Override public R accept(javax.lang.model.element.ElementVisitor v, P p) { return delegate.accept(v, p); } + @Override public String toString() { return delegate.toString(); } + @Override public boolean equals(Object obj) { return delegate.equals(obj); } + @Override public int hashCode() { return delegate.hashCode(); } + } + /** * Not interested in static, transient or Ebean internal fields. */ @@ -247,7 +296,118 @@ private String trimAnnotations(String type) { return trimAnnotations(remainder); } + /** + * Resolve generic type parameters for a superclass in the context of a subclass. + * This method maps generic type parameters from the superclass to their actual types + * as specified in the subclass declaration. + * + * @param superclassType The superclass type mirror (may be parameterized) + * @param parentTypeParameterMap Existing type parameter mappings from parent context + * @return A map of type parameter names to their resolved TypeMirror instances + */ + private Map resolveGenericTypes(TypeMirror superclassType, Map parentTypeParameterMap) { + Map typeParameterMap = new HashMap<>(); + + // If we have parent type parameter mappings, inherit them + if (parentTypeParameterMap != null) { + typeParameterMap.putAll(parentTypeParameterMap); + } + + if (superclassType.getKind() != TypeKind.DECLARED) { + return typeParameterMap; + } + + DeclaredType declaredSuperclass = (DeclaredType) superclassType; + TypeElement superclassElement = (TypeElement) declaredSuperclass.asElement(); + + // Get the type parameters from the superclass definition + List typeParameters = superclassElement.getTypeParameters(); + + // Get the actual type arguments used in this specific inheritance + List typeArguments = declaredSuperclass.getTypeArguments(); + + // Map each type parameter to its actual type + for (int i = 0; i < typeParameters.size() && i < typeArguments.size(); i++) { + String parameterName = typeParameters.get(i).getSimpleName().toString(); + TypeMirror actualType = typeArguments.get(i); + + // If the actual type is itself a type variable, try to resolve it from parent context + if (actualType.getKind() == TypeKind.TYPEVAR && parentTypeParameterMap != null) { + TypeVariable typeVar = (TypeVariable) actualType; + String varName = typeVar.asElement().getSimpleName().toString(); + TypeMirror resolvedType = parentTypeParameterMap.get(varName); + if (resolvedType != null) { + actualType = resolvedType; + } + } + + typeParameterMap.put(parameterName, actualType); + } + + // Cache the resolved types for performance + String cacheKey = superclassElement.getQualifiedName().toString(); + genericTypeCache.put(cacheKey, new HashMap<>(typeParameterMap)); + + return typeParameterMap; + } + + /** + * Resolve a field's type in the context of generic type parameters. + * If the field type is a type variable, resolve it to the actual type. + * + * @param fieldType The original field type + * @param typeParameterMap The resolved type parameter mappings + * @return The resolved type mirror, or the original type if no resolution needed + */ + private TypeMirror resolveFieldType(TypeMirror fieldType, Map typeParameterMap) { + if (typeParameterMap == null || typeParameterMap.isEmpty()) { + return fieldType; + } + + if (fieldType.getKind() == TypeKind.TYPEVAR) { + TypeVariable typeVar = (TypeVariable) fieldType; + String parameterName = typeVar.asElement().getSimpleName().toString(); + TypeMirror resolvedType = typeParameterMap.get(parameterName); + if (resolvedType != null) { + return resolvedType; + } + } + + // Handle parameterized types (e.g., List where T needs resolution) + if (fieldType.getKind() == TypeKind.DECLARED) { + DeclaredType declaredType = (DeclaredType) fieldType; + List typeArguments = declaredType.getTypeArguments(); + + if (!typeArguments.isEmpty()) { + List resolvedArguments = new ArrayList<>(); + boolean hasChanges = false; + + for (TypeMirror typeArg : typeArguments) { + TypeMirror resolvedArg = resolveFieldType(typeArg, typeParameterMap); + resolvedArguments.add(resolvedArg); + if (resolvedArg != typeArg) { + hasChanges = true; + } + } + + if (hasChanges) { + // Create a new DeclaredType with resolved type arguments + TypeElement typeElement = (TypeElement) declaredType.asElement(); + return typeUtils.getDeclaredType(typeElement, resolvedArguments.toArray(new TypeMirror[0])); + } + } + } + + return fieldType; + } + PropertyType getPropertyType(VariableElement field) { + // Check if this is a resolved field from a generic mapped superclass + Map typeParameterMap = null; + if (field instanceof ResolvedVariableElement) { + typeParameterMap = ((ResolvedVariableElement) field).getTypeParameterMap(); + } + boolean toMany = dbToMany(field); if (dbJsonField(field)) { return propertyTypeMap.getDbJsonType(); @@ -255,10 +415,17 @@ PropertyType getPropertyType(VariableElement field) { if (dbArrayField(field)) { // get generic parameter type DeclaredType declaredType = (DeclaredType) field.asType(); - String fullType = typeDef(declaredType.getTypeArguments().get(0)); + TypeMirror arrayElementType = declaredType.getTypeArguments().get(0); + // Resolve the array element type if it's generic + TypeMirror resolvedElementType = resolveFieldType(arrayElementType, typeParameterMap); + String fullType = typeDef(resolvedElementType); return new PropertyTypeArray(fullType, Split.shortName(fullType)); } - final TypeMirror typeMirror = field.asType(); + + // Get the field type, potentially resolved if it's generic + final TypeMirror originalTypeMirror = field.asType(); + final TypeMirror typeMirror = resolveFieldType(originalTypeMirror, typeParameterMap); + TypeMirror currentType = typeMirror; while (currentType != null) { PropertyType type = propertyTypeMap.getType(typeDef(currentType)); @@ -278,7 +445,7 @@ PropertyType getPropertyType(VariableElement field) { // workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=544288 fieldType = elementUtils.getTypeElement(fieldType.toString()); - if (fieldType.getKind() == ElementKind.ENUM) { + if (fieldType != null && fieldType.getKind() == ElementKind.ENUM) { String fullType = typeDef(typeMirror); return new PropertyTypeEnum(fullType, Split.shortName(fullType)); } @@ -301,7 +468,7 @@ PropertyType getPropertyType(VariableElement field) { final PropertyType result; if (typeMirror.getKind() == TypeKind.DECLARED) { - result = createManyTypeAssoc(field, (DeclaredType) typeMirror); + result = createManyTypeAssoc(field, (DeclaredType) typeMirror, typeParameterMap); } else { result = null; } @@ -333,20 +500,26 @@ private boolean typeInstanceOf(final TypeMirror typeMirror, final CharSequence d .anyMatch(t -> typeInstanceOf(t, desiredInterface)); } - private PropertyType createManyTypeAssoc(VariableElement field, DeclaredType declaredType) { + private PropertyType createManyTypeAssoc(VariableElement field, DeclaredType declaredType, Map typeParameterMap) { boolean toMany = dbToMany(field); List typeArguments = declaredType.getTypeArguments(); if (typeArguments.size() == 1) { - Element argElement = typeUtils.asElement(typeArguments.get(0)); + TypeMirror argType = typeArguments.get(0); + // Resolve the type argument if it's generic + TypeMirror resolvedArgType = resolveFieldType(argType, typeParameterMap); + Element argElement = typeUtils.asElement(resolvedArgType); if (isEntityOrEmbedded(argElement)) { boolean embeddable = isEmbeddable(argElement); - return createPropertyTypeAssoc(embeddable, toMany, typeDef(argElement.asType())); + return createPropertyTypeAssoc(embeddable, toMany, typeDef(resolvedArgType)); } } else if (typeArguments.size() == 2) { - Element argElement = typeUtils.asElement(typeArguments.get(1)); + TypeMirror argType = typeArguments.get(1); + // Resolve the type argument if it's generic + TypeMirror resolvedArgType = resolveFieldType(argType, typeParameterMap); + Element argElement = typeUtils.asElement(resolvedArgType); if (isEntityOrEmbedded(argElement)) { boolean embeddable = isEmbeddable(argElement); - return createPropertyTypeAssoc(embeddable, toMany, typeDef(argElement.asType())); + return createPropertyTypeAssoc(embeddable, toMany, typeDef(resolvedArgType)); } } return null; From e8e7d4b9b96b9e97e313898c4b7f71aaaed6cadf Mon Sep 17 00:00:00 2001 From: dferreira Date: Wed, 15 Oct 2025 15:04:15 +0100 Subject: [PATCH 2/2] Changes DeployCreateProperties to allow processing of inheritance hierarchies with generics --- .../deploy/parse/DeployCreateProperties.java | 91 +++++++++++++++---- 1 file changed, 73 insertions(+), 18 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java index 4e887bbbc8..58969bb8c7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java @@ -11,6 +11,8 @@ import jakarta.persistence.*; import java.lang.reflect.*; +import java.util.HashMap; +import java.util.Map; import static java.lang.System.Logger.Level.*; @@ -35,7 +37,7 @@ public DeployCreateProperties(TypeManager typeManager) { * Create the appropriate properties for a bean. */ public void createProperties(DeployBeanDescriptor desc) { - createProperties(desc, desc.getBeanType(), 0); + createProperties(desc, desc.getBeanType(), 0, new HashMap<>()); desc.sortProperties(); } @@ -56,15 +58,20 @@ private boolean ignoreFieldByName(String fieldName) { private boolean ignoreField(Field field) { return Modifier.isStatic(field.getModifiers()) - || Modifier.isTransient(field.getModifiers()) - || ignoreFieldByName(field.getName()); + || Modifier.isTransient(field.getModifiers()) + || ignoreFieldByName(field.getName()); } /** - * properties the bean properties from Class. Some of these properties may not map to database + * properties the bean properties from Class. Some of these properties may not + * map to database * columns. */ - private void createProperties(DeployBeanDescriptor desc, Class beanType, int level) { + private void createProperties( + DeployBeanDescriptor desc, + Class beanType, + int level, + Map, Class> genericTypeMap) { if (beanType.equals(Model.class)) { // ignore all fields on model (_$dbName) return; @@ -74,7 +81,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); + DeployBeanProperty prop = createProp(desc, field, beanType, genericTypeMap); if (prop != null) { // set a order that gives priority to inherited properties // push Id/EmbeddedId up and CreatedTimestamp/UpdatedTimestamp down @@ -95,7 +102,7 @@ private void createProperties(DeployBeanDescriptor desc, Class beanType, i if (!superClass.equals(Object.class)) { // recursively add any properties in the inheritance hierarchy // up to the Object.class level... - createProperties(desc, superClass, level + 1); + createProperties(desc, superClass, level + 1, mapGenerics(beanType)); } } catch (PersistenceException ex) { throw ex; @@ -116,8 +123,13 @@ private DeployBeanProperty createManyType(DeployBeanDescriptor desc, Class return new DeployBeanPropertyAssocMany<>(desc, targetType, manyType); } - private DeployBeanProperty createProp(DeployBeanDescriptor desc, Field field) { - Class propertyType = field.getType(); + private DeployBeanProperty createProp( + DeployBeanDescriptor desc, + Field field, + Map, Class> genericTypeMap) { + Class propertyType = field.getGenericType() instanceof TypeVariable + ? genericTypeMap.get(field.getGenericType()) + : field.getType(); if (isSpecialScalarType(field)) { return new DeployBeanProperty(desc, propertyType, field.getGenericType()); } @@ -131,7 +143,8 @@ private DeployBeanProperty createProp(DeployBeanDescriptor desc, Field field) // not supporting this field (generic type used) return null; } - CoreLog.internal.log(WARNING, "Could not find parameter type (via reflection) on " + desc.getFullName() + " " + field.getName()); + CoreLog.internal.log(WARNING, + "Could not find parameter type (via reflection) on " + desc.getFullName() + " " + field.getName()); } return createManyType(desc, targetType, manyType); } @@ -147,7 +160,8 @@ private DeployBeanProperty createProp(DeployBeanDescriptor desc, Field field) 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()); + throw new IllegalStateException("No AttributeConverter registered for type " + propertyType + " at " + + desc.getFullName() + "." + field.getName()); } try { return new DeployBeanPropertyAssocOne<>(desc, propertyType); @@ -162,18 +176,22 @@ private DeployBeanProperty createProp(DeployBeanDescriptor desc, Field field) */ private boolean isSpecialScalarType(Field field) { return (AnnotationUtil.has(field, DbJson.class)) - || (AnnotationUtil.has(field, DbJsonB.class)) - || (AnnotationUtil.has(field, DbArray.class)) - || (AnnotationUtil.has(field, DbMap.class)) - || (AnnotationUtil.has(field, UnmappedJson.class)); + || (AnnotationUtil.has(field, DbJsonB.class)) + || (AnnotationUtil.has(field, DbArray.class)) + || (AnnotationUtil.has(field, DbMap.class)) + || (AnnotationUtil.has(field, UnmappedJson.class)); } private boolean isTransientField(Field field) { return AnnotationUtil.has(field, Transient.class); } - private DeployBeanProperty createProp(DeployBeanDescriptor desc, Field field, Class beanType) { - DeployBeanProperty prop = createProp(desc, field); + private DeployBeanProperty createProp( + DeployBeanDescriptor desc, + Field field, + Class beanType, + Map, Class> genericTypeMap) { + DeployBeanProperty prop = createProp(desc, field, genericTypeMap); if (prop == null) { // transient annotation on unsupported type return null; @@ -186,7 +204,8 @@ private DeployBeanProperty createProp(DeployBeanDescriptor desc, Field field, } /** - * Determine the type of the List,Set or Map. Not been set explicitly so determine this from + * Determine the type of the List,Set or Map. Not been set explicitly so + * determine this from * ParameterizedType. */ private Class determineTargetType(Field field) { @@ -224,4 +243,40 @@ private Class determineTargetType(Field field) { // if targetType is null, then must be set in annotations return null; } + + private Map, 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, 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; + } }