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"); + } + +}