diff --git a/extensions/spring-data-jpa/deployment/pom.xml b/extensions/spring-data-jpa/deployment/pom.xml
index 3597eba0146b3..7dfcd14e19ea2 100644
--- a/extensions/spring-data-jpa/deployment/pom.xml
+++ b/extensions/spring-data-jpa/deployment/pom.xml
@@ -48,6 +48,11 @@
rest-assured
test
+
+ org.assertj
+ assertj-core
+ test
+
diff --git a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/DotNames.java b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/DotNames.java
index 27c08c80c4536..6b5063c0fd773 100644
--- a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/DotNames.java
+++ b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/DotNames.java
@@ -75,6 +75,16 @@ public final class DotNames {
public static final DotName PRIMITIVE_LONG = DotName.createSimple(long.class.getName());
public static final DotName INTEGER = DotName.createSimple(Integer.class.getName());
public static final DotName PRIMITIVE_INTEGER = DotName.createSimple(int.class.getName());
+ public static final DotName SHORT = DotName.createSimple(Short.class.getName());
+ public static final DotName PRIMITIVE_SHORT = DotName.createSimple(short.class.getName());
+ public static final DotName CHARACTER = DotName.createSimple(Character.class.getName());
+ public static final DotName PRIMITIVE_CHAR = DotName.createSimple(char.class.getName());
+ public static final DotName BYTE = DotName.createSimple(Byte.class.getName());
+ public static final DotName PRIMITIVE_BYTE = DotName.createSimple(byte.class.getName());
+ public static final DotName DOUBLE = DotName.createSimple(Double.class.getName());
+ public static final DotName PRIMITIVE_DOUBLE = DotName.createSimple(double.class.getName());
+ public static final DotName FLOAT = DotName.createSimple(Float.class.getName());
+ public static final DotName PRIMITIVE_FLOAT = DotName.createSimple(float.class.getName());
public static final DotName BOOLEAN = DotName.createSimple(Boolean.class.getName());
public static final DotName PRIMITIVE_BOOLEAN = DotName.createSimple(boolean.class.getName());
public static final DotName STRING = DotName.createSimple(String.class.getName());
diff --git a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/MethodNameParser.java b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/MethodNameParser.java
index 837fba8d44023..75315d6eab8ba 100644
--- a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/MethodNameParser.java
+++ b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/MethodNameParser.java
@@ -7,11 +7,8 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.stream.Collectors;
-import org.apache.commons.lang3.StringUtils;
import org.jboss.jandex.ClassInfo;
-import org.jboss.jandex.ClassType;
import org.jboss.jandex.DotName;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.IndexView;
@@ -60,6 +57,17 @@ public class MethodNameParser {
private static final Set BOOLEAN_OPERATIONS = new HashSet<>(Arrays.asList("True", "False"));
+ private static final Set SIMPLE_FIELD_TYPES = new HashSet<>(Arrays.asList(
+ DotNames.STRING,
+ DotNames.BOOLEAN, DotNames.PRIMITIVE_BOOLEAN,
+ DotNames.INTEGER, DotNames.PRIMITIVE_INTEGER,
+ DotNames.LONG, DotNames.PRIMITIVE_LONG,
+ DotNames.SHORT, DotNames.PRIMITIVE_SHORT,
+ DotNames.BYTE, DotNames.PRIMITIVE_BYTE,
+ DotNames.CHARACTER, DotNames.PRIMITIVE_CHAR,
+ DotNames.DOUBLE, DotNames.PRIMITIVE_DOUBLE,
+ DotNames.FLOAT, DotNames.PRIMITIVE_FLOAT));
+
private final ClassInfo entityClass;
private final IndexView indexView;
private final List mappedSuperClassInfos;
@@ -172,6 +180,7 @@ public Result parse(MethodInfo methodInfo) {
parts = Arrays.asList(afterByPart.split("Or"));
}
+ MutableReference> mappedSuperClassInfoRef = MutableReference.of(mappedSuperClassInfos);
StringBuilder where = new StringBuilder();
int paramsCount = 0;
for (String part : parts) {
@@ -192,64 +201,13 @@ public Result parse(MethodInfo methodInfo) {
} else {
fieldName = lowerFirstLetter(part.replaceAll(operation, ""));
}
- FieldInfo fieldInfo = getField(fieldName);
+ FieldInfo fieldInfo = getFieldInfo(fieldName, entityClass, mappedSuperClassInfoRef);
if (fieldInfo == null) {
- ClassInfo associatedEntityClassInfo;
- String associatedEntityFieldName;
- String simpleFieldName;
- String parsingExceptionMethod = "Entity " + entityClass + " does not contain a field named: " + fieldName +
- ". " + "Offending method is " + methodName;
-
- // determine if we are trying to use a field of one of the associated entities
- int nextStartingIndex = 1;
- while (true) {
- int fieldEndIndex = -1;
- for (int i = nextStartingIndex; i < fieldName.length() - 1; i++) {
- char c = fieldName.charAt(i);
- if ((c >= 'A' && c <= 'Z') || c == '_') {
- fieldEndIndex = i;
- break;
- }
- }
-
- if (fieldEndIndex == -1) {
- throw new UnableToParseMethodException(parsingExceptionMethod);
- }
-
- int associatedEntityFieldStartIndex = fieldName.charAt(fieldEndIndex) == '_' ? fieldEndIndex + 1
- : fieldEndIndex;
- if (associatedEntityFieldStartIndex >= fieldName.length() - 1) {
- throw new UnableToParseMethodException(parsingExceptionMethod);
- }
-
- simpleFieldName = fieldName.substring(0, fieldEndIndex);
- associatedEntityFieldName = lowerFirstLetter(fieldName.substring(associatedEntityFieldStartIndex));
- fieldInfo = getField(simpleFieldName);
- if ((fieldInfo == null) || !(fieldInfo.type() instanceof ClassType)) {
- nextStartingIndex = fieldEndIndex + 1;
- } else {
- break;
- }
- }
-
- associatedEntityClassInfo = indexView.getClassByName(fieldInfo.type().name());
- if (associatedEntityClassInfo == null) {
- throw new IllegalStateException(
- "Entity class " + fieldInfo.type().name() + " was not part of the Quarkus index");
- }
- FieldInfo associatedEntityClassField = getAssociatedEntityClassField(associatedEntityFieldName,
- associatedEntityClassInfo);
- if (associatedEntityClassField == null) {
- throw new UnableToParseMethodException(parsingExceptionMethod);
- }
-
- validateFieldWithOperation(operation, associatedEntityClassField, methodName);
-
- // set the fieldName to the proper JPQL expression
- fieldName = simpleFieldName + "." + associatedEntityFieldName;
- } else {
- validateFieldWithOperation(operation, fieldInfo, methodName);
+ StringBuilder fieldPathBuilder = new StringBuilder(fieldName.length() + 5);
+ fieldInfo = resolveNestedField(methodName, fieldName, fieldPathBuilder);
+ fieldName = fieldPathBuilder.toString();
}
+ validateFieldWithOperation(operation, fieldInfo, fieldName, methodName);
if ((ignoreCase || allIgnoreCase) && !DotNames.STRING.equals(fieldInfo.type().name())) {
throw new UnableToParseMethodException(
"IgnoreCase cannot be specified for field" + fieldInfo.name() + " of method "
@@ -263,12 +221,6 @@ public Result parse(MethodInfo methodInfo) {
String upperPrefix = (ignoreCase || allIgnoreCase) ? "UPPER(" : "";
String upperSuffix = (ignoreCase || allIgnoreCase) ? ")" : "";
- // If the fieldName is not a field in the class and in camelcase format,
- // then split it as hierarchy of fields
- if (entityClass.field(fieldName) == null) {
- fieldName = handleFieldsHierarchy(fieldName, fieldInfo);
- }
-
where.append(upperPrefix).append(fieldName).append(upperSuffix);
if ((operation == null) || "Equals".equals(operation) || "Is".equals(operation)) {
paramsCount++;
@@ -382,73 +334,75 @@ public Result parse(MethodInfo methodInfo) {
topCount);
}
- private String handleFieldsHierarchy(String fieldName, FieldInfo currentField) {
- StringBuilder finalName = new StringBuilder(fieldName);
-
- Set childFields = new HashSet<>();
-
- childFields.addAll(entityClass.fields().stream()
- .map(FieldInfo::name)
- .collect(Collectors.toList()));
-
- // Collecting the current class fields
- ClassInfo currentClassInfo = indexView.getClassByName(currentField.type().name());
-
- if (currentClassInfo != null) {
- childFields.addAll(
- currentClassInfo.fields()
- .stream()
- .map(FieldInfo::name)
- .collect(Collectors.toList()));
- }
-
- // Collecting the inherited fields from the superclass of the actual class
- DotName superClassName = entityClass.superClassType().name();
- ClassInfo superClassInfo = indexView.getClassByName(superClassName);
-
- ClassInfo classByName;
+ /**
+ * See:
+ * https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-property-expressions
+ */
+ private FieldInfo resolveNestedField(String methodName, String fieldPathExpression, StringBuilder fieldPathBuilder) {
- if (superClassName != null && superClassInfo != null && currentClassInfo != null &&
- currentClassInfo.superClassType() != null &&
- (classByName = indexView.getClassByName(currentClassInfo.superClassType().name())) != null) {
+ String fieldNotResolvableMessage = "Entity " + this.entityClass + " does not contain a field named: "
+ + fieldPathExpression + ". ";
+ String offendingMethodMessage = "Offending method is " + methodName + ".";
- childFields.addAll(superClassInfo.fields()
- .stream()
- .map(FieldInfo::name).collect(Collectors.toList()));
+ ClassInfo parentClassInfo = this.entityClass;
+ FieldInfo fieldInfo = null;
- childFields.addAll(classByName.fields()
- .stream()
- .map(FieldInfo::name).collect(Collectors.toList()));
+ int fieldStartIndex = 0;
+ while (fieldStartIndex < fieldPathExpression.length()) {
+ if (fieldPathExpression.charAt(fieldStartIndex) == '_') {
+ fieldStartIndex++;
+ if (fieldStartIndex >= fieldPathExpression.length()) {
+ throw new UnableToParseMethodException(fieldNotResolvableMessage + offendingMethodMessage);
+ }
+ }
+ MutableReference> parentSuperClassInfos = new MutableReference<>();
+ // 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);
+ if (fieldInfo != null) {
+ break;
+ }
+ fieldEndIndex = previousPotentialFieldEnd(fieldPathExpression, fieldStartIndex, fieldEndIndex);
+ }
+ if (fieldInfo == null) {
+ String detail = "";
+ if (fieldStartIndex > 0) {
+ String notMatched = lowerFirstLetter(fieldPathExpression.substring(fieldStartIndex));
+ detail = "Can not resolve " + parentClassInfo + "." + notMatched + ". ";
+ }
+ throw new UnableToParseMethodException(
+ fieldNotResolvableMessage + detail + offendingMethodMessage);
+ }
+ if (fieldPathBuilder.length() > 0) {
+ fieldPathBuilder.append('.');
+ }
+ fieldPathBuilder.append(fieldInfo.name());
+ if (!isSupportedHibernateType(fieldInfo.type().name())) {
+ parentClassInfo = indexView.getClassByName(fieldInfo.type().name());
+ if (parentClassInfo == null) {
+ throw new IllegalStateException(
+ "Entity class " + fieldInfo.type().name() + " referenced by "
+ + this.entityClass + "." + fieldPathBuilder
+ + " was not part of the Quarkus index. " + offendingMethodMessage);
+ }
+ }
+ fieldStartIndex = fieldEndIndex;
}
- // Collecting the inherited fields from the superclasses of the attributes
- if (currentClassInfo != null && currentClassInfo.superClassType() != null
- && (classByName = indexView.getClassByName(currentClassInfo.superClassType().name())) != null) {
-
- childFields.addAll(
- classByName.fields()
- .stream()
- .map(FieldInfo::name).collect(Collectors.toList()));
- }
+ return fieldInfo;
+ }
- // Building the fieldName from the members classes and their superclasses
- for (String fieldInf : childFields) {
- if (StringUtils.containsIgnoreCase(fieldName, fieldInf)) {
- String newValue = finalName.toString()
- .replaceAll("(?i)" + fieldInf, lowerFirstLetter(fieldInf) + ".");
- newValue = newValue.replace("..", "."); // this is just the easiest way to deal with fields of fields
- finalName.delete(0, finalName.length());
- finalName.append(newValue);
+ private int previousPotentialFieldEnd(String fieldName, int fieldStartIndex, int fieldEndIndexExclusive) {
+ for (int i = fieldEndIndexExclusive - 1; i > fieldStartIndex; i--) {
+ char c = fieldName.charAt(i);
+ if (c >= 'A' && c <= 'Z') {
+ return i;
}
}
-
- // In some cases, the built hierarchy is ending by a joining point. so we need to remove it
- if (finalName.toString().charAt(finalName.length() - 1) == '.') {
- fieldName = finalName.toString().replaceAll(".$", "");
- } else {
- fieldName = finalName.toString();
- }
- return fieldName;
+ return -1;
}
/**
@@ -469,38 +423,18 @@ private boolean containsLogicOperator(String str, String operatorStr) {
return Character.isUpperCase(str.charAt(index + operatorStr.length()));
}
- /**
- * Looks for the field in either the class itself or in a superclass that is annotated with @MappedSuperClass
- */
- private FieldInfo getAssociatedEntityClassField(String associatedEntityFieldName, ClassInfo associatedEntityClassInfo) {
- FieldInfo fieldInfo = associatedEntityClassInfo.field(associatedEntityFieldName);
- if (fieldInfo != null) {
- return fieldInfo;
- }
- if (DotNames.OBJECT.equals(associatedEntityClassInfo.superName())) {
- return null;
- }
-
- ClassInfo superClassInfo = indexView.getClassByName(associatedEntityClassInfo.superName());
- if (superClassInfo.classAnnotation(DotNames.JPA_MAPPED_SUPERCLASS) == null) {
- return null;
- }
-
- return getAssociatedEntityClassField(associatedEntityFieldName, superClassInfo);
- }
-
- private void validateFieldWithOperation(String operation, FieldInfo fieldInfo, String methodName) {
+ private void validateFieldWithOperation(String operation, FieldInfo fieldInfo, String fieldPath, String methodName) {
DotName fieldTypeDotName = fieldInfo.type().name();
if (STRING_LIKE_OPERATIONS.contains(operation) && !DotNames.STRING.equals(fieldTypeDotName)) {
throw new UnableToParseMethodException(
- operation + " cannot be specified for field" + fieldInfo.name() + " of method "
+ operation + " cannot be specified for field" + fieldPath + " of method "
+ methodName + " because it is not a String type");
}
if (BOOLEAN_OPERATIONS.contains(operation) && !DotNames.BOOLEAN.equals(fieldTypeDotName)
&& !DotNames.PRIMITIVE_BOOLEAN.equals(fieldTypeDotName)) {
throw new UnableToParseMethodException(
- operation + " cannot be specified for field" + fieldInfo.name() + " of method "
+ operation + " cannot be specified for field" + fieldPath + " of method "
+ methodName + " because it is not a boolean type");
}
}
@@ -572,16 +506,14 @@ private boolean entityContainsField(String fieldName) {
return false;
}
- private FieldInfo getField(String fieldName) {
- // Before validating the fieldInfo,
- // we need to split the camelcase format and grab the first item
+ private FieldInfo getFieldInfo(String fieldName, ClassInfo entityClass,
+ MutableReference> mappedSuperClassInfos) {
FieldInfo fieldInfo = entityClass.field(fieldName);
if (fieldInfo == null) {
- String[] camelCaseStrings = StringUtils.splitByCharacterTypeCamelCase(fieldName);
- fieldInfo = entityClass.field(camelCaseStrings[0]);
- }
- if (fieldInfo == null) {
- for (ClassInfo superClass : mappedSuperClassInfos) {
+ if (mappedSuperClassInfos.isEmpty()) {
+ mappedSuperClassInfos.set(getMappedSuperClassInfos(indexView, entityClass));
+ }
+ for (ClassInfo superClass : mappedSuperClassInfos.get()) {
fieldInfo = superClass.field(fieldName);
if (fieldInfo != null) {
break;
@@ -610,6 +542,37 @@ private List getMappedSuperClassInfos(IndexView indexView, ClassInfo
return mappedSuperClassInfoElements;
}
+ private boolean isSupportedHibernateType(DotName dotName) {
+ return SIMPLE_FIELD_TYPES.contains(dotName);
+ }
+
+ private static class MutableReference {
+ private T reference;
+
+ public static MutableReference of(T reference) {
+ return new MutableReference<>(reference);
+ }
+
+ public MutableReference() {
+ }
+
+ private MutableReference(T reference) {
+ this.reference = reference;
+ }
+
+ public T get() {
+ return reference;
+ }
+
+ public void set(T value) {
+ this.reference = value;
+ }
+
+ public boolean isEmpty() {
+ return reference == null;
+ }
+ }
+
public static class Result {
private final ClassInfo entityClass;
private final String query;
diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/MethodNameParserTest.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/MethodNameParserTest.java
new file mode 100644
index 0000000000000..874cab3028af2
--- /dev/null
+++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/MethodNameParserTest.java
@@ -0,0 +1,124 @@
+package io.quarkus.spring.data.deployment;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.assertj.core.api.AbstractStringAssert;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.Index;
+import org.jboss.jandex.IndexView;
+import org.jboss.jandex.Indexer;
+import org.jboss.jandex.MethodInfo;
+import org.junit.jupiter.api.Test;
+
+public class MethodNameParserTest {
+
+ private final Class> repositoryClass = PersonRepository.class;
+ private final Class> entityClass = Person.class;
+ private final Class[] additionalClasses = new Class[] { Person.Address.class, Person.Country.class };
+
+ @Test
+ public void testFindAllByAddressZipCode() throws Exception {
+ MethodNameParser.Result result = parseMethod(repositoryClass, "findAllByAddressZipCode", entityClass,
+ additionalClasses);
+ assertThat(result).isNotNull();
+ assertSameClass(result.getEntityClass(), entityClass);
+ assertThat(result.getQuery()).isEqualTo("FROM Person WHERE address.zipCode = ?1");
+ assertThat(result.getParamCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testFindAllByAddressCountry() throws Exception {
+ MethodNameParser.Result result = parseMethod(repositoryClass, "findAllByAddressCountry", entityClass,
+ additionalClasses);
+ assertThat(result).isNotNull();
+ assertSameClass(result.getEntityClass(), entityClass);
+ assertThat(result.getQuery()).isEqualTo("FROM Person WHERE addressCountry = ?1");
+ assertThat(result.getParamCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testFindAllByAddress_Country() throws Exception {
+ MethodNameParser.Result result = parseMethod(repositoryClass, "findAllByAddress_Country", entityClass,
+ additionalClasses);
+ assertThat(result).isNotNull();
+ assertSameClass(result.getEntityClass(), entityClass);
+ assertThat(result.getQuery()).isEqualTo("FROM Person WHERE address.country = ?1");
+ assertThat(result.getParamCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testFindAllByAddressCountryIsoCode() throws Exception {
+ UnableToParseMethodException exception = assertThrows(UnableToParseMethodException.class,
+ () -> parseMethod(repositoryClass, "findAllByAddressCountryIsoCode", entityClass, additionalClasses));
+ assertThat(exception).hasMessageContaining("Person does not contain a field named: addressCountryIsoCode");
+ }
+
+ @Test
+ public void testFindAllByAddress_CountryIsoCode() throws Exception {
+ MethodNameParser.Result result = parseMethod(repositoryClass, "findAllByAddress_CountryIsoCode", entityClass,
+ additionalClasses);
+ assertThat(result).isNotNull();
+ assertSameClass(result.getEntityClass(), entityClass);
+ assertThat(result.getQuery()).isEqualTo("FROM Person WHERE address.country.isoCode = ?1");
+ assertThat(result.getParamCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testFindAllByAddress_Country_IsoCode() throws Exception {
+ MethodNameParser.Result result = parseMethod(repositoryClass, "findAllByAddress_Country_IsoCode", entityClass,
+ additionalClasses);
+ assertThat(result).isNotNull();
+ assertSameClass(result.getEntityClass(), entityClass);
+ assertThat(result.getQuery()).isEqualTo("FROM Person WHERE address.country.isoCode = ?1");
+ assertThat(result.getParamCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testFindAllByAddress_CountryInvalid() throws Exception {
+ UnableToParseMethodException exception = assertThrows(UnableToParseMethodException.class,
+ () -> parseMethod(repositoryClass, "findAllByAddress_CountryInvalid", entityClass, additionalClasses));
+ assertThat(exception).hasMessageContaining("Person does not contain a field named: address_CountryInvalid");
+ assertThat(exception).hasMessageContaining("Country.invalid");
+ }
+
+ @Test
+ public void testFindAllBy_() throws Exception {
+ UnableToParseMethodException exception = assertThrows(UnableToParseMethodException.class,
+ () -> parseMethod(repositoryClass, "findAllBy_", entityClass, additionalClasses));
+ assertThat(exception).hasMessageContaining("Person does not contain a field named: _");
+ }
+
+ private AbstractStringAssert> assertSameClass(ClassInfo classInfo, Class> aClass) {
+ return assertThat(classInfo.name().toString()).isEqualTo(aClass.getName());
+ }
+
+ private MethodNameParser.Result parseMethod(Class> repositoryClass, String methodToParse,
+ Class> entityClass, Class>... additionalClasses) throws IOException {
+ IndexView indexView = index(ArrayUtils.addAll(additionalClasses, repositoryClass, entityClass));
+ DotName repository = DotName.createSimple(repositoryClass.getName());
+ DotName entity = DotName.createSimple(entityClass.getName());
+ ClassInfo entityClassInfo = indexView.getClassByName(entity);
+ ClassInfo repositoryClassInfo = indexView.getClassByName(repository);
+ MethodNameParser methodNameParser = new MethodNameParser(entityClassInfo, indexView);
+ MethodInfo repositoryMethod = repositoryClassInfo.firstMethod(methodToParse);
+ MethodNameParser.Result result = methodNameParser.parse(repositoryMethod);
+ return result;
+ }
+
+ public static Index index(Class>... classes) throws IOException {
+ Indexer indexer = new Indexer();
+ for (Class> clazz : classes) {
+ try (InputStream stream = MethodNameParserTest.class.getClassLoader()
+ .getResourceAsStream(clazz.getName().replace('.', '/') + ".class")) {
+ indexer.index(stream);
+ }
+ }
+ return indexer.complete();
+ }
+}
diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/Person.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/Person.java
new file mode 100644
index 0000000000000..ecd339b23be09
--- /dev/null
+++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/Person.java
@@ -0,0 +1,33 @@
+package io.quarkus.spring.data.deployment;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+
+@Entity
+public class Person {
+ @Id
+ private Integer id;
+
+ private Address address;
+
+ private String addressCountry;
+
+ @Entity
+ public static class Address {
+ @Id
+ private Integer id;
+
+ private String zipCode;
+
+ private Country country;
+ }
+
+ @Entity
+ public static class Country {
+ @Id
+ private Integer id;
+
+ private String isoCode;
+ }
+
+}
diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/PersonRepository.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/PersonRepository.java
new file mode 100644
index 0000000000000..8f8f32c5f27a2
--- /dev/null
+++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/PersonRepository.java
@@ -0,0 +1,25 @@
+package io.quarkus.spring.data.deployment;
+
+import java.util.List;
+
+import org.springframework.data.repository.Repository;
+
+// issue 13067:
+public interface PersonRepository extends Repository {
+
+ List findAllByAddressZipCode(String zipCode);
+
+ List findAllByAddressCountry(String zipCode);
+
+ List findAllByAddress_Country(String zipCode);
+
+ List findAllByAddressCountryIsoCode(String zipCode);
+
+ List findAllByAddress_CountryIsoCode(String zipCode);
+
+ List findAllByAddress_Country_IsoCode(String zipCode);
+
+ List findAllByAddress_CountryInvalid(String zipCode);
+
+ List findAllBy_(String zipCode);
+}
diff --git a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/Employee.java b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/Employee.java
new file mode 100644
index 0000000000000..1e693b2af1fb3
--- /dev/null
+++ b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/Employee.java
@@ -0,0 +1,68 @@
+package io.quarkus.it.spring.data.jpa;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "employee")
+public class Employee extends AbstractEntity {
+
+ @Column(name = "user_id")
+ private String userId;
+
+ @Column(name = "first_name")
+ private String firstName;
+
+ @Column(name = "last_name")
+ private String lastName;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "team_id", nullable = false)
+ private Team belongsToTeam;
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public Team getBelongsToTeam() {
+ return belongsToTeam;
+ }
+
+ @Entity
+ @Table(name = "team")
+ public static class Team extends AbstractEntity {
+
+ private String name;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "unit_id", nullable = false)
+ private OrgUnit organizationalUnit;
+
+ public String getName() {
+ return name;
+ }
+
+ public OrgUnit getOrganizationalUnit() {
+ return organizationalUnit;
+ }
+ }
+
+ @Entity
+ @Table(name = "unit")
+ public static class OrgUnit extends AbstractEntity {
+
+ private String name;
+ }
+}
diff --git a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/EmployeeRepository.java b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/EmployeeRepository.java
new file mode 100644
index 0000000000000..48db9eac590d5
--- /dev/null
+++ b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/EmployeeRepository.java
@@ -0,0 +1,10 @@
+package io.quarkus.it.spring.data.jpa;
+
+import java.util.List;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface EmployeeRepository extends JpaRepository {
+
+ List findByBelongsToTeamOrganizationalUnitName(String orgUnitName);
+}
diff --git a/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/EmployeeResource.java b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/EmployeeResource.java
new file mode 100644
index 0000000000000..0fc23145dd80f
--- /dev/null
+++ b/integration-tests/spring-data-jpa/src/main/java/io/quarkus/it/spring/data/jpa/EmployeeResource.java
@@ -0,0 +1,36 @@
+package io.quarkus.it.spring.data.jpa;
+
+import java.util.List;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+
+@Path("/employee")
+@Produces("application/json")
+public class EmployeeResource {
+
+ private final EmployeeRepository employeeRepository;
+
+ public EmployeeResource(EmployeeRepository employeeRepository) {
+ this.employeeRepository = employeeRepository;
+ }
+
+ @GET
+ public List findAll() {
+ return this.employeeRepository.findAll();
+ }
+
+ @GET
+ @Path("/{id}")
+ public Employee findById(@PathParam("id") Long id) {
+ return this.employeeRepository.findById(id).orElse(null);
+ }
+
+ @GET
+ @Path("/unit/{orgUnitName}")
+ public List findByManagerOfManager(@PathParam("orgUnitName") String orgUnitName) {
+ return this.employeeRepository.findByBelongsToTeamOrganizationalUnitName(orgUnitName);
+ }
+}
diff --git a/integration-tests/spring-data-jpa/src/main/resources/import.sql b/integration-tests/spring-data-jpa/src/main/resources/import.sql
index c6b0da8d56c9f..e4668f9a3c317 100644
--- a/integration-tests/spring-data-jpa/src/main/resources/import.sql
+++ b/integration-tests/spring-data-jpa/src/main/resources/import.sql
@@ -69,3 +69,11 @@ INSERT INTO cart(id, customer_id, status) VALUES (3, 3, 'CANCELED');
INSERT INTO orders(id, cart_id) VALUES (1, 1);
INSERT INTO orders(id, cart_id) VALUES (2, 2);
+
+INSERT INTO unit(id, name) VALUES (1, 'Delivery Unit');
+INSERT INTO unit(id, name) VALUES (2, 'Sales and Marketing Unit');
+INSERT INTO team(id, name, unit_id) VALUES (10, 'Development Team', 1);
+INSERT INTO team(id, name, unit_id) VALUES (11, 'Sales Team', 2);
+INSERT INTO employee(id, user_id, first_name, last_name, team_id) VALUES (100, 'johdoe', 'John', 'Doe', 10);
+INSERT INTO employee(id, user_id, first_name, last_name, team_id) VALUES (101, 'petdig', 'Peter', 'Digger', 10);
+INSERT INTO employee(id, user_id, first_name, last_name, team_id) VALUES (102, 'stesmi', 'Stella', 'Smith', 11);
\ No newline at end of file
diff --git a/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/EmployeeResourceIT.java b/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/EmployeeResourceIT.java
new file mode 100644
index 0000000000000..835616c144cc9
--- /dev/null
+++ b/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/EmployeeResourceIT.java
@@ -0,0 +1,7 @@
+package io.quarkus.it.spring.data.jpa;
+
+import io.quarkus.test.junit.NativeImageTest;
+
+@NativeImageTest
+public class EmployeeResourceIT extends EmployeeResourceTest {
+}
\ No newline at end of file
diff --git a/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/EmployeeResourceTest.java b/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/EmployeeResourceTest.java
new file mode 100644
index 0000000000000..34fb73e079368
--- /dev/null
+++ b/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/EmployeeResourceTest.java
@@ -0,0 +1,24 @@
+package io.quarkus.it.spring.data.jpa;
+
+import static io.restassured.RestAssured.when;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+
+@QuarkusTest
+public class EmployeeResourceTest {
+
+ @Test
+ public void testFindEmployeesByOrganizationalUnit() {
+ List employees = when().get("/employee/unit/Delivery Unit").then()
+ .statusCode(200)
+ .extract().body().jsonPath().getList(".", Employee.class);
+
+ assertThat(employees).extracting("userId").containsExactlyInAnyOrder("johdoe", "petdig");
+ }
+
+}