Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for resolving entity fields based on collections and generics #42705

Merged
merged 1 commit into from
Aug 28, 2024
Merged
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 @@ -338,8 +338,29 @@ private int indexOfOrMaxValue(String methodName, String term) {
}

/**
* Resolves a nested field within an entity class based on a given field path expression.
* This method traverses through the entity class and potentially its related classes,
* identifying and returning the appropriate field. It handles complex field paths that may
* include multiple levels of nested fields, separated by underscores ('_').
*
* See:
* *
* https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-property-expressions
*
* @param repositoryMethodDescription A description of the repository method,
* typically used for error reporting.
* @param fieldPathExpression The expression representing the path of the field within
* the entity class. Fields at different levels of nesting
* should be separated by underscores ('_').
* @param fieldPathBuilder A StringBuilder used to construct and return the resolved field path.
* It will contain the fully qualified field path once the method completes.
* @return The {@link FieldInfo} object representing the resolved field. If the field cannot be resolved,
* an exception is thrown.
* @throws UnableToParseMethodException If the field cannot be resolved from the given
* field path expression, this exception is thrown
* with a detailed error message.
* @throws IllegalStateException If the resolved entity class referenced by the field is not found
* in the Quarkus index, or if a typed field could not be resolved properly.
*/
private FieldInfo resolveNestedField(String repositoryMethodDescription, String fieldPathExpression,
StringBuilder fieldPathBuilder) {
Expand All @@ -353,19 +374,30 @@ private FieldInfo resolveNestedField(String repositoryMethodDescription, String

MutableReference<List<ClassInfo>> parentSuperClassInfos = new MutableReference<>();
int fieldStartIndex = 0;
ClassInfo parentFieldInfo = null;
while (fieldStartIndex < fieldPathExpression.length()) {
// The underscore character is treated as reserved character to manually define traversal points.
// This means that path expression may have multiple levels separated by the '_' character. For example: person_address_city.
if (fieldPathExpression.charAt(fieldStartIndex) == '_') {
// See issue #34395
// For resolving correctly nested fields added using '_' we need to get the previous fieldInfo which will be the class containing the field starting by '_' in this loop.
DotName parentFieldInfoName;
if (fieldInfo != null && fieldInfo.type().kind() == Type.Kind.PARAMETERIZED_TYPE) {
parentFieldInfoName = fieldInfo.type().asParameterizedType().arguments().stream().findFirst().get().name();
parentFieldInfo = indexView.getClassByName(parentFieldInfoName);
}
fieldStartIndex++;
if (fieldStartIndex >= fieldPathExpression.length()) {
throw new UnableToParseMethodException(fieldNotResolvableMessage + offendingMethodMessage);
}
}
// the underscore character is treated as reserved character to manually define traversal points.
int firstSeparator = fieldPathExpression.indexOf('_', fieldStartIndex);
int fieldEndIndex = firstSeparator == -1 ? fieldPathExpression.length() : firstSeparator;
while (fieldEndIndex >= fieldStartIndex) {
String simpleFieldName = lowerFirstLetter(fieldPathExpression.substring(fieldStartIndex, fieldEndIndex));
fieldInfo = getFieldInfo(simpleFieldName, parentClassInfo, parentSuperClassInfos);
String fieldName = fieldPathExpression.substring(fieldStartIndex, fieldEndIndex);
String simpleFieldName = lowerFirstLetter(fieldName);
fieldInfo = getFieldInfo(simpleFieldName, parentFieldInfo == null ? parentClassInfo : parentFieldInfo,
parentSuperClassInfos);
if (fieldInfo != null) {
break;
}
Expand All @@ -390,6 +422,8 @@ private FieldInfo resolveNestedField(String repositoryMethodDescription, String
if (fieldInfo.type().kind() == Type.Kind.TYPE_VARIABLE) {
typed = true;
parentClassName = getParentNameFromTypedFieldViaHierarchy(fieldInfo, mappedSuperClassInfos);
} else if (fieldInfo.type().kind() == Type.Kind.PARAMETERIZED_TYPE) {
parentClassName = fieldInfo.type().asParameterizedType().arguments().stream().findFirst().get().name();
} else {
parentClassName = fieldInfo.type().name();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
import org.jboss.jandex.MethodInfo;
import org.junit.jupiter.api.Test;

import io.quarkus.spring.data.deployment.generics.ChildBase;
import io.quarkus.spring.data.deployment.generics.ParentBase;
import io.quarkus.spring.data.deployment.generics.ParentBaseRepository;

public class MethodNameParserTest {

private final Class<?> repositoryClass = PersonRepository.class;
Expand Down Expand Up @@ -95,6 +99,32 @@ public void testFindAllBy_() throws Exception {
assertThat(exception).hasMessageContaining("Person does not contain a field named: _");
}

@Test
public void testGenericsWithWildcard() throws Exception {
Class[] additionalClasses = new Class[] { ChildBase.class };

MethodNameParser.Result result = parseMethod(ParentBaseRepository.class, "countParentsByChildren_Nombre",
ParentBase.class,
additionalClasses);
assertThat(result).isNotNull();
assertSameClass(result.getEntityClass(), ParentBase.class);
assertThat(result.getQuery()).isEqualTo("FROM ParentBase WHERE children.nombre = ?1");
assertThat(result.getParamCount()).isEqualTo(1);
}

@Test
public void shouldParseRepositoryMethodOverEntityContainingACollection() throws Exception {
Class[] additionalClasses = new Class[] { LoginEvent.class };

MethodNameParser.Result result = parseMethod(UserRepository.class, "countUsersByLoginEvents_Id",
User.class,
additionalClasses);
assertThat(result).isNotNull();
assertSameClass(result.getEntityClass(), User.class);
assertThat(result.getQuery()).isEqualTo("FROM User WHERE loginEvents.id = ?1");
assertThat(result.getParamCount()).isEqualTo(1);
}

private AbstractStringAssert<?> assertSameClass(ClassInfo classInfo, Class<?> aClass) {
return assertThat(classInfo.name().toString()).isEqualTo(aClass.getName());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ public interface UserRepository extends JpaRepository<User, String> {

// purposely with compiled parameter name not matching the query to also test that @Param takes precedence
User getUserByFullNameUsingNamedQueries(@Param("name") String arg);

long countUsersByLoginEvents_Id(Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.quarkus.spring.data.deployment.generics;

import jakarta.persistence.MappedSuperclass;

@MappedSuperclass
public class ChildBase {
String nombre;
String detail;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkus.spring.data.deployment.generics;

import java.util.List;

import jakarta.persistence.CascadeType;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.OneToMany;

@MappedSuperclass
public class ParentBase<T extends ChildBase> {
String name;
String detail;
int age;
float test;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<T> children;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.quarkus.spring.data.deployment.generics;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ParentBaseRepository<T extends ParentBase<?>> extends JpaRepository<T, Long> {
long countParentsByChildren_Nombre(String name);
}