Skip to content

Commit

Permalink
Support user methods with @transactional in REST Data with Panache ORM
Browse files Browse the repository at this point in the history
Since the annotation `@Transactional` is ignored when used in default methods, we need to copy the default method from the interface into the generated resource. After these changes, we can now use the `@Transacational` annotation. 

Note that this solution is based on what REST Client also does. 

Fix quarkusio#35107
  • Loading branch information
Sgitario committed Aug 1, 2023
1 parent cc2ebbc commit efe7b41
Show file tree
Hide file tree
Showing 12 changed files with 124 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ void findEntityResources(CombinedIndexBuildItem index,
ResourceImplementor resourceImplementor = new ResourceImplementor(new EntityClassHelper(index.getComputingIndex()));
ClassOutput classOutput = new GeneratedBeanGizmoAdaptor(implementationsProducer);

for (ClassInfo classInfo : index.getComputingIndex().getKnownDirectImplementors(PANACHE_ENTITY_RESOURCE_INTERFACE)) {
validateResource(index.getComputingIndex(), classInfo);
for (ClassInfo resourceInterface : index.getComputingIndex()
.getKnownDirectImplementors(PANACHE_ENTITY_RESOURCE_INTERFACE)) {
validateResource(index.getComputingIndex(), resourceInterface);

List<Type> generics = getGenericTypes(classInfo);
String resourceInterface = classInfo.name().toString();
List<Type> generics = getGenericTypes(resourceInterface);
String entityType = generics.get(0).name().toString();
String idType = generics.get(1).name().toString();

Expand Down Expand Up @@ -120,12 +120,11 @@ void findRepositoryResources(CombinedIndexBuildItem index,
ResourceImplementor resourceImplementor = new ResourceImplementor(new EntityClassHelper(index.getComputingIndex()));
ClassOutput classOutput = new GeneratedBeanGizmoAdaptor(implementationsProducer);

for (ClassInfo classInfo : index.getComputingIndex()
for (ClassInfo resourceInterface : index.getComputingIndex()
.getKnownDirectImplementors(PANACHE_REPOSITORY_RESOURCE_INTERFACE)) {
validateResource(index.getComputingIndex(), classInfo);
validateResource(index.getComputingIndex(), resourceInterface);

List<Type> generics = getGenericTypes(classInfo);
String resourceInterface = classInfo.name().toString();
List<Type> generics = getGenericTypes(resourceInterface);
String repositoryClassName = generics.get(0).name().toString();
String entityType = generics.get(1).name().toString();
String idType = generics.get(2).name().toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ class ResourceImplementor {
* Implements {@link io.quarkus.rest.data.panache.RestDataResource} interfaces defined in a user application.
* Instances of this class are registered as beans and are later used in the generated JAX-RS controllers.
*/
String implement(ClassOutput classOutput, DataAccessImplementor dataAccessImplementor, String resourceType,
String implement(ClassOutput classOutput, DataAccessImplementor dataAccessImplementor, ClassInfo resourceInterface,
String entityType, List<ClassInfo> resourceMethodListeners) {
String resourceType = resourceInterface.name().toString();
String className = resourceType + "Impl_" + HashUtil.sha1(resourceType);
LOGGER.tracef("Starting generation of '%s'", className);
ClassCreator classCreator = ClassCreator.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import java.util.Collections;
import java.util.List;

import jakarta.transaction.Transactional;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;

Expand All @@ -22,4 +24,15 @@ default Collection findByName(@PathParam("name") String name) {

return collections.get(0);
}

@Transactional
@POST
@Path("/name/{name}")
default Collection addByName(@PathParam("name") String name) {
Collection collection = new Collection();
collection.id = name;
collection.name = name;
Collection.persist(collection);
return collection;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package io.quarkus.hibernate.orm.rest.data.panache.deployment.entity;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.is;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.hibernate.orm.rest.data.panache.deployment.AbstractPostMethodTest;
Expand All @@ -14,4 +18,13 @@ class PanacheEntityResourcePostMethodTest extends AbstractPostMethodTest {
Item.class, ItemsResource.class)
.addAsResource("application.properties")
.addAsResource("import.sql"));

@Test
void shouldCopyUserMethodsAnnotatedWithTransactional() {
given().accept("application/json")
.when().post("/collections/name/mycollection")
.then().statusCode(200)
.and().body("id", is("mycollection"))
.and().body("name", is("mycollection"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,10 @@ void findEntityResources(CombinedIndexBuildItem indexBuildItem,
ResourceImplementor resourceImplementor = new ResourceImplementor(new EntityClassHelper(index));
ClassOutput classOutput = new GeneratedBeanGizmoAdaptor(implementationsProducer);

for (ClassInfo classInfo : index.getKnownDirectImplementors(PANACHE_ENTITY_RESOURCE_INTERFACE)) {
validateResource(index, classInfo);
for (ClassInfo resourceInterface : index.getKnownDirectImplementors(PANACHE_ENTITY_RESOURCE_INTERFACE)) {
validateResource(index, resourceInterface);

List<Type> generics = getGenericTypes(classInfo);
String resourceInterface = classInfo.name().toString();
List<Type> generics = getGenericTypes(resourceInterface);
String entityType = generics.get(0).name().toString();
String idType = generics.get(1).name().toString();

Expand Down Expand Up @@ -105,11 +104,10 @@ void findRepositoryResources(CombinedIndexBuildItem indexBuildItem,
ResourceImplementor resourceImplementor = new ResourceImplementor(new EntityClassHelper(index));
ClassOutput classOutput = new GeneratedBeanGizmoAdaptor(implementationsProducer);

for (ClassInfo classInfo : index.getKnownDirectImplementors(PANACHE_REPOSITORY_RESOURCE_INTERFACE)) {
validateResource(index, classInfo);
for (ClassInfo resourceInterface : index.getKnownDirectImplementors(PANACHE_REPOSITORY_RESOURCE_INTERFACE)) {
validateResource(index, resourceInterface);

List<Type> generics = getGenericTypes(classInfo);
String resourceInterface = classInfo.name().toString();
List<Type> generics = getGenericTypes(resourceInterface);
String repositoryClassName = generics.get(0).name().toString();
String entityType = generics.get(1).name().toString();
String idType = generics.get(2).name().toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ class ResourceImplementor {
* Implements {@link io.quarkus.rest.data.panache.ReactiveRestDataResource} interfaces defined in a user application.
* Instances of this class are registered as beans and are later used in the generated JAX-RS controllers.
*/
String implement(ClassOutput classOutput, DataAccessImplementor dataAccessImplementor, String resourceType,
String implement(ClassOutput classOutput, DataAccessImplementor dataAccessImplementor, ClassInfo resourceInterface,
String entityType, List<ClassInfo> resourceMethodListeners) {
String resourceType = resourceInterface.name().toString();
String className = resourceType + "Impl_" + HashUtil.sha1(resourceType);
LOGGER.tracef("Starting generation of '%s'", className);
ClassCreator classCreator = ClassCreator.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,11 @@ void findEntityResources(CombinedIndexBuildItem index, Capabilities capabilities
ResourceImplementor resourceImplementor = new ResourceImplementor(entityClassHelper);
ClassOutput classOutput = new GeneratedBeanGizmoAdaptor(implementationsProducer);

for (ClassInfo classInfo : index.getComputingIndex()
for (ClassInfo resourceInterface : index.getComputingIndex()
.getKnownDirectImplementors(PANACHE_MONGO_ENTITY_RESOURCE_INTERFACE)) {
validateResource(index.getComputingIndex(), classInfo);
validateResource(index.getComputingIndex(), resourceInterface);

List<Type> generics = getGenericTypes(classInfo);
String resourceInterface = classInfo.name().toString();
List<Type> generics = getGenericTypes(resourceInterface);
String entityType = generics.get(0).toString();
String idType = generics.get(1).toString();

Expand Down Expand Up @@ -109,12 +108,11 @@ void findRepositoryResources(CombinedIndexBuildItem index, Capabilities capabili
ResourceImplementor resourceImplementor = new ResourceImplementor(entityClassHelper);
ClassOutput classOutput = new GeneratedBeanGizmoAdaptor(implementationsProducer);

for (ClassInfo classInfo : index.getComputingIndex()
for (ClassInfo resourceInterface : index.getComputingIndex()
.getKnownDirectImplementors(PANACHE_MONGO_REPOSITORY_RESOURCE_INTERFACE)) {
validateResource(index.getComputingIndex(), classInfo);
validateResource(index.getComputingIndex(), resourceInterface);

List<Type> generics = getGenericTypes(classInfo);
String resourceInterface = classInfo.name().toString();
List<Type> generics = getGenericTypes(resourceInterface);
String repositoryClassName = generics.get(0).toString();
String entityType = generics.get(1).toString();
String idType = generics.get(2).toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import jakarta.enterprise.context.ApplicationScoped;

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.FieldInfo;
import org.jboss.logging.Logger;

Expand Down Expand Up @@ -37,8 +38,9 @@ class ResourceImplementor {
this.entityClassHelper = entityClassHelper;
}

String implement(ClassOutput classOutput, DataAccessImplementor dataAccessImplementor, String resourceType,
String implement(ClassOutput classOutput, DataAccessImplementor dataAccessImplementor, ClassInfo resourceInterface,
String entityType) {
String resourceType = resourceInterface.name().toString();
String className = resourceType + "Impl_" + HashUtil.sha1(resourceType);
LOGGER.tracef("Starting generation of '%s'", className);
ClassCreator classCreator = ClassCreator.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import io.quarkus.rest.data.panache.deployment.methods.ListMethodImplementor;
import io.quarkus.rest.data.panache.deployment.methods.MethodImplementor;
import io.quarkus.rest.data.panache.deployment.methods.UpdateMethodImplementor;
import io.quarkus.rest.data.panache.deployment.methods.UserMethodsWithTransactionalImplementor;
import io.quarkus.rest.data.panache.deployment.methods.hal.ListHalMethodImplementor;
import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties;
import io.quarkus.runtime.util.HashUtil;
Expand All @@ -48,6 +49,7 @@ class JaxRsResourceImplementor {
new AddMethodImplementor(capabilities),
new UpdateMethodImplementor(capabilities),
new DeleteMethodImplementor(capabilities),
new UserMethodsWithTransactionalImplementor(capabilities),
// The list hal endpoint needs to be added for both resteasy classic and resteasy reactive
// because the pagination links are programmatically added.
new ListHalMethodImplementor(capabilities));
Expand Down Expand Up @@ -77,7 +79,7 @@ void implement(ClassOutput classOutput, ResourceMetadata resourceMetadata, Resou
.classOutput(classOutput).className(controllerClassName);

if (resourceMetadata.getResourceInterface() != null) {
classCreatorBuilder.interfaces(resourceMetadata.getResourceInterface());
classCreatorBuilder.interfaces(resourceMetadata.getResourceInterface().name().toString());
}

ClassCreator classCreator = classCreatorBuilder.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Map;

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.Type;

public class ResourceMetadata {
Expand All @@ -19,7 +20,7 @@ public class ResourceMetadata {
/**
* Application interface that extends RestDataResource interface.
*/
private final String resourceInterface;
private final ClassInfo resourceInterface;

/**
* Entity class that is used by the resource.
Expand All @@ -36,12 +37,12 @@ public class ResourceMetadata {
*/
private final Map<String, Type> fields;

public ResourceMetadata(String resourceClass, String resourceInterface, String entityType, String idType,
public ResourceMetadata(String resourceClass, ClassInfo resourceInterface, String entityType, String idType,
Map<String, Type> fields) {
this(resourceClass, resourceInterface, resourceInterface, entityType, idType, fields);
this(resourceClass, resourceInterface.name().toString(), resourceInterface, entityType, idType, fields);
}

public ResourceMetadata(String resourceClass, String resourceName, String resourceInterface, String entityType,
public ResourceMetadata(String resourceClass, String resourceName, ClassInfo resourceInterface, String entityType,
String idType, Map<String, Type> fields) {
this.resourceClass = resourceClass;
this.resourceName = resourceName;
Expand All @@ -59,7 +60,7 @@ public String getResourceName() {
return resourceName;
}

public String getResourceInterface() {
public ClassInfo getResourceInterface() {
return resourceInterface;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ void implementResources(CombinedIndexBuildItem index,
ResourcePropertiesProvider resourcePropertiesProvider = new ResourcePropertiesProvider(index.getIndex());

for (RestDataResourceBuildItem resourceBuildItem : resourceBuildItems) {
if (!excludedClasses.contains(resourceBuildItem.getResourceMetadata().getResourceInterface())) {
if (!excludedClasses.contains(resourceBuildItem.getResourceMetadata().getResourceName())) {
ResourceMetadata resourceMetadata = resourceBuildItem.getResourceMetadata();
ResourceProperties resourceProperties = getResourceProperties(resourcePropertiesProvider,
resourceMetadata, resourcePropertiesBuildItems);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.quarkus.rest.data.panache.deployment.methods;

import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.DotName;

import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.FieldDescriptor;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.rest.data.panache.deployment.ResourceMetadata;
import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties;

/**
* Propagate the user methods annotated with `@Transactional`.
* This implementor is only used if Hibernate ORM is present.
*/
public final class UserMethodsWithTransactionalImplementor implements MethodImplementor {

public static final DotName TRANSACTIONAL = DotName.createSimple("jakarta.transaction.Transactional");

private final Capabilities capabilities;

public UserMethodsWithTransactionalImplementor(Capabilities capabilities) {
this.capabilities = capabilities;
}

@Override
public void implement(ClassCreator classCreator, ResourceMetadata resourceMetadata,
ResourceProperties resourceProperties, FieldDescriptor resourceField) {
if (capabilities.isPresent(Capability.HIBERNATE_ORM) && resourceMetadata.getResourceInterface() != null) {
for (var methodInfo : resourceMetadata.getResourceInterface().methods()) {
// we only need to propagate the user methods annotated with `@Transactional`
if (methodInfo.hasAnnotation(TRANSACTIONAL)) {
MethodCreator methodCreator = classCreator.getMethodCreator(MethodDescriptor.of(methodInfo));
methodCreator.setSignature(methodInfo.genericSignatureIfRequired());
for (var annotation : methodInfo.annotations()) {
if (annotation.target().kind() == AnnotationTarget.Kind.METHOD_PARAMETER) {
short position = annotation.target().asMethodParameter().position();
methodCreator.getParameterAnnotations(position).addAnnotation(annotation);
}

if (annotation.target().kind() == AnnotationTarget.Kind.METHOD) {
methodCreator.addAnnotation(annotation);
}
}

ResultHandle[] params = new ResultHandle[methodInfo.parametersCount()];
for (int paramIdx = 0; paramIdx < methodInfo.parametersCount(); paramIdx++) {
params[paramIdx] = methodCreator.getMethodParam(paramIdx);
}

methodCreator.returnValue(
methodCreator.invokeSpecialInterfaceMethod(methodInfo, methodCreator.getThis(), params));
methodCreator.close();
}
}
}
}
}

0 comments on commit efe7b41

Please sign in to comment.