Skip to content

Commit

Permalink
Fix for resolving entity fields based on collections and generics
Browse files Browse the repository at this point in the history
  • Loading branch information
aureamunoz committed Aug 22, 2024
1 parent ff155e9 commit 144cf49
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public final class DotNames {
// https://docs.jboss.org/hibernate/stable/orm/userguide/html_single/Hibernate_User_Guide.html#basic
// Should be in sync with org.hibernate.type.BasicTypeRegistry
public static final Set<DotName> HIBERNATE_PROVIDED_BASIC_TYPES = new HashSet<>(Arrays.asList(
STRING, CLASS,
STRING, CLASS, LIST,
BOOLEAN, PRIMITIVE_BOOLEAN,
INTEGER, PRIMITIVE_INTEGER,
LONG, PRIMITIVE_LONG,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,8 +338,28 @@ private int indexOfOrMaxValue(String methodName, String term) {
}

/**
* See:
* https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-property-expressions
* 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 +373,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 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);
}

0 comments on commit 144cf49

Please sign in to comment.