diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/ReactiveSessionFactoryProducer.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/ReactiveSessionFactoryProducer.java index e2a664cd5a8ec4..6b63c95042af98 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/ReactiveSessionFactoryProducer.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/ReactiveSessionFactoryProducer.java @@ -7,6 +7,7 @@ import javax.persistence.EntityManagerFactory; import javax.persistence.PersistenceUnit; +import org.hibernate.reactive.common.spi.Implementor; import org.hibernate.reactive.common.spi.MutinyImplementor; import org.hibernate.reactive.mutiny.Mutiny; import org.hibernate.reactive.mutiny.impl.MutinySessionFactoryImpl; @@ -28,7 +29,7 @@ public class ReactiveSessionFactoryProducer { @ApplicationScoped @DefaultBean @Unremovable - @Typed({ Mutiny.SessionFactory.class, MutinyImplementor.class }) + @Typed({ Mutiny.SessionFactory.class, MutinyImplementor.class, Implementor.class }) public MutinySessionFactoryImpl mutinySessionFactory() { if (jpaConfig.getDeactivatedPersistenceUnitNames() .contains(HibernateReactive.DEFAULT_REACTIVE_PERSISTENCE_UNIT_NAME)) { diff --git a/extensions/panache/hibernate-reactive-panache-common/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheJpaCommonResourceProcessor.java b/extensions/panache/hibernate-reactive-panache-common/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheJpaCommonResourceProcessor.java index 338213dcf98a6c..78406b55b70354 100644 --- a/extensions/panache/hibernate-reactive-panache-common/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheJpaCommonResourceProcessor.java +++ b/extensions/panache/hibernate-reactive-panache-common/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheJpaCommonResourceProcessor.java @@ -1,9 +1,12 @@ package io.quarkus.hibernate.reactive.panache.common.deployment; +import java.lang.reflect.Modifier; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import javax.annotation.Priority; @@ -12,14 +15,24 @@ import javax.persistence.NamedQuery; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget.Kind; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; +import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; +import io.quarkus.arc.deployment.ValidationPhaseBuildItem; +import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; +import io.quarkus.arc.processor.Annotations; +import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.arc.processor.BeanInfo; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.IsTest; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -31,9 +44,15 @@ import io.quarkus.gizmo.ClassCreator; import io.quarkus.hibernate.orm.deployment.HibernateOrmEnabled; import io.quarkus.hibernate.orm.deployment.JpaModelBuildItem; +import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.quarkus.hibernate.reactive.panache.common.WithSessionOnDemand; import io.quarkus.hibernate.reactive.panache.common.runtime.PanacheHibernateRecorder; +import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactional; import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactionalInterceptor; import io.quarkus.hibernate.reactive.panache.common.runtime.TestReactiveTransactionalInterceptor; +import io.quarkus.hibernate.reactive.panache.common.runtime.WithSessionInterceptor; +import io.quarkus.hibernate.reactive.panache.common.runtime.WithSessionOnDemandInterceptor; +import io.smallrye.mutiny.Uni; @BuildSteps(onlyIf = HibernateOrmEnabled.class) public final class PanacheJpaCommonResourceProcessor { @@ -42,6 +61,18 @@ public final class PanacheJpaCommonResourceProcessor { private static final DotName DOTNAME_NAMED_QUERIES = DotName.createSimple(NamedQueries.class.getName()); private static final String TEST_REACTIVE_TRANSACTION = "io.quarkus.test.TestReactiveTransaction"; + private static final DotName REACTIVE_TRANSACTIONAL = DotName.createSimple(ReactiveTransactional.class.getName()); + private static final DotName WITH_SESSION_IF_NEEDED = DotName.createSimple(WithSessionOnDemand.class.getName()); + private static final DotName WITH_SESSION = DotName.createSimple(WithSession.class.getName()); + private static final DotName UNI = DotName.createSimple(Uni.class.getName()); + private static final DotName PANACHE_ENTITY_BASE = DotName + .createSimple("io.quarkus.hibernate.reactive.panache.PanacheEntityBase"); + private static final DotName PANACHE_ENTITY = DotName.createSimple("io.quarkus.hibernate.reactive.panache.PanacheEntity"); + private static final DotName PANACHE_KOTLIN_ENTITY_BASE = DotName + .createSimple("io.quarkus.hibernate.reactive.panache.kotlin.PanacheEntityBase"); + private static final DotName PANACHE_KOTLIN_ENTITY = DotName + .createSimple("io.quarkus.hibernate.reactive.panache.kotlin.PanacheEntity"); + @BuildStep(onlyIf = IsTest.class) void testTx(BuildProducer generatedBeanBuildItemBuildProducer, BuildProducer additionalBeans) { @@ -61,12 +92,97 @@ void testTx(BuildProducer generatedBeanBuildItemBuildPro } @BuildStep - void registerInterceptor(BuildProducer additionalBeans) { + void registerInterceptors(BuildProducer additionalBeans) { AdditionalBeanBuildItem.Builder builder = AdditionalBeanBuildItem.builder(); + builder.addBeanClass(WithSessionOnDemandInterceptor.class); + builder.addBeanClass(WithSessionInterceptor.class); builder.addBeanClass(ReactiveTransactionalInterceptor.class); additionalBeans.produce(builder.build()); } + @BuildStep + void validateInterceptedMethods(ValidationPhaseBuildItem validationPhase, + BuildProducer errors) { + List bindings = List.of(REACTIVE_TRANSACTIONAL, WITH_SESSION, WITH_SESSION_IF_NEEDED); + for (BeanInfo bean : validationPhase.getContext().beans().withAroundInvokeInterceptor()) { + for (Entry> e : bean.getInterceptedMethodsBindings().entrySet()) { + if (e.getKey().returnType().name().equals(UNI)) { + // Method returns Uni - no need to iterate over the bindings + continue; + } + if (Annotations.containsAny(e.getValue(), bindings)) { + errors.produce(new ValidationErrorBuildItem( + new IllegalStateException( + "A method annotated with @ReactiveTransactional, @WithSession or @WithSessionIfNeeded must return Uni: " + + e.getKey()))); + } + } + } + } + + @BuildStep + void transformResourceMethods(CombinedIndexBuildItem index, Capabilities capabilities, + BuildProducer annotationsTransformer) { + if (capabilities.isPresent(Capability.RESTEASY_REACTIVE)) { + // Custom request method designators are not supported + List designators = List.of(DotName.createSimple("javax.ws.rs.GET"), + DotName.createSimple("javax.ws.rs.HEAD"), + DotName.createSimple("javax.ws.rs.DELETE"), DotName.createSimple("javax.ws.rs.OPTIONS"), + DotName.createSimple("javax.ws.rs.PATCH"), DotName.createSimple("javax.ws.rs.POST"), + DotName.createSimple("javax.ws.rs.PUT")); + List bindings = List.of(REACTIVE_TRANSACTIONAL, WITH_SESSION, WITH_SESSION_IF_NEEDED); + + // Collect all panache entities + Set entities = new HashSet<>(); + for (ClassInfo subclass : index.getIndex().getAllKnownSubclasses(PANACHE_ENTITY_BASE)) { + if (!subclass.name().equals(PANACHE_ENTITY)) { + entities.add(subclass.name()); + } + } + for (ClassInfo subclass : index.getIndex().getAllKnownSubclasses(PANACHE_KOTLIN_ENTITY_BASE)) { + if (!subclass.name().equals(PANACHE_KOTLIN_ENTITY)) { + entities.add(subclass.name()); + } + } + Set entityUsers = new HashSet<>(); + for (DotName entity : entities) { + for (ClassInfo user : index.getIndex().getKnownUsers(entity)) { + entityUsers.add(user.name()); + } + } + + annotationsTransformer.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { + @Override + public boolean appliesTo(Kind kind) { + return kind == Kind.METHOD; + } + + @Override + public void transform(TransformationContext context) { + MethodInfo method = context.getTarget().asMethod(); + Collection annotations = context.getAnnotations(); + if (method.isSynthetic() + || Modifier.isStatic(method.flags()) + || method.declaringClass().isInterface() + || !method.returnType().name().equals(UNI) + || !entityUsers.contains(method.declaringClass().name()) + || !Annotations.containsAny(annotations, designators) + || Annotations.containsAny(annotations, bindings)) { + return; + } + // Add @WithSessionIfNeeded to a method that + // - is not static + // - is not synthetic + // - returns Uni + // - is declared in a class that uses a panache entity + // - is annotated with @GET, @POST, @PUT, @DELETE ,@PATCH ,@HEAD or @OPTIONS + // - is not annotated with @ReactiveTransactional, @WithSession or @WithSessionIfNeeded + context.transform().add(WITH_SESSION_IF_NEEDED).done(); + } + })); + } + } + @BuildStep void lookupNamedQueries(CombinedIndexBuildItem index, BuildProducer namedQueries, diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithSession.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithSession.java new file mode 100644 index 00000000000000..f3f1f753d496a8 --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithSession.java @@ -0,0 +1,29 @@ +package io.quarkus.hibernate.reactive.panache.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.interceptor.InterceptorBinding; + +import org.hibernate.reactive.mutiny.Mutiny; + +import io.smallrye.mutiny.Uni; + +/** + * Instructs Panache to trigger the {@link Uni} returned from the intercepted method within a scope of a reactive + * {@link Mutiny.Session}. If a reactive session exists when the {@link Uni} returned from the annotated method is triggered, + * then this session is reused. Otherwise, a new session is opened and eventually closed when the {@link Uni} completes. + *

+ * A method annotated with this annotation must return {@link Uni}. If declared on a class then all methods that are intercepted + * must return {@link Uni}. + */ +@Inherited +@InterceptorBinding +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(value = RetentionPolicy.RUNTIME) +public @interface WithSession { + +} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithSessionOnDemand.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithSessionOnDemand.java new file mode 100644 index 00000000000000..26453f6391018e --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithSessionOnDemand.java @@ -0,0 +1,30 @@ +package io.quarkus.hibernate.reactive.panache.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.interceptor.InterceptorBinding; + +import org.hibernate.reactive.mutiny.Mutiny; + +import io.smallrye.mutiny.Uni; + +/** + * Instructs Panache to trigger the {@link Uni} returned from the intercepted method within a scope of a reactive + * {@link Mutiny.Session} (if needed). If a reactive session exists when the {@link Uni} returned from the annotated method is + * triggered, then this session is reused. Otherwise, a new session is opened when needed and eventually closed when the + * {@link Uni} completes. + *

+ * A method annotated with this annotation must return {@link Uni}. If declared on a class then all methods that are intercepted + * must return {@link Uni}. + */ +@Inherited +@InterceptorBinding +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(value = RetentionPolicy.RUNTIME) +public @interface WithSessionOnDemand { + +} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithTransaction.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithTransaction.java new file mode 100644 index 00000000000000..0a38d996da6a1a --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithTransaction.java @@ -0,0 +1,33 @@ +package io.quarkus.hibernate.reactive.panache.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.interceptor.InterceptorBinding; + +import org.hibernate.reactive.mutiny.Mutiny; +import org.hibernate.reactive.mutiny.Mutiny.SessionFactory; + +import io.smallrye.mutiny.Uni; + +/** + * Instructs Panache to trigger the {@link Uni} returned from the intercepted method within a scope of a reactive + * {@link Mutiny.Transaction}. If a reactive session exists when the {@link Uni} returned from the annotated method is + * triggered, then this session is reused. Otherwise, a new session is opened and eventually closed when the {@link Uni} + * completes. + *

+ * A method annotated with this annotation must return {@link Uni}. If declared on a class then all methods that are intercepted + * must return {@link Uni}. + * + * @see SessionFactory#withTransaction(java.util.function.Function) + */ +@Inherited +@InterceptorBinding +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(value = RetentionPolicy.RUNTIME) +public @interface WithTransaction { + +} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractJpaOperations.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractJpaOperations.java index feedef2dc2b1d1..368ea582927a46 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractJpaOperations.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractJpaOperations.java @@ -5,28 +5,19 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.stream.Stream; -import javax.enterprise.inject.spi.Bean; import javax.persistence.LockModeType; import org.hibernate.internal.util.LockModeConverter; import org.hibernate.reactive.mutiny.Mutiny; import org.hibernate.reactive.mutiny.Mutiny.Session; -import io.quarkus.arc.Arc; import io.quarkus.panache.common.Parameters; import io.quarkus.panache.common.Sort; import io.quarkus.panache.hibernate.common.runtime.PanacheJpaUtil; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; -import io.vertx.core.Vertx; public abstract class AbstractJpaOperations { @@ -34,29 +25,6 @@ public abstract class AbstractJpaOperations { static final long TIMEOUT_MS = 5000; private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; - private static void executeInVertxEventLoop(Runnable runnable) { - Vertx vertx = Arc.container().instance(Vertx.class).get(); - // this needs to be sync - CompletableFuture cf = new CompletableFuture<>(); - vertx.runOnContext(v -> { - try { - runnable.run(); - cf.complete(null); - } catch (Throwable t) { - cf.completeExceptionally(t); - } - }); - try { - cf.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new RuntimeException(e); - } - } - - private static Session lookupSessionFromArc() { - return Arc.container().instance(Session.class).get(); - } - protected abstract PanacheQueryType createPanacheQuery(Uni session, String query, String orderBy, Object paramsArrayOrMap); @@ -109,62 +77,14 @@ public Uni delete(Object entity) { } public boolean isPersistent(Object entity) { - // only attempt to look up the request context session if it's already there: do not - // run the producer method otherwise, before we know which thread we're on - Session requestSession = isInRequestContext(Mutiny.Session.class) ? lookupSessionFromArc() - : null; - if (requestSession != null) { - return requestSession.contains(entity); - } else { - return false; - } + Mutiny.Session current = SessionOperations.getCurrentSession(); + return current != null ? current.contains(entity) : false; } public Uni flush() { return getSession().chain(Session::flush); } - // - // Private stuff - - public static Uni getSession() { - // Always check if we're running on the event loop: if not, - // we need to delegate the execution of all tasks on it. - if (io.vertx.core.Context.isOnEventLoopThread()) { - return Uni.createFrom().item(lookupSessionFromArc()); - } else { - // FIXME: we may need context propagation - final Executor executor = AbstractJpaOperations::executeInVertxEventLoop; - return Uni.createFrom().item(AbstractJpaOperations::lookupSessionFromArc) - .runSubscriptionOn(executor); - } - } - - private static boolean isInRequestContext(Class klass) { - Set> beans = Arc.container().beanManager().getBeans(klass); - if (beans.isEmpty()) - return false; - return Arc.container().requestContext().get(beans.iterator().next()) != null; - } - - public static Mutiny.Query bindParameters(Mutiny.Query query, Object[] params) { - if (params == null || params.length == 0) - return query; - for (int i = 0; i < params.length; i++) { - query.setParameter(i + 1, params[i]); - } - return query; - } - - public static Mutiny.Query bindParameters(Mutiny.Query query, Map params) { - if (params == null || params.size() == 0) - return query; - for (Entry entry : params.entrySet()) { - query.setParameter(entry.getKey(), entry.getValue()); - } - return query; - } - public int paramCount(Object[] params) { return params != null ? params.length : 0; } @@ -421,22 +341,6 @@ public IllegalStateException implementationInjectionMissing() { "This method is normally automatically overridden in subclasses: did you forget to annotate your entity with @Entity?"); } - public static Uni executeUpdate(String query, Object... params) { - return getSession().chain(session -> { - Mutiny.Query jpaQuery = session.createQuery(query); - bindParameters(jpaQuery, params); - return jpaQuery.executeUpdate(); - }); - } - - public static Uni executeUpdate(String query, Map params) { - return getSession().chain(session -> { - Mutiny.Query jpaQuery = session.createQuery(query); - bindParameters(jpaQuery, params); - return jpaQuery.executeUpdate(); - }); - } - public Uni executeUpdate(Class entityClass, String query, Object... params) { if (PanacheJpaUtil.isNamedQuery(query)) @@ -474,4 +378,45 @@ public Uni update(Class entityClass, String query, Parameters params public Uni update(Class entityClass, String query, Object... params) { return executeUpdate(entityClass, query, params); } + + // + // Static helpers + + public static Uni getSession() { + return SessionOperations.getSession(); + } + + public static Mutiny.Query bindParameters(Mutiny.Query query, Object[] params) { + if (params == null || params.length == 0) + return query; + for (int i = 0; i < params.length; i++) { + query.setParameter(i + 1, params[i]); + } + return query; + } + + public static Mutiny.Query bindParameters(Mutiny.Query query, Map params) { + if (params == null || params.size() == 0) + return query; + for (Entry entry : params.entrySet()) { + query.setParameter(entry.getKey(), entry.getValue()); + } + return query; + } + + public static Uni executeUpdate(String query, Object... params) { + return getSession().chain(session -> { + Mutiny.Query jpaQuery = session.createQuery(query); + bindParameters(jpaQuery, params); + return jpaQuery.executeUpdate(); + }); + } + + public static Uni executeUpdate(String query, Map params) { + return getSession().chain(session -> { + Mutiny.Query jpaQuery = session.createQuery(query); + bindParameters(jpaQuery, params); + return jpaQuery.executeUpdate(); + }); + } } diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractUniInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractUniInterceptor.java new file mode 100644 index 00000000000000..8ef605c17ed6cc --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractUniInterceptor.java @@ -0,0 +1,18 @@ +package io.quarkus.hibernate.reactive.panache.common.runtime; + +import javax.interceptor.InvocationContext; + +import io.smallrye.mutiny.Uni; + +abstract class AbstractUniInterceptor { + + @SuppressWarnings("unchecked") + protected Uni proceed(InvocationContext context) { + try { + return ((Uni) context.proceed()); + } catch (Exception e) { + return Uni.createFrom().failure(e); + } + } + +} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactional.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactional.java index 42b44f0901912b..b693383ddebadc 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactional.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactional.java @@ -10,18 +10,17 @@ import org.hibernate.reactive.mutiny.Mutiny; +import io.quarkus.hibernate.reactive.panache.common.WithTransaction; import io.smallrye.mutiny.Uni; -import io.vertx.core.impl.VertxThread; /** * Use this annotation on your method to run them in a reactive {@link Mutiny.Transaction}. + *

+ * The annotated method must return a {@link Uni}. * - * If the annotated method returns a {@link Uni}, this has exactly the same behaviour as if the method - * was enclosed in a call to {@link Mutiny.Session#withTransaction(java.util.function.Function)}. - * - * Otherwise, invocations are only allowed when not running from a {@link VertxThread} and the behaviour - * will be the same as a blocking call to {@link Mutiny.Session#withTransaction(java.util.function.Function)}. + * @deprecated Use {@link WithTransaction} instead. */ +@Deprecated(forRemoval = true) @Inherited @InterceptorBinding @Target({ ElementType.TYPE, ElementType.METHOD }) diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java index 5272fc6cfd01d9..349df98ef691f4 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java @@ -1,11 +1,20 @@ package io.quarkus.hibernate.reactive.panache.common.runtime; import javax.annotation.Priority; +import javax.interceptor.AroundInvoke; import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; -@Interceptor @ReactiveTransactional +@Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) -public class ReactiveTransactionalInterceptor extends ReactiveTransactionalInterceptorBase { +public class ReactiveTransactionalInterceptor extends AbstractUniInterceptor { + + @AroundInvoke + public Object intercept(InvocationContext context) throws Exception { + // Note that intercepted methods annotated with @ReactiveTransactional are validated at build time + // The build fails if the method does not return Uni + return SessionOperations.withTransaction(() -> proceed(context)); + } } diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptorBase.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptorBase.java deleted file mode 100644 index 47b92817d09119..00000000000000 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptorBase.java +++ /dev/null @@ -1,134 +0,0 @@ -package io.quarkus.hibernate.reactive.panache.common.runtime; - -import java.lang.annotation.Annotation; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.time.Duration; -import java.util.function.Function; - -import javax.interceptor.AroundInvoke; -import javax.interceptor.InvocationContext; - -import org.hibernate.reactive.mutiny.Mutiny.Transaction; - -import io.smallrye.mutiny.Uni; - -public abstract class ReactiveTransactionalInterceptorBase { - private static final String JUNIT_TEST_ANN = "org.junit.jupiter.api.Test"; - private static final String JUNIT_BEFORE_EACH_ANN = "org.junit.jupiter.api.BeforeEach"; - private static final String JUNIT_AFTER_EACH_ANN = "org.junit.jupiter.api.AfterEach"; - private static final String UNI_ASSERTER_CLASS = "io.quarkus.test.vertx.UniAsserter"; - - @SuppressWarnings("unchecked") - @AroundInvoke - public Object intercept(InvocationContext ic) throws Exception { - Class returnType = ic.getMethod().getReturnType(); - if (returnType == Uni.class) { - return AbstractJpaOperations.getSession().flatMap(session -> session.withTransaction(tx -> { - inTransactionCallback(tx); - try { - return (Uni) ic.proceed(); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(e); - } - })); - } else if (io.vertx.core.Context.isOnVertxThread()) { - if (isSpecialTestMethod(ic)) { - return handleSpecialTestMethod(ic); - } - throw new RuntimeException("Unsupported return type " + returnType + " in method " + ic.getMethod() - + ": only Uni is supported when using @ReactiveTransaction if you are running on a VertxThread"); - } else { - // we're not on a Vert.x thread, we can block, and we assume the intercepted method is blocking - // FIXME: should we require a @Blocking annotation? - Uni ret = AbstractJpaOperations.getSession().flatMap(session -> session.withTransaction(tx -> { - inTransactionCallback(tx); - try { - return Uni.createFrom().item(ic.proceed()); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(e); - } - })); - return ret.await().atMost(Duration.ofMillis(AbstractJpaOperations.TIMEOUT_MS)); - } - } - - protected boolean isSpecialTestMethod(InvocationContext ic) { - Method method = ic.getMethod(); - return hasParameter(UNI_ASSERTER_CLASS, method) - && (hasAnnotation(JUNIT_TEST_ANN, method) - || hasAnnotation(JUNIT_BEFORE_EACH_ANN, method) - || hasAnnotation(JUNIT_AFTER_EACH_ANN, method)); - } - - protected Object handleSpecialTestMethod(InvocationContext ic) { - // let's not deal with generics/erasure - Class[] parameterTypes = ic.getMethod().getParameterTypes(); - Object uniAsserter = null; - Class uniAsserterClass = null; - for (int i = 0; i < parameterTypes.length; i++) { - Class klass = parameterTypes[i]; - if (klass.getName().equals(UNI_ASSERTER_CLASS)) { - uniAsserter = ic.getParameters()[i]; - uniAsserterClass = klass; - break; - } - } - if (uniAsserter == null) { - throw new AssertionError("We could not find the right UniAsserter parameter, please file a bug report"); - } - try { - Method execute = uniAsserterClass.getMethod("surroundWith", Function.class); - // here our execution differs: we can run the test method first, which uses the UniAsserter, and all its code is deferred - // by pushing execution into the asserter pipeline - try { - ic.proceed(); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(e); - } - // Now the pipeline is set up, we need to surround it with our transaction - execute.invoke(uniAsserter, new Function, Uni>() { - @Override - public Uni apply(Uni t) { - return AbstractJpaOperations.getSession().flatMap(session -> session.withTransaction(tx -> { - inTransactionCallback(tx); - return t; - })); - } - - }); - return null; - } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException - | InvocationTargetException e) { - throw new AssertionError("Reflective call to UniAsserter parameter failed, please file a bug report", e); - } - } - - private boolean hasParameter(String parameterType, Method method) { - // let's not deal with generics/erasure - for (Class klass : method.getParameterTypes()) { - if (klass.getName().equals(parameterType)) { - return true; - } - } - return false; - } - - private boolean hasAnnotation(String annotationName, Method method) { - for (Annotation annotation : method.getAnnotations()) { - if (annotation.annotationType().getName().equals(annotationName)) { - return true; - } - } - return false; - } - - protected void inTransactionCallback(Transaction tx) { - } -} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/SessionOperations.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/SessionOperations.java new file mode 100644 index 00000000000000..f14c5f7be2ccda --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/SessionOperations.java @@ -0,0 +1,208 @@ +package io.quarkus.hibernate.reactive.panache.common.runtime; + +import java.util.function.Function; +import java.util.function.Supplier; + +import org.hibernate.reactive.common.spi.Implementor; +import org.hibernate.reactive.context.Context.Key; +import org.hibernate.reactive.context.impl.BaseKey; +import org.hibernate.reactive.mutiny.Mutiny; +import org.hibernate.reactive.mutiny.Mutiny.Session; +import org.hibernate.reactive.mutiny.Mutiny.SessionFactory; +import org.hibernate.reactive.mutiny.Mutiny.Transaction; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.impl.LazyValue; +import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Context; +import io.vertx.core.Vertx; + +/** + * Static util methods for {@link Mutiny.Session}. + */ +public final class SessionOperations { + + private static final String ERROR_MSG = "Hibernate Reactive Panache requires a safe (isolated) Vert.x sub-context, but the current context hasn't been flagged as such."; + + private static final LazyValue SESSION_FACTORY = new LazyValue<>( + new Supplier() { + @Override + public SessionFactory get() { + // Note that Mutiny.SessionFactory is @ApplicationScoped bean - it's safe to use the cached client proxy + Mutiny.SessionFactory sessionFactory = Arc.container().instance(Mutiny.SessionFactory.class).get(); + if (sessionFactory == null) { + throw new IllegalStateException("Mutiny.SessionFactory bean not found"); + } + return sessionFactory; + } + }); + + private static final LazyValue> SESSION_KEY = new LazyValue<>( + new Supplier>() { + + @Override + public Key get() { + return new BaseKey<>(Mutiny.Session.class, ((Implementor) SESSION_FACTORY.get()).getUuid()); + } + }); + + // This key is used to indicate that a reactive session should be opened lazily (when needed) in the current vertx context + private static final String SESSION_ON_DEMAND_KEY = "hibernate.reactive.panache.sessionOnDemand"; + + /** + * Marks the current vertx duplicated context as "lazy" which indicates that a reactive session should be opened lazily if + * needed. The opened session is eventually closed and the marking key is removed when the provided {@link Uni} completes. + * + * @param + * @param work + * @return a new {@link Uni} + * @see #getSession() + */ + static Uni withSessionOnDemand(Supplier> work) { + Context context = vertxContext(); + if (context.getLocal(SESSION_ON_DEMAND_KEY) != null) { + // context already marked - no need to set the key and close the session + return work.get(); + } else { + // mark the lazy session + context.putLocal(SESSION_ON_DEMAND_KEY, true); + // perform the work and eventually close the session and remove the key + return work.get().eventually(() -> { + context.removeLocal(SESSION_ON_DEMAND_KEY); + return closeSession(); + }); + } + } + + /** + * Performs the work in the scope of a reactive transaction. An existing session is reused if possible. + * + * @param + * @param work + * @return a new {@link Uni} + */ + public static Uni withTransaction(Supplier> work) { + return withSession(s -> { + return s.withTransaction(t -> work.get()); + }); + } + + /** + * Performs the work in the scope of a reactive transaction. An existing session is reused if possible. + * + * @param + * @param work + * @return a new {@link Uni} + */ + public static Uni withTransaction(Function> work) { + return withSession(s -> { + return s.withTransaction(t -> work.apply(t)); + }); + } + + /** + * Performs the work in the scope of a reactive session. An existing session is reused if possible. + * + * @param + * @param work + * @return a new {@link Uni} + */ + public static Uni withSession(Function> work) { + Context context = vertxContext(); + Key key = getSessionKey(); + Mutiny.Session current = context.getLocal(key); + if (current != null && current.isOpen()) { + // reactive session exists - reuse this session + return work.apply(current); + } else { + // reactive session does not exist - open a new one and close it when the returned Uni completes + return getSessionFactory() + .openSession() + .invoke(s -> context.putLocal(key, s)) + .chain(s -> work.apply(s)) + .eventually(() -> closeSession()); + } + } + + /** + * If there is a reactive session stored in the current Vert.x duplicated context then this session is reused. + *

+ * However, if there is no reactive session found then: + *

    + *
  1. if the current vertx duplicated context is marked as "lazy" then a new session is opened and stored it in the + * context
  2. + *
  3. otherwise an exception thrown
  4. + *
+ * + * @throws IllegalStateException If no reactive session was found in the context and the context was not marked to open a + * new session lazily + * @return the {@link Mutiny.Session} + */ + public static Uni getSession() { + Context context = vertxContext(); + Key key = getSessionKey(); + Mutiny.Session current = context.getLocal(key); + if (current != null && current.isOpen()) { + // reuse the existing reactive session + return Uni.createFrom().item(current); + } else { + if (context.getLocal(SESSION_ON_DEMAND_KEY) != null) { + // open a new reactive session and store it in the vertx duplicated context + // the context was marked as "lazy" which means that the session will be eventually closed + return getSessionFactory().openSession().invoke(s -> context.putLocal(key, s)); + } else { + throw new IllegalStateException("No current Mutiny.Session found" + + "\n\t- no reactive session was found in the context and the context was not marked to open a new session lazily" + + "\n\t- you might need to annotate the business method with @ReactiveSession"); + } + } + } + + /** + * @return the current reactive session stored in the context, or {@code null} if no session exists + */ + public static Mutiny.Session getCurrentSession() { + Context context = vertxContext(); + Mutiny.Session current = context.getLocal(getSessionKey()); + if (current != null && current.isOpen()) { + return current; + } + return null; + } + + /** + * + * @return the current vertx duplicated context + * @throws IllegalStateException If no vertx context is found or is not a safe context as mandated by the + * {@link VertxContextSafetyToggle} + */ + private static Context vertxContext() { + Context context = Vertx.currentContext(); + if (context != null) { + VertxContextSafetyToggle.validateContextIfExists(ERROR_MSG, ERROR_MSG); + return context; + } else { + throw new IllegalStateException("No current Vertx context found"); + } + } + + static Uni closeSession() { + Context context = vertxContext(); + Key key = getSessionKey(); + Mutiny.Session current = context.getLocal(key); + if (current != null && current.isOpen()) { + return current.close().eventually(() -> context.removeLocal(key)); + } + return Uni.createFrom().voidItem(); + } + + static Key getSessionKey() { + return SESSION_KEY.get(); + } + + static Mutiny.SessionFactory getSessionFactory() { + return SESSION_FACTORY.get(); + } + +} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/TestReactiveTransactionalInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/TestReactiveTransactionalInterceptor.java index db626fb07ac8d6..483ba23ba98e1c 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/TestReactiveTransactionalInterceptor.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/TestReactiveTransactionalInterceptor.java @@ -1,12 +1,102 @@ package io.quarkus.hibernate.reactive.panache.common.runtime; -import org.hibernate.reactive.mutiny.Mutiny.Transaction; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.function.Function; -public class TestReactiveTransactionalInterceptor extends ReactiveTransactionalInterceptorBase { +import javax.interceptor.AroundInvoke; +import javax.interceptor.InvocationContext; - @Override - protected void inTransactionCallback(Transaction tx) { - tx.markForRollback(); +import io.smallrye.mutiny.Uni; + +public class TestReactiveTransactionalInterceptor { + + private static final String JUNIT_TEST_ANN = "org.junit.jupiter.api.Test"; + private static final String JUNIT_BEFORE_EACH_ANN = "org.junit.jupiter.api.BeforeEach"; + private static final String JUNIT_AFTER_EACH_ANN = "org.junit.jupiter.api.AfterEach"; + private static final String UNI_ASSERTER_CLASS = "io.quarkus.test.vertx.UniAsserter"; + + @AroundInvoke + public Object intercept(InvocationContext context) throws Exception { + if (isSpecialTestMethod(context)) { + return handleSpecialTestMethod(context); + } + // TODO validate this requirement during build + throw new IllegalStateException( + "A test method annotated with @TestReactiveTransaction must accept io.quarkus.test.vertx.UniAsserter"); + } + + protected boolean isSpecialTestMethod(InvocationContext ic) { + Method method = ic.getMethod(); + return hasParameter(UNI_ASSERTER_CLASS, method) + && (hasAnnotation(JUNIT_TEST_ANN, method) + || hasAnnotation(JUNIT_BEFORE_EACH_ANN, method) + || hasAnnotation(JUNIT_AFTER_EACH_ANN, method)); + } + + protected Object handleSpecialTestMethod(InvocationContext ic) { + // let's not deal with generics/erasure + Class[] parameterTypes = ic.getMethod().getParameterTypes(); + Object uniAsserter = null; + Class uniAsserterClass = null; + for (int i = 0; i < parameterTypes.length; i++) { + Class klass = parameterTypes[i]; + if (klass.getName().equals(UNI_ASSERTER_CLASS)) { + uniAsserter = ic.getParameters()[i]; + uniAsserterClass = klass; + break; + } + } + if (uniAsserter == null) { + throw new AssertionError("We could not find the right UniAsserter parameter, please file a bug report"); + } + try { + Method execute = uniAsserterClass.getMethod("surroundWith", Function.class); + // here our execution differs: we can run the test method first, which uses the UniAsserter, and all its code is deferred + // by pushing execution into the asserter pipeline + try { + ic.proceed(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + // Now the pipeline is set up, we need to surround it with our transaction + execute.invoke(uniAsserter, new Function, Uni>() { + @Override + public Uni apply(Uni t) { + return SessionOperations.withTransaction(tx -> { + tx.markForRollback(); + return t; + }); + } + + }); + return null; + } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + throw new AssertionError("Reflective call to UniAsserter parameter failed, please file a bug report", e); + } + } + + private boolean hasParameter(String parameterType, Method method) { + // let's not deal with generics/erasure + for (Class klass : method.getParameterTypes()) { + if (klass.getName().equals(parameterType)) { + return true; + } + } + return false; + } + + private boolean hasAnnotation(String annotationName, Method method) { + for (Annotation annotation : method.getAnnotations()) { + if (annotation.annotationType().getName().equals(annotationName)) { + return true; + } + } + return false; } } diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionInterceptor.java new file mode 100644 index 00000000000000..fc0f92d01d43ef --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionInterceptor.java @@ -0,0 +1,22 @@ +package io.quarkus.hibernate.reactive.panache.common.runtime; + +import javax.annotation.Priority; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; + +import io.quarkus.hibernate.reactive.panache.common.WithSession; + +@WithSession +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) +public class WithSessionInterceptor extends AbstractUniInterceptor { + + @AroundInvoke + public Object intercept(InvocationContext context) throws Exception { + // Note that intercepted methods annotated with @WithSession are validated at build time + // The build fails if a method does not return Uni + return SessionOperations.withSession(s -> proceed(context)); + } + +} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java new file mode 100644 index 00000000000000..a904ed8372bfa1 --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java @@ -0,0 +1,22 @@ +package io.quarkus.hibernate.reactive.panache.common.runtime; + +import javax.annotation.Priority; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; + +import io.quarkus.hibernate.reactive.panache.common.WithSessionOnDemand; + +@WithSessionOnDemand +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) +public class WithSessionOnDemandInterceptor extends AbstractUniInterceptor { + + @AroundInvoke + public Object intercept(InvocationContext context) throws Exception { + // Note that intercepted methods annotated with @WithSessionOnDemand are validated at build time + // The build fails if a method does not return Uni + return SessionOperations.withSessionOnDemand(() -> proceed(context)); + } + +} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java new file mode 100644 index 00000000000000..867f56fbe19570 --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java @@ -0,0 +1,22 @@ +package io.quarkus.hibernate.reactive.panache.common.runtime; + +import javax.annotation.Priority; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; + +import io.quarkus.hibernate.reactive.panache.common.WithTransaction; + +@WithTransaction +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) +public class WithTransactionInterceptor extends AbstractUniInterceptor { + + @AroundInvoke + public Object intercept(InvocationContext context) throws Exception { + // Note that intercepted methods annotated with @WithTransaction are validated at build time + // The build fails if the method does not return Uni + return SessionOperations.withTransaction(() -> proceed(context)); + } + +} diff --git a/extensions/panache/hibernate-reactive-panache/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/Panache.java b/extensions/panache/hibernate-reactive-panache/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/Panache.java index 789e187629d111..aee58386e7c827 100644 --- a/extensions/panache/hibernate-reactive-panache/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/Panache.java +++ b/extensions/panache/hibernate-reactive-panache/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/Panache.java @@ -6,6 +6,7 @@ import org.hibernate.reactive.mutiny.Mutiny; import io.quarkus.hibernate.reactive.panache.common.runtime.AbstractJpaOperations; +import io.quarkus.hibernate.reactive.panache.common.runtime.SessionOperations; import io.quarkus.panache.common.Parameters; import io.smallrye.mutiny.Uni; @@ -16,13 +17,25 @@ */ public class Panache { + /** + * Performs the given work within the scope of a reactive session. The session is automatically closed when the provided + * {@link Uni} completes. + * + * @param + * @param work + * @return a new {@link Uni} + */ + public static Uni withSession(Supplier> work) { + return SessionOperations.withSession(s -> work.get()); + } + /** * Returns the current {@link Mutiny.Session} * * @return the current {@link Mutiny.Session} */ public static Uni getSession() { - return AbstractJpaOperations.getSession(); + return SessionOperations.getSession(); } /** @@ -36,7 +49,7 @@ public static Uni getSession() { * @see Panache#currentTransaction() */ public static Uni withTransaction(Supplier> work) { - return getSession().flatMap(session -> session.withTransaction(t -> work.get())); + return SessionOperations.withTransaction(() -> work.get()); } /** diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java index 0c32a3a479b9a7..15de72c6f88c8f 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java @@ -14,7 +14,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -62,10 +61,10 @@ public class BeanInfo implements InjectionTargetInfo { private final DisposerInfo disposer; - private final Map interceptedMethods; - private final Map decoratedMethods; - - private final Map lifecycleInterceptors; + // These maps are initialized during BeanDeployment.init() + private volatile Map interceptedMethods; + private volatile Map decoratedMethods; + private volatile Map lifecycleInterceptors; private final boolean alternative; private final Integer priority; @@ -142,9 +141,9 @@ public class BeanInfo implements InjectionTargetInfo { this.params = params; // Identifier must be unique for a specific deployment this.identifier = Hashes.sha1(toString()); - this.interceptedMethods = new ConcurrentHashMap<>(); - this.decoratedMethods = new ConcurrentHashMap<>(); - this.lifecycleInterceptors = new ConcurrentHashMap<>(); + this.interceptedMethods = Collections.emptyMap(); + this.decoratedMethods = Collections.emptyMap(); + this.lifecycleInterceptors = Collections.emptyMap(); this.forceApplicationClass = forceApplicationClass; this.targetPackageName = targetPackageName; } @@ -292,6 +291,30 @@ Map getDecoratedMethods() { return decoratedMethods; } + /** + * @return {@code true} if the bean has an associated interceptor with the given binding, {@code false} otherwise + */ + public boolean hasAroundInvokeInterceptorWithBinding(DotName binding) { + if (interceptedMethods.isEmpty()) { + return false; + } + for (InterceptionInfo interception : interceptedMethods.values()) { + if (Annotations.contains(interception.bindings, binding)) { + return true; + } + } + return false; + } + + /** + * + * @return an immutable map of intercepted methods to the set of interceptor bindings + */ + public Map> getInterceptedMethodsBindings() { + return interceptedMethods.entrySet().stream() + .collect(Collectors.toUnmodifiableMap(Entry::getKey, e -> Collections.unmodifiableSet(e.getValue().bindings))); + } + List getInterceptedOrDecoratedMethods() { Set methods = new HashSet<>(interceptedMethods.keySet()); methods.addAll(decoratedMethods.keySet()); @@ -543,10 +566,11 @@ void init(List errors, Consumer bytecodeTransfor if (disposer != null) { disposer.init(errors); } - interceptedMethods.putAll(initInterceptedMethods(errors, bytecodeTransformerConsumer, transformUnproxyableClasses)); - decoratedMethods.putAll(initDecoratedMethods()); + interceptedMethods = Map + .copyOf(initInterceptedMethods(errors, bytecodeTransformerConsumer, transformUnproxyableClasses)); + decoratedMethods = Map.copyOf(initDecoratedMethods()); if (errors.isEmpty()) { - lifecycleInterceptors.putAll(initLifecycleInterceptors()); + lifecycleInterceptors = Map.copyOf(initLifecycleInterceptors()); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanStream.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanStream.java index 106cdce96be402..a3c155faee4d45 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanStream.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanStream.java @@ -185,6 +185,24 @@ public BeanStream withName() { return this; } + /** + * + * @return the new stream of beans that have an associated interceptor + */ + public BeanStream withAroundInvokeInterceptor() { + stream = stream.filter(BeanInfo::hasAroundInvokeInterceptors); + return this; + } + + /** + * + * @return the new stream of beans that have an associated lifecycle interceptor + */ + public BeanStream withLifecycleInterceptor() { + stream = stream.filter(BeanInfo::hasLifecycleInterceptors); + return this; + } + /** * * @param id diff --git a/integration-tests/hibernate-reactive-panache-blocking/pom.xml b/integration-tests/hibernate-reactive-panache-blocking/pom.xml deleted file mode 100644 index 422231187646e3..00000000000000 --- a/integration-tests/hibernate-reactive-panache-blocking/pom.xml +++ /dev/null @@ -1,317 +0,0 @@ - - - - quarkus-integration-tests-parent - io.quarkus - 999-SNAPSHOT - - 4.0.0 - - quarkus-integration-test-hibernate-reactive-panache-blocking - Quarkus - Integration Tests - Hibernate Reactive with Panache in Blocking mode - To test proper error reporting and safeguards when Panache Reactive is being used in combination with blocking technologies - - - vertx-reactive:postgresql://localhost:5432/hibernate_orm_test - - - - - io.quarkus - quarkus-hibernate-reactive-panache - - - io.quarkus - quarkus-resteasy - - - io.quarkus - quarkus-resteasy-jackson - - - io.quarkus - quarkus-core - - - io.quarkus - quarkus-reactive-pg-client - - - org.junit.jupiter - junit-jupiter-api - compile - - - - - io.quarkus - quarkus-junit5 - test - - - io.quarkus - quarkus-junit5-internal - test - - - io.rest-assured - rest-assured - test - - - io.quarkus - quarkus-test-h2 - test - - - io.quarkus - quarkus-panache-mock - test - - - net.bytebuddy - byte-buddy - - - - - org.assertj - assertj-core - test - - - org.awaitility - awaitility - test - - - - io.quarkus - quarkus-core-deployment - ${project.version} - pom - test - - - * - * - - - - - io.quarkus - quarkus-hibernate-reactive-panache-deployment - ${project.version} - pom - test - - - * - * - - - - - io.quarkus - quarkus-reactive-pg-client-deployment - ${project.version} - pom - test - - - * - * - - - - - io.quarkus - quarkus-resteasy-deployment - ${project.version} - pom - test - - - * - * - - - - - io.quarkus - quarkus-resteasy-jackson-deployment - ${project.version} - pom - test - - - * - * - - - - - - - - - src/main/resources - true - - - - - maven-surefire-plugin - - true - - - - maven-failsafe-plugin - - true - - - - io.quarkus - quarkus-maven-plugin - - - - build - - - - - - - - - - test-postgresql - - - test-containers - - - - - - maven-surefire-plugin - - false - - - - - prod-mode - test - - test - - - **/*PMT.java - - - - - - maven-failsafe-plugin - - false - - - - - - - - docker-postgresql - - - start-containers - - - - vertx-reactive:postgresql://localhost:5431/hibernate_orm_test - - - - - io.fabric8 - docker-maven-plugin - - - - ${postgres.image} - postgresql - - - hibernate_orm_test - hibernate_orm_test - hibernate_orm_test - - - 5431:5432 - - - - mapped - - 5432 - - - - - - - - - true - - - - docker-start - compile - - stop - start - - - - docker-stop - post-integration-test - - stop - - - - - - org.codehaus.mojo - exec-maven-plugin - - - docker-prune - generate-resources - - exec - - - ${docker-prune.location} - - - - - - - - - diff --git a/integration-tests/hibernate-reactive-panache-blocking/src/main/java/io/quarkus/it/panache/reactive/Fruit.java b/integration-tests/hibernate-reactive-panache-blocking/src/main/java/io/quarkus/it/panache/reactive/Fruit.java deleted file mode 100644 index e9c44f954f0417..00000000000000 --- a/integration-tests/hibernate-reactive-panache-blocking/src/main/java/io/quarkus/it/panache/reactive/Fruit.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.quarkus.it.panache.reactive; - -import javax.persistence.Entity; - -import io.quarkus.hibernate.reactive.panache.PanacheEntity; - -@Entity -public class Fruit extends PanacheEntity { - - public String name; - public String color; - - public Fruit(String name, String color) { - this.name = name; - this.color = color; - } - - public Fruit() { - } - -} diff --git a/integration-tests/hibernate-reactive-panache-blocking/src/main/java/io/quarkus/it/panache/reactive/TestEndpoint.java b/integration-tests/hibernate-reactive-panache-blocking/src/main/java/io/quarkus/it/panache/reactive/TestEndpoint.java deleted file mode 100644 index 7aa0925ae9c230..00000000000000 --- a/integration-tests/hibernate-reactive-panache-blocking/src/main/java/io/quarkus/it/panache/reactive/TestEndpoint.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.quarkus.it.panache.reactive; - -import java.util.List; - -import javax.ws.rs.GET; -import javax.ws.rs.Path; - -import io.quarkus.hibernate.reactive.panache.Panache; -import io.quarkus.hibernate.reactive.panache.PanacheEntityBase; - -/** - * These tests cover for "mixed mode" usage of Panache Reactive from a blocking thread; - * this is known to be tricky as Hibernate Reactive requires running on the event loop, - * while Panache relies on the notion of "current Session" being stored in the current - * CDI context. - */ -@Path("test") -public class TestEndpoint { - - @GET - @Path("store3fruits") - public String testStorage() { - Fruit apple = new Fruit("apple", "red"); - Fruit orange = new Fruit("orange", "orange"); - Fruit banana = new Fruit("banana", "yellow"); - - Panache.withTransaction(() -> Fruit.persist(apple, orange, banana)).subscribeAsCompletionStage().join(); - - //We wants this same request to also perform a read, so to trigger a second lookup of the Mutiny.Session from ArC - return verifyStored(); - } - - @GET - @Path("load3fruits") - public String verifyStored() { - final List fruitsList = Panache.withTransaction(() -> Fruit.find("select name, color from Fruit") - .list()) - .subscribeAsCompletionStage() - .join(); - return fruitsList.size() == 3 ? "OK" : "KO"; - } - -} diff --git a/integration-tests/hibernate-reactive-panache-blocking/src/main/resources/application.properties b/integration-tests/hibernate-reactive-panache-blocking/src/main/resources/application.properties deleted file mode 100644 index 6d0922b28d2fe5..00000000000000 --- a/integration-tests/hibernate-reactive-panache-blocking/src/main/resources/application.properties +++ /dev/null @@ -1,6 +0,0 @@ -quarkus.datasource.db-kind=postgresql -quarkus.datasource.username=hibernate_orm_test -quarkus.datasource.password=hibernate_orm_test -quarkus.datasource.reactive.url=${postgres.reactive.url} - -quarkus.hibernate-orm.database.generation=drop-and-create diff --git a/integration-tests/hibernate-reactive-panache-blocking/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityInGraalITCase.java b/integration-tests/hibernate-reactive-panache-blocking/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityInGraalITCase.java deleted file mode 100644 index 6f88d80421ba38..00000000000000 --- a/integration-tests/hibernate-reactive-panache-blocking/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityInGraalITCase.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.quarkus.it.panache.reactive; - -import io.quarkus.test.junit.QuarkusIntegrationTest; - -/** - * Repeat all tests from {@link PanacheFunctionalityTest} in native image mode. - */ -@QuarkusIntegrationTest -public class PanacheFunctionalityInGraalITCase extends PanacheFunctionalityTest { - -} diff --git a/integration-tests/hibernate-reactive-panache-blocking/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityTest.java b/integration-tests/hibernate-reactive-panache-blocking/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityTest.java deleted file mode 100644 index fac550debcebdd..00000000000000 --- a/integration-tests/hibernate-reactive-panache-blocking/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.quarkus.it.panache.reactive; - -import static org.hamcrest.Matchers.is; - -import org.junit.jupiter.api.Test; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; - -/** - * Test various Panache operations running in Quarkus - */ -@QuarkusTest -public class PanacheFunctionalityTest { - - @Test - public void tests() { - RestAssured.when().get("/test/store3fruits").then().body(is("OK")); - RestAssured.when().get("/test/load3fruits").then().body(is("OK")); - } - -} diff --git a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityTest.java b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityTest.java index 4fa2980e03c05d..68fd4fbc66f4c5 100644 --- a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityTest.java +++ b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityTest.java @@ -2,12 +2,12 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import javax.json.bind.Jsonb; import javax.json.bind.JsonbBuilder; import javax.persistence.PersistenceException; -import org.hibernate.reactive.mutiny.Mutiny.Transaction; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Order; @@ -18,6 +18,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.hibernate.reactive.panache.Panache; +import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.quarkus.hibernate.reactive.panache.common.WithTransaction; import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactional; import io.quarkus.test.TestReactiveTransaction; import io.quarkus.test.junit.DisabledOnIntegrationTest; @@ -65,9 +67,10 @@ public void testPanacheSerialisation() { } @DisabledOnIntegrationTest + @RunOnVertxContext @Test - public void testPanacheInTest() { - Assertions.assertEquals(0, Person.count().await().indefinitely()); + public void testPanacheInTest(UniAsserter asserter) { + asserter.assertEquals(() -> Panache.withSession(() -> Person.count()), 0l); } @Test @@ -157,25 +160,24 @@ public void testSortByNullPrecedence() { } @DisabledOnIntegrationTest - @ReactiveTransactional + @RunOnVertxContext @Test - Uni testTransaction() { - Transaction transaction = Panache.currentTransaction().await().indefinitely(); - Assertions.assertNotNull(transaction); - return Uni.createFrom().nullItem(); + void testTransaction(UniAsserter asserter) { + asserter.assertNotNull(() -> Panache.withTransaction(() -> Panache.currentTransaction())); } @DisabledOnIntegrationTest + @RunOnVertxContext @Test - void testNoTransaction() { - Transaction transaction = Panache.currentTransaction().await().indefinitely(); - Assertions.assertNull(transaction); + void testNoTransaction(UniAsserter asserter) { + asserter.assertNull(() -> Panache.withSession(() -> Panache.currentTransaction())); } @DisabledOnIntegrationTest + @RunOnVertxContext @Test - public void testBug7102() { - createBug7102() + public void testBug7102(UniAsserter asserter) { + asserter.execute(() -> createBug7102() .flatMap(person -> { return getBug7102(person.id) .flatMap(person1 -> { @@ -187,18 +189,17 @@ public void testBug7102() { Assertions.assertEquals("jozo", person2.name); return null; }); - }).flatMap(v -> Person.deleteAll()) - .await().indefinitely(); + }).flatMap(v -> Panache.withSession(() -> Person.deleteAll()))); } - @ReactiveTransactional + @WithTransaction Uni createBug7102() { Person personPanache = new Person(); personPanache.name = "pero"; return personPanache.persistAndFlush().map(v -> personPanache); } - @ReactiveTransactional + @WithTransaction Uni updateBug7102(Long id) { return Person. findById(id) .map(person -> { @@ -207,12 +208,13 @@ Uni updateBug7102(Long id) { }); } - @ReactiveTransactional + @WithSession Uni getBug7102(Long id) { return Person.findById(id); } @DisabledOnIntegrationTest + @RunOnVertxContext @TestReactiveTransaction @Test @Order(100) @@ -224,6 +226,7 @@ public void testTestTransaction(UniAsserter asserter) { } @DisabledOnIntegrationTest + @RunOnVertxContext @TestReactiveTransaction @Test @Order(101) @@ -234,39 +237,60 @@ public void testTestTransaction2(UniAsserter asserter) { } @DisabledOnIntegrationTest - @ReactiveTransactional + @RunOnVertxContext @Test @Order(200) public void testReactiveTransactional(UniAsserter asserter) { - asserter.assertNotNull(() -> Panache.currentTransaction()); - asserter.assertEquals(() -> Person.count(), 0l); - asserter.assertNotNull(() -> new Person().persist()); - asserter.assertEquals(() -> Person.count(), 1l); + asserter.assertEquals(() -> reactiveTransactional(), 1l); + } + + @WithTransaction + Uni reactiveTransactional() { + return Panache.currentTransaction() + .invoke(tx -> assertNotNull(tx)) + .chain(tx -> Person.count()) + .invoke(count -> assertEquals(0l, count)) + .call(() -> new Person().persist()) + .chain(tx -> Person.count()); } @DisabledOnIntegrationTest - @ReactiveTransactional + @RunOnVertxContext @Test @Order(201) public void testReactiveTransactional2(UniAsserter asserter) { - asserter.assertNotNull(() -> Panache.currentTransaction()); - // make sure the previous one was NOT rolled back - asserter.assertEquals(() -> Person.count(), 1l); - // now delete everything and cause a rollback - asserter.assertEquals(() -> Person.deleteAll(), 1l); - asserter.execute(() -> Panache.currentTransaction().invoke(tx -> tx.markForRollback())); + asserter.assertTrue(() -> reactiveTransactional2()); + } + + @WithTransaction + Uni reactiveTransactional2() { + return Panache.currentTransaction() + .invoke(tx -> assertNotNull(tx)) + .chain(tx -> Person.count()) + .invoke(count -> assertEquals(1l, count)) + .chain(() -> Person.deleteAll()) + .invoke(count -> assertEquals(1l, count)) + .chain(() -> Panache.currentTransaction()) + .invoke(tx -> tx.markForRollback()) + .map(tx -> true); } @DisabledOnIntegrationTest - @ReactiveTransactional + @RunOnVertxContext @Test @Order(202) public void testReactiveTransactional3(UniAsserter asserter) { - asserter.assertNotNull(() -> Panache.currentTransaction()); - // make sure it was rolled back - asserter.assertEquals(() -> Person.count(), 1l); - // and clean up - asserter.assertEquals(() -> Person.deleteAll(), 1l); + asserter.assertEquals(() -> testReactiveTransactional3(), 1l); + } + + @ReactiveTransactional + Uni testReactiveTransactional3() { + return Panache.currentTransaction() + .invoke(tx -> assertNotNull(tx)) + .chain(tx -> Person.count()) + // make sure it was rolled back + .invoke(count -> assertEquals(1l, count)) + .call(() -> Person.deleteAll()); } @DisabledOnIntegrationTest @@ -274,6 +298,6 @@ public void testReactiveTransactional3(UniAsserter asserter) { @Test @Order(300) public void testPersistenceException(UniAsserter asserter) { - asserter.assertFailedWith(() -> new Person().delete(), PersistenceException.class); + asserter.assertFailedWith(() -> Panache.withSession(() -> new Person().delete()), PersistenceException.class); } } diff --git a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheMockingTest.java b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheMockingTest.java index c780914895b0a4..d0b778851caa46 100644 --- a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheMockingTest.java +++ b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheMockingTest.java @@ -1,5 +1,8 @@ package io.quarkus.it.panache.reactive; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + import java.util.Collections; import javax.inject.Inject; @@ -12,56 +15,74 @@ import org.mockito.Mockito; import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; +import io.quarkus.hibernate.reactive.panache.common.runtime.SessionOperations; import io.quarkus.panache.mock.PanacheMock; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.mockito.InjectMock; +import io.quarkus.test.vertx.RunOnVertxContext; +import io.quarkus.test.vertx.UniAsserter; import io.smallrye.mutiny.Uni; @QuarkusTest public class PanacheMockingTest { + @SuppressWarnings("static-access") @Test + @RunOnVertxContext @Order(1) - public void testPanacheMocking() { - PanacheMock.mock(Person.class); - - Assertions.assertEquals(0, Person.count().await().indefinitely()); - - Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(23l)); - Assertions.assertEquals(23, Person.count().await().indefinitely()); - - Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(42l)); - Assertions.assertEquals(42, Person.count().await().indefinitely()); - - Mockito.when(Person.count()).thenCallRealMethod(); - Assertions.assertEquals(0, Person.count().await().indefinitely()); - - PanacheMock.verify(Person.class, Mockito.times(4)).count(); - - Person p = new Person(); - Mockito.when(Person.findById(12l)).thenReturn(Uni.createFrom().item(p)); - Assertions.assertSame(p, Person.findById(12l).await().indefinitely()); - Assertions.assertNull(Person.findById(42l).await().indefinitely()); - - Person.persist(p).await().indefinitely(); - Assertions.assertNull(p.id); - - Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException()); - try { - Person.findById(12l); - Assertions.fail(); - } catch (WebApplicationException x) { - } - - Mockito.when(Person.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList())); - Assertions.assertTrue(Person.findOrdered().await().indefinitely().isEmpty()); - - PanacheMock.verify(Person.class).findOrdered(); - PanacheMock.verify(Person.class).persist(Mockito. any(), Mockito. any()); - PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any()); - PanacheMock.verifyNoMoreInteractions(Person.class); - - Assertions.assertEquals(0, Person.methodWithPrimitiveParams(true, (byte) 0, (short) 0, 0, 2, 2.0f, 2.0, 'c')); + public void testPanacheMocking(UniAsserter asserter) { + String key = "person"; + + asserter.execute(() -> PanacheMock.mock(Person.class)); + asserter.assertEquals(() -> Person.count(), 0l); + + asserter.execute(() -> Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(23l))); + asserter.assertEquals(() -> Person.count(), 23l); + + asserter.execute(() -> Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(42l))); + asserter.assertEquals(() -> Person.count(), 42l); + + asserter.execute(() -> Mockito.when(Person.count()).thenCallRealMethod()); + asserter.assertEquals(() -> Person.count(), 0l); + + asserter.execute(() -> { + // use block lambda here, otherwise mutiny fails with NPE + PanacheMock.verify(Person.class, Mockito.times(4)).count(); + }); + + asserter.execute(() -> { + Person p = new Person(); + Mockito.when(Person.findById(12l)).thenReturn(Uni.createFrom().item(p)); + asserter.putData(key, p); + }); + asserter.assertThat(() -> Person.findById(12l), p -> Assertions.assertSame(p, asserter.getData(key))); + asserter.assertNull(() -> Person.findById(42l)); + + asserter.execute(() -> Person.persist(asserter.getData(key))); + asserter.execute(() -> assertNull(((Person) asserter.getData(key)).id)); + + asserter.execute(() -> Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException())); + asserter.assertFailedWith(() -> { + try { + return Person.findById(12l); + } catch (Exception e) { + return Uni.createFrom().failure(e); + } + }, t -> assertEquals(WebApplicationException.class, t.getClass())); + + asserter.execute(() -> Mockito.when(Person.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList()))); + asserter.assertThat(() -> Person.findOrdered(), list -> list.isEmpty()); + + asserter.execute(() -> { + PanacheMock.verify(Person.class).findOrdered(); + PanacheMock.verify(Person.class).persist(Mockito. any(), Mockito. any()); + PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any()); + PanacheMock.verifyNoMoreInteractions(Person.class); + Assertions.assertEquals(0, Person.methodWithPrimitiveParams(true, (byte) 0, (short) 0, 0, 2, 2.0f, 2.0, 'c')); + }); + + // Execute the asserter within a reactive session + asserter.surroundWith(u -> SessionOperations.withSession(s -> u)); } @Test @@ -73,63 +94,87 @@ public void testPanacheMockingWasCleared() { @InjectMock MockablePersonRepository mockablePersonRepository; + @RunOnVertxContext @Test - public void testPanacheRepositoryMocking() throws Throwable { - Assertions.assertEquals(0, mockablePersonRepository.count().await().indefinitely()); + public void testPanacheRepositoryMocking(UniAsserter asserter) throws Throwable { + String key = "person"; + + asserter.assertEquals(() -> mockablePersonRepository.count(), 0l); - Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(23l)); - Assertions.assertEquals(23, mockablePersonRepository.count().await().indefinitely()); + asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(23l))); + asserter.assertEquals(() -> mockablePersonRepository.count(), 23l); - Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(42l)); - Assertions.assertEquals(42, mockablePersonRepository.count().await().indefinitely()); + asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(42l))); + asserter.assertEquals(() -> mockablePersonRepository.count(), 42l); - Mockito.when(mockablePersonRepository.count()).thenCallRealMethod(); - Assertions.assertEquals(0, mockablePersonRepository.count().await().indefinitely()); + asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenCallRealMethod()); + asserter.assertEquals(() -> mockablePersonRepository.count(), 0l); - Mockito.verify(mockablePersonRepository, Mockito.times(4)).count(); + asserter.execute(() -> { + // use block lambda here, otherwise mutiny fails with NPE + Mockito.verify(mockablePersonRepository, Mockito.times(4)).count(); + }); - Person p = new Person(); - Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(Uni.createFrom().item(p)); - Assertions.assertSame(p, mockablePersonRepository.findById(12l).await().indefinitely()); - Assertions.assertNull(mockablePersonRepository.findById(42l).await().indefinitely()); + asserter.execute(() -> { + Person p = new Person(); + Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(Uni.createFrom().item(p)); + asserter.putData(key, p); + }); - mockablePersonRepository.persist(p).await().indefinitely(); - Assertions.assertNull(p.id); + asserter.assertThat(() -> mockablePersonRepository.findById(12l), p -> Assertions.assertSame(p, asserter.getData(key))); + asserter.assertNull(() -> mockablePersonRepository.findById(42l)); - Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException()); - try { - mockablePersonRepository.findById(12l); - Assertions.fail(); - } catch (WebApplicationException x) { - } + asserter.execute(() -> mockablePersonRepository.persist((Person) asserter.getData(key))); + asserter.execute(() -> - Mockito.when(mockablePersonRepository.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList())); - Assertions.assertTrue(mockablePersonRepository.findOrdered().await().indefinitely().isEmpty()); + assertNull(((Person) asserter.getData(key)).id)); - Mockito.verify(mockablePersonRepository).findOrdered(); - Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any()); - Mockito.verify(mockablePersonRepository).persist(Mockito. any()); - Mockito.verifyNoMoreInteractions(mockablePersonRepository); + asserter.execute(() -> Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException())); + asserter.assertFailedWith(() -> { + try { + return mockablePersonRepository.findById(12l); + } catch (Exception e) { + return Uni.createFrom().failure(e); + } + }, t -> assertEquals(WebApplicationException.class, t.getClass())); + + asserter.execute(() -> Mockito.when(mockablePersonRepository.findOrdered()) + .thenReturn(Uni.createFrom().item(Collections.emptyList()))); + asserter.assertThat(() -> mockablePersonRepository.findOrdered(), list -> list.isEmpty()); + + asserter.execute(() -> { + Mockito.verify(mockablePersonRepository).findOrdered(); + Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any()); + Mockito.verify(mockablePersonRepository).persist(Mockito. any()); + Mockito.verifyNoMoreInteractions(mockablePersonRepository); + }); + + // Execute the asserter within a reactive session + asserter.surroundWith(u -> SessionOperations.withSession(s -> u)); } @Inject PersonRepository realPersonRepository; + @SuppressWarnings({ "unchecked", "rawtypes" }) + @RunOnVertxContext @Test - public void testPanacheRepositoryBridges() { + public void testPanacheRepositoryBridges(UniAsserter asserter) { // normal method call - Assertions.assertNull(realPersonRepository.findById(0l).await().indefinitely()); + asserter.assertNull(() -> realPersonRepository.findById(0l)); // bridge call - Assertions.assertNull(((PanacheRepositoryBase) realPersonRepository).findById(0l).await().indefinitely()); + asserter.assertNull(() -> ((PanacheRepositoryBase) realPersonRepository).findById(0l)); // normal method call - Assertions.assertNull(realPersonRepository.findById(0l, LockModeType.NONE).await().indefinitely()); + asserter.assertNull(() -> realPersonRepository.findById(0l, LockModeType.NONE)); // bridge call - Assertions.assertNull( - ((PanacheRepositoryBase) realPersonRepository).findById(0l, LockModeType.NONE).await().indefinitely()); + asserter.assertNull(() -> ((PanacheRepositoryBase) realPersonRepository).findById(0l, LockModeType.NONE)); // normal method call - Assertions.assertEquals(false, realPersonRepository.deleteById(0l).await().indefinitely()); + asserter.assertFalse(() -> realPersonRepository.deleteById(0l)); // bridge call - Assertions.assertEquals(false, ((PanacheRepositoryBase) realPersonRepository).deleteById(0l).await().indefinitely()); + asserter.assertFalse(() -> ((PanacheRepositoryBase) realPersonRepository).deleteById(0l)); + + // Execute the asserter within a reactive session + asserter.surroundWith(u -> SessionOperations.withSession(s -> u)); } } diff --git a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/TestReactiveTransactionTest.java b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/TestReactiveTransactionTest.java index 2a40905c07e427..241f7bf51e6e56 100644 --- a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/TestReactiveTransactionTest.java +++ b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/TestReactiveTransactionTest.java @@ -6,17 +6,20 @@ import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.test.TestReactiveTransaction; import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.vertx.RunOnVertxContext; import io.quarkus.test.vertx.UniAsserter; @QuarkusTest public class TestReactiveTransactionTest { + @RunOnVertxContext @TestReactiveTransaction @Test public void testTestTransaction(UniAsserter asserter) { asserter.assertNotNull(() -> Panache.currentTransaction()); } + @RunOnVertxContext @TestReactiveTransaction @BeforeEach public void beforeEach(UniAsserter asserter) { diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 559d728b5ef371..bd7d5644a9d972 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -190,7 +190,6 @@ hibernate-reactive-postgresql hibernate-reactive-panache hibernate-reactive-panache-kotlin - hibernate-reactive-panache-blocking hibernate-search-orm-elasticsearch hibernate-search-orm-elasticsearch-coordination-outbox-polling hibernate-search-orm-opensearch