Skip to content
Closed
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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>4.0.0-SNAPSHOT</version>
<version>4.0.x-GH-3374-SNAPSHOT</version>

<name>Spring Data Core</name>
<description>Core Spring concepts underpinning every Spring Data module.</description>
Expand Down
44 changes: 43 additions & 1 deletion src/main/java/org/springframework/data/javapoet/TypeNames.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
*/
package org.springframework.data.javapoet;

import java.util.Arrays;

import org.springframework.core.ResolvableType;
import org.springframework.javapoet.ArrayTypeName;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.ParameterizedTypeName;
import org.springframework.javapoet.TypeName;
import org.springframework.util.ClassUtils;

Expand All @@ -28,6 +33,7 @@
* Mainly for internal use within the framework
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 4.0
*/
public abstract class TypeNames {
Expand Down Expand Up @@ -65,6 +71,42 @@ public static TypeName className(ResolvableType resolvableType) {
return TypeName.get(resolvableType.toClass());
}

/**
* Obtain a {@link TypeName} for the underlying type of the given {@link ResolvableType}. Can render a class name, a
* type signature with resolved generics or a generic type variable.
*
* @param resolvableType the resolvable type represent.
* @return the corresponding {@link TypeName}.
*/
public static TypeName resolvedTypeName(ResolvableType resolvableType) {

if (resolvableType.equals(ResolvableType.NONE)) {
return TypeName.get(Object.class);
}

if (resolvableType.hasResolvableGenerics()) {
return ParameterizedTypeName.get(ClassName.get(resolvableType.toClass()),
Arrays.stream(resolvableType.getGenerics()).map(TypeNames::resolvedTypeName).toArray(TypeName[]::new));
}

if (!resolvableType.hasGenerics()) {

Class<?> resolvedType = resolvableType.toClass();

if (!resolvableType.isArray() || resolvedType.isArray()) {
return TypeName.get(resolvedType);
}

if (resolvableType.isArray()) {
return ArrayTypeName.of(resolvedType);
}

return TypeName.get(resolvedType);
}

return ClassName.get(resolvableType.toClass());
}

/**
* Obtain a {@link TypeName} for the underlying type of the given {@link ResolvableType}. Can render a class name, a
* type signature or a generic type variable.
Expand Down Expand Up @@ -98,7 +140,7 @@ public static TypeName typeNameOrWrapper(Class<?> type) {
public static TypeName typeNameOrWrapper(ResolvableType resolvableType) {
return ClassUtils.isPrimitiveOrWrapper(resolvableType.toClass())
? TypeName.get(ClassUtils.resolvePrimitiveIfNecessary(resolvableType.toClass()))
: typeName(resolvableType);
: resolvedTypeName(resolvableType);
}

private TypeNames() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,32 @@
package org.springframework.data.repository.aot.generate;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;

import javax.lang.model.element.Modifier;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;

import org.springframework.core.ResolvableType;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.FieldSpec;
import org.springframework.javapoet.MethodSpec;
Expand Down Expand Up @@ -290,6 +298,7 @@ private void contributeMethod(Method method, @Nullable MethodContributorFactory
MethodContributor<? extends QueryMethod> contributor = contributorFactory.create(method);

if (contributor == null) {

if (logger.isTraceEnabled()) {
logger.trace("Skipping method [%s.%s] contribution, no MethodContributor available"
.formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName()));
Expand All @@ -298,11 +307,241 @@ private void contributeMethod(Method method, @Nullable MethodContributorFactory
return;
}

if (contributor.contributesMethodSpec() && !repositoryInformation.isReactiveRepository()) {
generationMetadata.addRepositoryMethod(method, contributor);
} else {
if (ResolvableGenerics.of(method, repositoryInformation.getRepositoryInterface()).hasUnresolvableGenerics()) {

if (logger.isTraceEnabled()) {
logger.trace(
"Skipping implementation method [%s.%s] contribution. Method uses generics that currently cannot be resolved."
.formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName()));
}

generationMetadata.addDelegateMethod(method, contributor);
return;
}

if (!contributor.contributesMethodSpec() || repositoryInformation.isReactiveRepository()) {

if (repositoryInformation.isReactiveRepository() && logger.isTraceEnabled()) {
logger.trace(
"Skipping implementation method [%s.%s] contribution. AOT repositories are not supported for reactive repositories."
.formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName()));
}

if (!contributor.contributesMethodSpec() && logger.isTraceEnabled()) {
logger.trace(
"Skipping implementation method [%s.%s] contribution. Spring Data %s did not provide a method implementation."
.formatted(repositoryInformation.getRepositoryInterface().getName(), method.getName(), moduleName));
}

generationMetadata.addDelegateMethod(method, contributor);
return;
}

generationMetadata.addRepositoryMethod(method, contributor);
}

/**
* Value object to determine whether generics in a given {@link Method} can be resolved. Resolvable generics are e.g.
* declared on the method level (unbounded type variables, type variables using class boundaries). Considers
* collections and map types.
* <p>
* Considers resolvable:
* <ul>
* <li>Unbounded method-level type parameters {@code <T> T foo(Class<T>)}</li>
* <li>Bounded method-level type parameters that resolve to a class
* {@code <T extends Serializable> T foo(Class<T>)}</li>
* <li>Simple references to interface variables {@code T foo(), List<T> foo(…)}</li>
* <li>Unbounded wildcards {@code User foo(GeoJson<?>)}</li>
* </ul>
* Considers non-resolvable:
* <ul>
* <li>Parametrized bounds referring to known variables on method-level type parameters
* {@code <P extends T> T foo(Class<T>), List<? super T> foo()}</li>
* <li>Generally unresolvable generics</li>
* </ul>
* </p>
*
* @author Mark Paluch
*/
record ResolvableGenerics(Method method, Class<?> implClass, Set<Type> resolvableTypeVariables,
Set<Type> unwantedMethodVariables) {

/**
* Create a new {@code ResolvableGenerics} object for the given {@link Method}.
*
* @param method
* @return
*/
public static ResolvableGenerics of(Method method, Class<?> implClass) {
return new ResolvableGenerics(method, implClass, getResolvableTypeVariables(method),
getUnwantedMethodVariables(method));
}

private static Set<Type> getResolvableTypeVariables(Method method) {

Set<Type> simpleTypeVariables = new HashSet<>();

for (TypeVariable<Method> typeParameter : method.getTypeParameters()) {
if (isClassBounded(typeParameter.getBounds())) {
simpleTypeVariables.add(typeParameter);
}
}

return simpleTypeVariables;
}

private static Set<Type> getUnwantedMethodVariables(Method method) {

Set<Type> unwanted = new HashSet<>();

for (TypeVariable<Method> typeParameter : method.getTypeParameters()) {
if (!isClassBounded(typeParameter.getBounds())) {
unwanted.add(typeParameter);
}
}
return unwanted;
}

/**
* Check whether the {@link Method} has unresolvable generics when being considered in the context of the
* implementation class.
*
* @return
*/
public boolean hasUnresolvableGenerics() {

ResolvableType resolvableType = ResolvableType.forMethodReturnType(method, implClass);

if (isUnresolvable(resolvableType)) {
return true;
}

for (int i = 0; i < method.getParameterCount(); i++) {
if (isUnresolvable(ResolvableType.forMethodParameter(method, i, implClass))) {
return true;
}
}

return false;
}

private boolean isUnresolvable(TypeInformation<?> typeInformation) {
return isUnresolvable(typeInformation.toResolvableType());
}

private boolean isUnresolvable(ResolvableType resolvableType) {

if (isResolvable(resolvableType)) {
return false;
}

if (isUnwanted(resolvableType)) {
return true;
}

if (resolvableType.isAssignableFrom(Class.class)) {
return isUnresolvable(resolvableType.getGeneric(0));
}

TypeInformation<?> typeInformation = TypeInformation.of(resolvableType);
if (typeInformation.isMap() || typeInformation.isCollectionLike()) {

for (ResolvableType type : resolvableType.getGenerics()) {
if (isUnresolvable(type)) {
return true;
}
}

return false;
}

if (typeInformation.getActualType() != null && typeInformation.getActualType() != typeInformation) {
return isUnresolvable(typeInformation.getRequiredActualType());
}

return resolvableType.hasUnresolvableGenerics();
}

private boolean isResolvable(Type[] types) {

for (Type type : types) {

if (resolvableTypeVariables.contains(type)) {
continue;
}

if (isClass(type)) {
continue;
}

return false;
}

return true;
}

private boolean isResolvable(ResolvableType resolvableType) {

return testGenericType(resolvableType, it -> {

if (resolvableTypeVariables.contains(it)) {
return true;
}

if (it instanceof WildcardType wt) {
return isClassBounded(wt.getLowerBounds()) && isClassBounded(wt.getUpperBounds());
}

return false;
});
}

private boolean isUnwanted(ResolvableType resolvableType) {

return testGenericType(resolvableType, o -> {

if (o instanceof WildcardType wt) {
return !isResolvable(wt.getLowerBounds()) || !isResolvable(wt.getUpperBounds());
}

return unwantedMethodVariables.contains(o);
});
}

private static boolean testGenericType(ResolvableType resolvableType, Predicate<Type> predicate) {

if (predicate.test(resolvableType.getType())) {
return true;
}

ResolvableType[] generics = resolvableType.getGenerics();
for (ResolvableType generic : generics) {
if (testGenericType(generic, predicate)) {
return true;
}
}

return false;
}

private static boolean isClassBounded(Type[] bounds) {

for (Type bound : bounds) {

if (isClass(bound)) {
continue;
}

return false;
}

return true;
}

private static boolean isClass(Type type) {
return type instanceof Class;
}

}

/**
Expand Down
Loading