diff --git a/thunx-spring/build.gradle b/thunx-spring/build.gradle index a678d830..801489f4 100644 --- a/thunx-spring/build.gradle +++ b/thunx-spring/build.gradle @@ -27,7 +27,6 @@ dependencies { compileOnly 'org.springframework.data:spring-data-jpa' compileOnly 'org.springframework.data:spring-data-rest-core' compileOnly 'org.springframework.data:spring-data-rest-webmvc' - compileOnly 'com.github.paulcwarren:spring-content-rest:2.1.0' compileOnly 'jakarta.persistence:jakarta.persistence-api' compileOnly 'javax.servlet:javax.servlet-api' compileOnly 'com.querydsl:querydsl-core' @@ -37,11 +36,14 @@ dependencies { testRuntimeOnly 'org.springframework.cloud:spring-cloud-gateway-server' testRuntimeOnly "org.springframework.security:spring-security-web" + testImplementation "org.springframework.data:spring-data-commons" + testImplementation 'jakarta.persistence:jakarta.persistence-api' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'net.javacrumbs.json-unit:json-unit-assertj:2.33.0' + testImplementation 'org.springframework.security:spring-security-oauth2-core' } diff --git a/thunx-spring/src/main/java/eu/xenit/contentcloud/thunx/spring/data/rest/AbacRepositoryInvokerAdapter.java b/thunx-spring/src/main/java/eu/xenit/contentcloud/thunx/spring/data/rest/AbacRepositoryInvokerAdapter.java index 40f0a24e..4503291c 100644 --- a/thunx-spring/src/main/java/eu/xenit/contentcloud/thunx/spring/data/rest/AbacRepositoryInvokerAdapter.java +++ b/thunx-spring/src/main/java/eu/xenit/contentcloud/thunx/spring/data/rest/AbacRepositoryInvokerAdapter.java @@ -6,13 +6,9 @@ import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.PathBuilder; import eu.xenit.contentcloud.thunx.spring.data.context.AbacContext; -import java.lang.reflect.Field; -import java.util.Optional; -import javax.persistence.Id; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; -import org.springframework.content.commons.utils.BeanUtils; -import org.springframework.content.commons.utils.DomainObjectUtils; +import org.springframework.beans.BeansException; import org.springframework.core.convert.ConversionService; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.QuerydslRepositoryInvokerAdapter; @@ -23,6 +19,12 @@ import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Optional; +import java.util.stream.Stream; public class AbacRepositoryInvokerAdapter extends QuerydslRepositoryInvokerAdapter { @@ -139,8 +141,68 @@ private String toAlias(Class subjectType) { } private BooleanExpression idExpr(Object id, PathBuilder entityPath) { - Field idField = BeanUtils.findFieldWithAnnotation(domainType, Id.class); + Field idField = DomainObjectUtils.getIdField(domainType); PathBuilder idPath = entityPath.get(idField.getName(), id.getClass()); return idPath.eq(Expressions.constant(id)); } + + + static class DomainObjectUtils { + + private static final boolean JAVAX_PERSISTENCE_PRESENT = ClassUtils.isPresent( + "javax.persistence.Id", DomainObjectUtils.class.getClassLoader()); + + static final Field getIdField(Class domainClass) { + + // Looking for @javax.persistence.Id + if (JAVAX_PERSISTENCE_PRESENT) { + var jpaIdField = DomainObjectUtils.findFieldWithAnnotation(domainClass, javax.persistence.Id.class); + if (jpaIdField.isPresent()) { + return jpaIdField.get(); + } + } + + // Looking for @org.springframework.data.annotation.Id + var springDataId = DomainObjectUtils.findFieldWithAnnotation(domainClass, org.springframework.data.annotation.Id.class); + if (springDataId.isPresent()) { + return springDataId.get(); + } + + // None found + return null; + } + + private static Optional findFieldWithAnnotation(Class domainObjClass, + Class annotationClass) + throws SecurityException, BeansException { + + // First look for the annotation on the accessor methods + BeanWrapper wrapper = new BeanWrapperImpl(domainObjClass); + return Stream.of(wrapper.getPropertyDescriptors()) + .map(descriptor -> findFieldByName(domainObjClass, descriptor.getName())) + .filter(Optional::isPresent) + .map(Optional::get) + .filter(field -> field.isAnnotationPresent(annotationClass)) + .findFirst() + + // Otherwise look for the annotation on the fields directly + .or(() -> allFields(domainObjClass) + .filter(field -> field.isAnnotationPresent(annotationClass)) + .findFirst()); + + + } + + private static Optional findFieldByName(Class type, String fieldName) { + return allFields(type) + .filter(field -> field.getName().equals(fieldName)) + .findFirst(); + } + + private static Stream allFields(Class type) { + return Stream.concat( + Stream.of(type.getDeclaredFields()), + type.getSuperclass() != null ? allFields(type.getSuperclass()) : Stream.empty()); + } + } } diff --git a/thunx-spring/src/test/java/eu/xenit/contentcloud/thunx/spring/data/rest/AbacRepositoryInvokerAdapterTest.java b/thunx-spring/src/test/java/eu/xenit/contentcloud/thunx/spring/data/rest/AbacRepositoryInvokerAdapterTest.java new file mode 100644 index 00000000..4eeb5e4e --- /dev/null +++ b/thunx-spring/src/test/java/eu/xenit/contentcloud/thunx/spring/data/rest/AbacRepositoryInvokerAdapterTest.java @@ -0,0 +1,85 @@ +package eu.xenit.contentcloud.thunx.spring.data.rest; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class AbacRepositoryInvokerAdapterTest { + + static class DomainObjectUtilsTest { + + @Test + void long_jpaAnnotated_idField() { + var idField = AbacRepositoryInvokerAdapter.DomainObjectUtils.getIdField(JpaEntity.class); + assertThat(idField).isNotNull(); + assertThat(idField.getName()).isEqualTo("myId"); + } + + @Test + @Disabled("@Id on getter method not supported (ported as-is from spring-content)") + void long_jpaAnnotated_idGetter() { + var idField = AbacRepositoryInvokerAdapter.DomainObjectUtils.getIdField(AccessorJpaEntity.class); + assertThat(idField).isNotNull(); + assertThat(idField.getName()).isEqualTo("myId"); + } + + @Test + void jpa_customIdType() { + var idField = AbacRepositoryInvokerAdapter.DomainObjectUtils.getIdField(JpaEntityWithValueObjectId.class); + assertThat(idField).isNotNull(); + assertThat(idField.getType()).isEqualTo(CustomIdClass.class); + } + + @Test + void long_springAnnotated_idField() { + var idField = AbacRepositoryInvokerAdapter.DomainObjectUtils.getIdField(SpringDataEntity.class); + assertThat(idField).isNotNull(); + assertThat(idField.getName()).isEqualTo("otherId"); + } + + @Test + void id_from_subclass() { + var idField = AbacRepositoryInvokerAdapter.DomainObjectUtils.getIdField(JpaSubClass.class); + assertThat(idField).isNotNull(); + assertThat(idField.getName()).isEqualTo("myId"); + } + + + static class JpaEntity { + @javax.persistence.Id + private Long myId; + } + + static class JpaSubClass extends JpaEntity { + + } + + static class CustomIdClass { + private String value; + } + + static class JpaEntityWithValueObjectId { + @javax.persistence.Id + private CustomIdClass id; + } + + static class AccessorJpaEntity { + + private Long myId; + + @javax.persistence.Id + public Long getMyId() { + return this.myId; + } + + } + + static class SpringDataEntity { + @org.springframework.data.annotation.Id + private Long otherId; + } + + } + +} \ No newline at end of file