diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml
index 96505db6bfed4..0c7f7f0f495a3 100644
--- a/bom/runtime/pom.xml
+++ b/bom/runtime/pom.xml
@@ -427,6 +427,11 @@
quarkus-panache-common
${project.version}
+
+ io.quarkus
+ quarkus-panache-mock
+ ${project.version}
+
io.quarkus
quarkus-panacheql
diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc
index 3f400611aaeae..3289fba901168 100644
--- a/docs/src/main/asciidoc/getting-started-testing.adoc
+++ b/docs/src/main/asciidoc/getting-started-testing.adoc
@@ -569,6 +569,10 @@ public class GreetingResourceTest {
----
<1> Indicate that this injection point is meant to use an instance of `RestClient`.
+=== Mocking with Panache
+
+If you are using the `quarkus-hibernate-orm-panache` or `quarkus-mongodb-panache` extensions, check out the link:hibernate-orm-panache#mocking[Hibernate ORM with Panache Mocking] and link:mongodb-panache#mocking[MongoDB with Panache Mocking] documentation for the easiest way to mock your data access.
+
== Test Bootstrap Configuration Options
There are a few system properties that can be used to tune the bootstrap of the test, specifically its classpath.
diff --git a/docs/src/main/asciidoc/hibernate-orm-panache.adoc b/docs/src/main/asciidoc/hibernate-orm-panache.adoc
index 8dcd69ff252ef..92d6492a2fab5 100644
--- a/docs/src/main/asciidoc/hibernate-orm-panache.adoc
+++ b/docs/src/main/asciidoc/hibernate-orm-panache.adoc
@@ -611,7 +611,7 @@ public void create(Parameter parameter){
Panache provides direct support for database locking with your entity/repository, using `findById(Object, LockModeType)` or `find().withLock(LockModeType)`.
-The following examples are for the entity pattern, but the same can be used with repositories.
+The following examples are for the active record pattern, but the same can be used with repositories.
=== First: Locking using findById().
@@ -686,6 +686,184 @@ public class PersonRepository implements PanacheRepositoryBase {
}
----
+== Mocking
+
+=== Using the active record pattern
+
+If you are using the active record pattern you cannot use Mockito directly as it does not support mocking static methods,
+but you can use the `quarkus-panache-mock` module which allows you to use Mockito to mock all provided static
+methods, including your own.
+
+Add this dependency to your `pom.xml`:
+
+[source,xml]
+----
+
+ io.quarkus
+ quarkus-panache-mock
+ test
+
+----
+
+
+Given this simple entity:
+
+[source,java]
+----
+@Entity
+public class Person extends PanacheEntity {
+
+ public String name;
+
+ public static List findOrdered() {
+ return find("ORDER BY name").list();
+ }
+}
+----
+
+You can write your mocking test like this:
+
+[source,java]
+----
+@QuarkusTest
+public class PanacheFunctionalityTest {
+
+ @Test
+ public void testPanacheMocking() {
+ PanacheMock.mock(Person.class);
+
+ // Mocked classes always return a default value
+ Assertions.assertEquals(0, Person.count());
+
+ // Now let's specify the return value
+ Mockito.when(Person.count()).thenReturn(23l);
+ Assertions.assertEquals(23, Person.count());
+
+ // Now let's change the return value
+ Mockito.when(Person.count()).thenReturn(42l);
+ Assertions.assertEquals(42, Person.count());
+
+ // Now let's call the original method
+ Mockito.when(Person.count()).thenCallRealMethod();
+ Assertions.assertEquals(0, Person.count());
+
+ // Check that we called it 4 times
+ PanacheMock.verify(Person.class, Mockito.times(4)).count();// <1>
+
+ // Mock only with specific parameters
+ Person p = new Person();
+ Mockito.when(Person.findById(12l)).thenReturn(p);
+ Assertions.assertSame(p, Person.findById(12l));
+ Assertions.assertNull(Person.findById(42l));
+
+ // Mock throwing
+ Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException());
+ Assertions.assertThrows(WebApplicationException.class, () -> Person.findById(12l));
+
+ // We can even mock your custom methods
+ Mockito.when(Person.findOrdered()).thenReturn(Collections.emptyList());
+ Assertions.assertTrue(Person.findOrdered().isEmpty());
+
+ PanacheMock.verify(Person.class).findOrdered();
+ PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any());
+ PanacheMock.verifyNoMoreInteractions(Person.class);
+ }
+}
+----
+<1> Be sure to call your `verify` methods on `PanacheMock` rather than `Mockito`, otherwise you won't know
+what mock object to pass.
+
+=== Using the repository pattern
+
+If you are using the repository pattern you can use Mockito directly, using the `quarkus-junit5-mockito` module,
+which makes mocking beans much easier:
+
+[source,java]
+----
+
+ io.quarkus
+ quarkus-junit5-mockito
+ test
+
+----
+
+Given this simple entity:
+
+[source,java]
+----
+@Entity
+public class Person {
+
+ @Id
+ @GeneratedValue
+ public Long id;
+
+ public String name;
+}
+----
+
+And this repository:
+
+[source,java]
+----
+@ApplicationScoped
+public class PersonRepository implements PanacheRepository {
+ public List findOrdered() {
+ return find("ORDER BY name").list();
+ }
+}
+----
+
+You can write your mocking test like this:
+
+[source,java]
+----
+@QuarkusTest
+public class PanacheFunctionalityTest {
+ @InjectMock
+ PersonRepository personRepository;
+
+ @Test
+ public void testPanacheRepositoryMocking() throws Throwable {
+ // Mocked classes always return a default value
+ Assertions.assertEquals(0, personRepository.count());
+
+ // Now let's specify the return value
+ Mockito.when(personRepository.count()).thenReturn(23l);
+ Assertions.assertEquals(23, personRepository.count());
+
+ // Now let's change the return value
+ Mockito.when(personRepository.count()).thenReturn(42l);
+ Assertions.assertEquals(42, personRepository.count());
+
+ // Now let's call the original method
+ Mockito.when(personRepository.count()).thenCallRealMethod();
+ Assertions.assertEquals(0, personRepository.count());
+
+ // Check that we called it 4 times
+ Mockito.verify(personRepository, Mockito.times(4)).count();
+
+ // Mock only with specific parameters
+ Person p = new Person();
+ Mockito.when(personRepository.findById(12l)).thenReturn(p);
+ Assertions.assertSame(p, personRepository.findById(12l));
+ Assertions.assertNull(personRepository.findById(42l));
+
+ // Mock throwing
+ Mockito.when(personRepository.findById(12l)).thenThrow(new WebApplicationException());
+ Assertions.assertThrows(WebApplicationException.class, () -> personRepository.findById(12l));
+
+ Mockito.when(personRepository.findOrdered()).thenReturn(Collections.emptyList());
+ Assertions.assertTrue(personRepository.findOrdered().isEmpty());
+
+ // We can even mock your custom methods
+ Mockito.verify(personRepository).findOrdered();
+ Mockito.verify(personRepository, Mockito.atLeastOnce()).findById(Mockito.any());
+ Mockito.verifyNoMoreInteractions(personRepository);
+ }
+}
+----
+
== How and why we simplify Hibernate ORM mappings
When it comes to writing Hibernate ORM entities, there are a number of annoying things that users have grown used to
diff --git a/docs/src/main/asciidoc/mongodb-panache.adoc b/docs/src/main/asciidoc/mongodb-panache.adoc
index 474dc668c89bd..3b3f9d7add49c 100644
--- a/docs/src/main/asciidoc/mongodb-panache.adoc
+++ b/docs/src/main/asciidoc/mongodb-panache.adoc
@@ -867,6 +867,181 @@ public Multi streamPersons() {
TIP: `@SseElementType(MediaType.APPLICATION_JSON)` tells RESTEasy to serialize the object in JSON.
+== Mocking
+
+=== Using the active-record pattern
+
+If you are using the active-record pattern you cannot use Mockito directly as it does not support mocking static methods,
+but you can use the `quarkus-panache-mock` module which allows you to use Mockito to mock all provided static
+methods, including your own.
+
+Add this dependency to your `pom.xml`:
+
+[source,xml]
+----
+
+ io.quarkus
+ quarkus-panache-mock
+ test
+
+----
+
+Given this simple entity:
+
+[source,java]
+----
+public class Person extends PanacheMongoEntity {
+
+ public String name;
+
+ public static List findOrdered() {
+ return findAll(Sort.by("lastname", "firstname")).list();
+ }
+}
+----
+
+You can write your mocking test like this:
+
+[source,java]
+----
+@QuarkusTest
+public class PanacheFunctionalityTest {
+
+ @Test
+ public void testPanacheMocking() {
+ PanacheMock.mock(Person.class);
+
+ // Mocked classes always return a default value
+ Assertions.assertEquals(0, Person.count());
+
+ // Now let's specify the return value
+ Mockito.when(Person.count()).thenReturn(23l);
+ Assertions.assertEquals(23, Person.count());
+
+ // Now let's change the return value
+ Mockito.when(Person.count()).thenReturn(42l);
+ Assertions.assertEquals(42, Person.count());
+
+ // Now let's call the original method
+ Mockito.when(Person.count()).thenCallRealMethod();
+ Assertions.assertEquals(0, Person.count());
+
+ // Check that we called it 4 times
+ PanacheMock.verify(Person.class, Mockito.times(4)).count();// <1>
+
+ // Mock only with specific parameters
+ Person p = new Person();
+ Mockito.when(Person.findById(12l)).thenReturn(p);
+ Assertions.assertSame(p, Person.findById(12l));
+ Assertions.assertNull(Person.findById(42l));
+
+ // Mock throwing
+ Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException());
+ Assertions.assertThrows(WebApplicationException.class, () -> Person.findById(12l));
+
+ // We can even mock your custom methods
+ Mockito.when(Person.findOrdered()).thenReturn(Collections.emptyList());
+ Assertions.assertTrue(Person.findOrdered().isEmpty());
+
+ PanacheMock.verify(Person.class).findOrdered();
+ PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any());
+ PanacheMock.verifyNoMoreInteractions(Person.class);
+ }
+}
+----
+<1> Be sure to call your `verify` methods on `PanacheMock` rather than `Mockito`, otherwise you won't know
+what mock object to pass.
+
+=== Using the repository pattern
+
+If you are using the repository pattern you can use Mockito directly, using the `quarkus-junit5-mockito` module,
+which makes mocking beans much easier:
+
+[source,java]
+----
+
+ io.quarkus
+ quarkus-junit5-mockito
+ test
+
+----
+
+Given this simple entity:
+
+[source,java]
+----
+public class Person {
+
+ @BsonId
+ public Long id;
+
+ public String name;
+}
+----
+
+And this repository:
+
+[source,java]
+----
+@ApplicationScoped
+public class PersonRepository implements PanacheMongoRepository {
+ public List findOrdered() {
+ return findAll(Sort.by("lastname", "firstname")).list();
+ }
+}
+----
+
+You can write your mocking test like this:
+
+[source,java]
+----
+@QuarkusTest
+public class PanacheFunctionalityTest {
+ @InjectMock
+ PersonRepository personRepository;
+
+ @Test
+ public void testPanacheRepositoryMocking() throws Throwable {
+ // Mocked classes always return a default value
+ Assertions.assertEquals(0, personRepository.count());
+
+ // Now let's specify the return value
+ Mockito.when(personRepository.count()).thenReturn(23l);
+ Assertions.assertEquals(23, personRepository.count());
+
+ // Now let's change the return value
+ Mockito.when(personRepository.count()).thenReturn(42l);
+ Assertions.assertEquals(42, personRepository.count());
+
+ // Now let's call the original method
+ Mockito.when(personRepository.count()).thenCallRealMethod();
+ Assertions.assertEquals(0, personRepository.count());
+
+ // Check that we called it 4 times
+ Mockito.verify(personRepository, Mockito.times(4)).count();
+
+ // Mock only with specific parameters
+ Person p = new Person();
+ Mockito.when(personRepository.findById(12l)).thenReturn(p);
+ Assertions.assertSame(p, personRepository.findById(12l));
+ Assertions.assertNull(personRepository.findById(42l));
+
+ // Mock throwing
+ Mockito.when(personRepository.findById(12l)).thenThrow(new WebApplicationException());
+ Assertions.assertThrows(WebApplicationException.class, () -> personRepository.findById(12l));
+
+ Mockito.when(personRepository.findOrdered()).thenReturn(Collections.emptyList());
+ Assertions.assertTrue(personRepository.findOrdered().isEmpty());
+
+ // We can even mock your custom methods
+ Mockito.verify(personRepository).findOrdered();
+ Mockito.verify(personRepository, Mockito.atLeastOnce()).findById(Mockito.any());
+ Mockito.verifyNoMoreInteractions(personRepository);
+ }
+}
+----
+
+
== How and why we simplify MongoDB API
When it comes to writing MongoDB entities, there are a number of annoying things that users have grown used to
diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaEntityEnhancer.java b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaEntityEnhancer.java
index f29c14ad090c2..7e3135519826c 100644
--- a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaEntityEnhancer.java
+++ b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaEntityEnhancer.java
@@ -1,6 +1,7 @@
package io.quarkus.hibernate.orm.panache.deployment;
import java.lang.reflect.Modifier;
+import java.util.List;
import javax.persistence.Transient;
@@ -22,6 +23,7 @@
import io.quarkus.panache.common.deployment.EntityModel;
import io.quarkus.panache.common.deployment.MetamodelInfo;
import io.quarkus.panache.common.deployment.PanacheEntityEnhancer;
+import io.quarkus.panache.common.deployment.PanacheMethodCustomizer;
public class PanacheJpaEntityEnhancer extends PanacheEntityEnhancer>> {
@@ -38,15 +40,15 @@ public class PanacheJpaEntityEnhancer extends PanacheEntityEnhancer methodCustomizers) {
+ super(index, PanacheResourceProcessor.DOTNAME_PANACHE_ENTITY_BASE, methodCustomizers);
modelInfo = new MetamodelInfo<>();
}
@Override
public ClassVisitor apply(String className, ClassVisitor outputClassVisitor) {
return new PanacheJpaEntityClassVisitor(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo,
- indexView.getClassByName(DotName.createSimple(className)));
+ indexView.getClassByName(DotName.createSimple(className)), methodCustomizers);
}
static class PanacheJpaEntityClassVisitor extends PanacheEntityClassVisitor {
@@ -54,8 +56,9 @@ static class PanacheJpaEntityClassVisitor extends PanacheEntityClassVisitor> modelInfo,
ClassInfo panacheEntityBaseClassInfo,
- ClassInfo entityInfo) {
- super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, entityInfo);
+ ClassInfo entityInfo,
+ List methodCustomizers) {
+ super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, entityInfo, methodCustomizers);
}
@Override
diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaRepositoryEnhancer.java b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaRepositoryEnhancer.java
index 920d0f76dc527..0153edbc59f87 100644
--- a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaRepositoryEnhancer.java
+++ b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaRepositoryEnhancer.java
@@ -5,11 +5,9 @@
import org.jboss.jandex.IndexView;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
-import org.objectweb.asm.Opcodes;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
-import io.quarkus.panache.common.deployment.JandexUtil;
import io.quarkus.panache.common.deployment.PanacheRepositoryEnhancer;
public class PanacheJpaRepositoryEnhancer extends PanacheRepositoryEnhancer {
@@ -50,58 +48,6 @@ protected String getPanacheOperationsBinaryName() {
return PanacheJpaEntityEnhancer.JPA_OPERATIONS_BINARY_NAME;
}
- @Override
- public void visitEnd() {
- // Bridge for findById, but only if we actually know the end entity (which we don't for intermediate
- // abstract repositories that haven't fixed their entity type yet
- if (!"Ljava/lang/Object;".equals(entitySignature)) {
- if (!JandexUtil.containsMethod(daoClassInfo, "findById",
- Object.class.getName(),
- Object.class.getName())) {
- MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_BRIDGE,
- "findById",
- "(Ljava/lang/Object;)Ljava/lang/Object;",
- null,
- null);
- mv.visitParameter("id", 0);
- mv.visitCode();
- mv.visitIntInsn(Opcodes.ALOAD, 0);
- mv.visitIntInsn(Opcodes.ALOAD, 1);
-
- mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
- daoBinaryName,
- "findById",
- "(Ljava/lang/Object;)" + entitySignature, false);
- mv.visitInsn(Opcodes.ARETURN);
- mv.visitMaxs(0, 0);
- mv.visitEnd();
- }
- if (!JandexUtil.containsMethod(daoClassInfo, "findById",
- Object.class.getName(),
- Object.class.getName(), "javax.persistence.LockModeType")) {
- MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_BRIDGE,
- "findById",
- "(Ljava/lang/Object;Ljavax/persistence/LockModeType;)Ljava/lang/Object;",
- null,
- null);
- mv.visitParameter("id", 0);
- mv.visitParameter("lockModeType", 0);
- mv.visitCode();
- mv.visitIntInsn(Opcodes.ALOAD, 0);
- mv.visitIntInsn(Opcodes.ALOAD, 1);
- mv.visitIntInsn(Opcodes.ALOAD, 2);
- mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
- daoBinaryName,
- "findById",
- "(Ljava/lang/Object;Ljavax/persistence/LockModeType;)" + entitySignature, false);
- mv.visitInsn(Opcodes.ARETURN);
- mv.visitMaxs(0, 0);
- mv.visitEnd();
- }
- }
- super.visitEnd();
- }
-
@Override
protected void injectModel(MethodVisitor mv) {
// inject Class
diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java
index 7de5c41758178..3d64066db93f5 100644
--- a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java
+++ b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java
@@ -6,6 +6,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
import javax.persistence.EntityManager;
import javax.persistence.NamedQueries;
@@ -38,6 +39,8 @@
import io.quarkus.panache.common.deployment.MetamodelInfo;
import io.quarkus.panache.common.deployment.PanacheEntityClassesBuildItem;
import io.quarkus.panache.common.deployment.PanacheFieldAccessEnhancer;
+import io.quarkus.panache.common.deployment.PanacheMethodCustomizer;
+import io.quarkus.panache.common.deployment.PanacheMethodCustomizerBuildItem;
import io.quarkus.panache.common.deployment.PanacheRepositoryEnhancer;
public final class PanacheResourceProcessor {
@@ -76,7 +79,11 @@ void build(CombinedIndexBuildItem index,
BuildProducer transformers,
HibernateEnhancersRegisteredBuildItem hibernateMarker,
BuildProducer entityClasses,
- BuildProducer namedQueries) throws Exception {
+ BuildProducer namedQueries,
+ List methodCustomizersBuildItems) throws Exception {
+
+ List methodCustomizers = methodCustomizersBuildItems.stream()
+ .map(bi -> bi.getMethodCustomizer()).collect(Collectors.toList());
PanacheJpaRepositoryEnhancer daoEnhancer = new PanacheJpaRepositoryEnhancer(index.getIndex());
Set daoClasses = new HashSet<>();
@@ -107,7 +114,7 @@ void build(CombinedIndexBuildItem index,
namedQueries.produce(new NamedQueryEntityClassBuildStep(parameterType.name().toString(), typeNamedQueries));
}
- PanacheJpaEntityEnhancer modelEnhancer = new PanacheJpaEntityEnhancer(index.getIndex());
+ PanacheJpaEntityEnhancer modelEnhancer = new PanacheJpaEntityEnhancer(index.getIndex(), methodCustomizers);
Set modelClasses = new HashSet<>();
// Note that we do this in two passes because for some reason Jandex does not give us subtypes
// of PanacheEntity if we ask for subtypes of PanacheEntityBase
diff --git a/extensions/panache/hibernate-orm-panache/runtime/pom.xml b/extensions/panache/hibernate-orm-panache/runtime/pom.xml
index 619864a3a1296..db189f7b4f32d 100644
--- a/extensions/panache/hibernate-orm-panache/runtime/pom.xml
+++ b/extensions/panache/hibernate-orm-panache/runtime/pom.xml
@@ -39,6 +39,16 @@
quarkus-jackson
true
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
org.hibernate
diff --git a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java
index e786ff47d73e9..45b64eb2f4ee7 100644
--- a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java
+++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java
@@ -2,6 +2,7 @@
import java.lang.reflect.Modifier;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import org.bson.codecs.pojo.annotations.BsonIgnore;
@@ -19,6 +20,7 @@
import io.quarkus.panache.common.deployment.EntityModel;
import io.quarkus.panache.common.deployment.MetamodelInfo;
import io.quarkus.panache.common.deployment.PanacheEntityEnhancer;
+import io.quarkus.panache.common.deployment.PanacheMethodCustomizer;
public class PanacheMongoEntityEnhancer extends PanacheEntityEnhancer>> {
public final static String MONGO_OPERATIONS_NAME = MongoOperations.class.getName();
@@ -28,23 +30,23 @@ public class PanacheMongoEntityEnhancer extends PanacheEntityEnhancer entities = new HashMap<>();
- public PanacheMongoEntityEnhancer(IndexView index) {
- super(index, PanacheResourceProcessor.DOTNAME_PANACHE_ENTITY_BASE);
+ public PanacheMongoEntityEnhancer(IndexView index, List methodCustomizers) {
+ super(index, PanacheResourceProcessor.DOTNAME_PANACHE_ENTITY_BASE, methodCustomizers);
modelInfo = new MetamodelInfo<>();
}
@Override
public ClassVisitor apply(String className, ClassVisitor outputClassVisitor) {
return new PanacheMongoEntityClassVisitor(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo,
- indexView.getClassByName(DotName.createSimple(className)));
+ indexView.getClassByName(DotName.createSimple(className)), methodCustomizers);
}
static class PanacheMongoEntityClassVisitor extends PanacheEntityClassVisitor {
public PanacheMongoEntityClassVisitor(String className, ClassVisitor outputClassVisitor,
MetamodelInfo> modelInfo, ClassInfo panacheEntityBaseClassInfo,
- ClassInfo entityInfo) {
- super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, entityInfo);
+ ClassInfo entityInfo, List methodCustomizers) {
+ super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, entityInfo, methodCustomizers);
}
@Override
diff --git a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoRepositoryEnhancer.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoRepositoryEnhancer.java
index 17b245e852b2f..29ab719398877 100644
--- a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoRepositoryEnhancer.java
+++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoRepositoryEnhancer.java
@@ -5,11 +5,9 @@
import org.jboss.jandex.IndexView;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
-import org.objectweb.asm.Opcodes;
import io.quarkus.mongodb.panache.PanacheMongoRepository;
import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase;
-import io.quarkus.panache.common.deployment.JandexUtil;
import io.quarkus.panache.common.deployment.PanacheRepositoryEnhancer;
public class PanacheMongoRepositoryEnhancer extends PanacheRepositoryEnhancer {
@@ -49,36 +47,6 @@ protected String getPanacheOperationsBinaryName() {
return PanacheMongoEntityEnhancer.MONGO_OPERATIONS_BINARY_NAME;
}
- @Override
- public void visitEnd() {
- // Bridge for findById, but only if we actually know the end entity (which we don't for intermediate
- // abstract repositories that haven't fixed their entity type yet
- if (!entitySignature.equals("Ljava/lang/Object;")) {
- if (!JandexUtil.containsMethod(daoClassInfo, "findById",
- Object.class.getName(),
- Object.class.getName())) {
- MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_BRIDGE,
- "findById",
- "(Ljava/lang/Object;)Ljava/lang/Object;",
- null,
- null);
- mv.visitParameter("id", 0);
- mv.visitCode();
- mv.visitIntInsn(Opcodes.ALOAD, 0);
- mv.visitIntInsn(Opcodes.ALOAD, 1);
- mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
- daoBinaryName,
- "findById",
- "(Ljava/lang/Object;)" + entitySignature, false);
- mv.visitInsn(Opcodes.ARETURN);
- mv.visitMaxs(0, 0);
- mv.visitEnd();
- }
- }
-
- super.visitEnd();
- }
-
@Override
protected void injectModel(MethodVisitor mv) {
// inject Class
diff --git a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java
index 96ae8cdf948f6..25bfd54ddcb43 100644
--- a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java
+++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java
@@ -7,6 +7,7 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
import org.bson.codecs.pojo.annotations.BsonProperty;
import org.bson.types.ObjectId;
@@ -51,6 +52,8 @@
import io.quarkus.mongodb.panache.reactive.ReactivePanacheMongoRepositoryBase;
import io.quarkus.panache.common.deployment.PanacheEntityClassesBuildItem;
import io.quarkus.panache.common.deployment.PanacheFieldAccessEnhancer;
+import io.quarkus.panache.common.deployment.PanacheMethodCustomizer;
+import io.quarkus.panache.common.deployment.PanacheMethodCustomizerBuildItem;
import io.quarkus.panache.common.deployment.PanacheRepositoryEnhancer;
public class PanacheResourceProcessor {
@@ -133,7 +136,11 @@ void buildImperative(CombinedIndexBuildItem index,
BuildProducer transformers,
BuildProducer reflectiveClass,
BuildProducer propertyMappingClass,
- BuildProducer entityClasses) {
+ BuildProducer entityClasses,
+ List methodCustomizersBuildItems) {
+
+ List methodCustomizers = methodCustomizersBuildItems.stream()
+ .map(bi -> bi.getMethodCustomizer()).collect(Collectors.toList());
PanacheMongoRepositoryEnhancer daoEnhancer = new PanacheMongoRepositoryEnhancer(index.getIndex());
Set daoClasses = new HashSet<>();
@@ -167,7 +174,7 @@ void buildImperative(CombinedIndexBuildItem index,
propertyMappingClass.produce(new PropertyMappingClassBuildStep(parameterType.name().toString()));
}
- PanacheMongoEntityEnhancer modelEnhancer = new PanacheMongoEntityEnhancer(index.getIndex());
+ PanacheMongoEntityEnhancer modelEnhancer = new PanacheMongoEntityEnhancer(index.getIndex(), methodCustomizers);
Set modelClasses = new HashSet<>();
// Note that we do this in two passes because for some reason Jandex does not give us subtypes
// of PanacheMongoEntity if we ask for subtypes of PanacheMongoEntityBase
@@ -214,7 +221,11 @@ void buildMutiny(CombinedIndexBuildItem index,
ApplicationIndexBuildItem applicationIndex,
BuildProducer reflectiveClass,
BuildProducer propertyMappingClass,
- BuildProducer transformers) {
+ BuildProducer transformers,
+ List methodCustomizersBuildItems) {
+
+ List methodCustomizers = methodCustomizersBuildItems.stream()
+ .map(bi -> bi.getMethodCustomizer()).collect(Collectors.toList());
ReactivePanacheMongoRepositoryEnhancer daoEnhancer = new ReactivePanacheMongoRepositoryEnhancer(index.getIndex());
Set daoClasses = new HashSet<>();
@@ -249,7 +260,8 @@ void buildMutiny(CombinedIndexBuildItem index,
propertyMappingClass.produce(new PropertyMappingClassBuildStep(parameterType.name().toString()));
}
- ReactivePanacheMongoEntityEnhancer modelEnhancer = new ReactivePanacheMongoEntityEnhancer(index.getIndex());
+ ReactivePanacheMongoEntityEnhancer modelEnhancer = new ReactivePanacheMongoEntityEnhancer(index.getIndex(),
+ methodCustomizers);
Set modelClasses = new HashSet<>();
// Note that we do this in two passes because for some reason Jandex does not give us subtypes
// of PanacheMongoEntity if we ask for subtypes of PanacheMongoEntityBase
diff --git a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/ReactivePanacheMongoEntityEnhancer.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/ReactivePanacheMongoEntityEnhancer.java
index f204029cdd645..679d8cb9794ce 100644
--- a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/ReactivePanacheMongoEntityEnhancer.java
+++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/ReactivePanacheMongoEntityEnhancer.java
@@ -2,6 +2,7 @@
import java.lang.reflect.Modifier;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import org.bson.codecs.pojo.annotations.BsonIgnore;
@@ -19,6 +20,7 @@
import io.quarkus.panache.common.deployment.EntityModel;
import io.quarkus.panache.common.deployment.MetamodelInfo;
import io.quarkus.panache.common.deployment.PanacheEntityEnhancer;
+import io.quarkus.panache.common.deployment.PanacheMethodCustomizer;
public class ReactivePanacheMongoEntityEnhancer extends PanacheEntityEnhancer>> {
public final static String MONGO_OPERATIONS_NAME = ReactiveMongoOperations.class.getName();
@@ -28,23 +30,23 @@ public class ReactivePanacheMongoEntityEnhancer extends PanacheEntityEnhancer entities = new HashMap<>();
- public ReactivePanacheMongoEntityEnhancer(IndexView index) {
- super(index, PanacheResourceProcessor.DOTNAME_MUTINY_PANACHE_ENTITY_BASE);
+ public ReactivePanacheMongoEntityEnhancer(IndexView index, List methodCustomizers) {
+ super(index, PanacheResourceProcessor.DOTNAME_MUTINY_PANACHE_ENTITY_BASE, methodCustomizers);
modelInfo = new MetamodelInfo<>();
}
@Override
public ClassVisitor apply(String className, ClassVisitor outputClassVisitor) {
return new PanacheMongoEntityClassVisitor(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo,
- indexView.getClassByName(DotName.createSimple(className)));
+ indexView.getClassByName(DotName.createSimple(className)), methodCustomizers);
}
static class PanacheMongoEntityClassVisitor extends PanacheEntityClassVisitor {
public PanacheMongoEntityClassVisitor(String className, ClassVisitor outputClassVisitor,
MetamodelInfo> modelInfo, ClassInfo panacheEntityBaseClassInfo,
- ClassInfo entityInfo) {
- super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, entityInfo);
+ ClassInfo entityInfo, List methodCustomizers) {
+ super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, entityInfo, methodCustomizers);
}
@Override
diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/JandexUtil.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/JandexUtil.java
index 5a1359ed69f36..475e1990dc9ba 100644
--- a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/JandexUtil.java
+++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/JandexUtil.java
@@ -1,19 +1,18 @@
package io.quarkus.panache.common.deployment;
-import static java.util.stream.Collectors.toList;
-
-import java.util.Arrays;
+import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.jboss.jandex.ArrayType;
-import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.ParameterizedType;
import org.jboss.jandex.PrimitiveType.Primitive;
import org.jboss.jandex.Type;
+import org.jboss.jandex.Type.Kind;
import org.jboss.jandex.TypeVariable;
+import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import io.quarkus.panache.common.impl.GenerateBridge;
@@ -57,6 +56,12 @@ public static String getDescriptor(MethodInfo method, Function t
return descriptor.toString();
}
+ public static String getDescriptor(Type type, Function typeArgMapper) {
+ StringBuilder sb = new StringBuilder();
+ toSignature(sb, type, typeArgMapper, true);
+ return sb.toString();
+ }
+
static void toSignature(StringBuilder sb, Type type, Function typeArgMapper, boolean erased) {
switch (type.kind()) {
case ARRAY:
@@ -163,42 +168,273 @@ public static int getReturnInstruction(String typeDescriptor) {
}
}
- /**
- * Checks if the {@link ClassInfo} contains a method
- *
- * @param classInfo the {@link ClassInfo} instance
- * @param methodName the method name to check
- * @param parameters the parameter types, if any
- * @return true if the {@link ClassInfo} parameter contains this method
- */
- public static boolean containsMethod(ClassInfo classInfo,
- String methodName,
- String returnType,
- String... parameters) {
- List types = Arrays.stream(parameters).map(JandexUtil::toClassType).collect(toList());
- for (MethodInfo methodInfo : classInfo.methods()) {
- if (methodInfo.name().equals(methodName) && methodInfo.parameters().equals(types)) {
- return true;
+ public static int getReturnInstruction(Type jandexType) {
+ if (jandexType.kind() == Kind.PRIMITIVE) {
+ switch (jandexType.asPrimitiveType().primitive()) {
+ case BOOLEAN:
+ case BYTE:
+ case SHORT:
+ case INT:
+ case CHAR:
+ return Opcodes.IRETURN;
+ case DOUBLE:
+ return Opcodes.DRETURN;
+ case FLOAT:
+ return Opcodes.FRETURN;
+ case LONG:
+ return Opcodes.LRETURN;
+ default:
+ throw new IllegalArgumentException("Unknown primitive type: " + jandexType);
+ }
+ } else if (jandexType.kind() == Kind.VOID) {
+ return Opcodes.RETURN;
+ }
+ return Opcodes.ARETURN;
+ }
+
+ public static void visitLdc(MethodVisitor mv, Type jandexType) {
+ switch (jandexType.kind()) {
+ case ARRAY:
+ mv.visitLdcInsn(org.objectweb.asm.Type.getType(jandexType.name().toString('/').replace('.', '/')));
+ break;
+ case CLASS:
+ case PARAMETERIZED_TYPE:
+ mv.visitLdcInsn(org.objectweb.asm.Type.getType("L" + jandexType.name().toString('/') + ";"));
+ break;
+ case PRIMITIVE:
+ switch (jandexType.asPrimitiveType().primitive()) {
+ case BOOLEAN:
+ mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Boolean", "TYPE", "Ljava/lang/Class;");
+ break;
+ case BYTE:
+ mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Byte", "TYPE", "Ljava/lang/Class;");
+ break;
+ case CHAR:
+ mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Character", "TYPE", "Ljava/lang/Class;");
+ break;
+ case DOUBLE:
+ mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Double", "TYPE", "Ljava/lang/Class;");
+ break;
+ case FLOAT:
+ mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Float", "TYPE", "Ljava/lang/Class;");
+ break;
+ case INT:
+ mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Integer", "TYPE", "Ljava/lang/Class;");
+ break;
+ case LONG:
+ mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Long", "TYPE", "Ljava/lang/Class;");
+ break;
+ case SHORT:
+ mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Short", "TYPE", "Ljava/lang/Class;");
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown primitive type: " + jandexType);
+ }
+ break;
+ case TYPE_VARIABLE:
+ List bounds = jandexType.asTypeVariable().bounds();
+ if (bounds.isEmpty())
+ mv.visitLdcInsn(org.objectweb.asm.Type.getType(Object.class));
+ else
+ visitLdc(mv, bounds.get(0));
+ break;
+ case UNRESOLVED_TYPE_VARIABLE:
+ mv.visitLdcInsn(org.objectweb.asm.Type.getType(Object.class));
+ break;
+ case VOID:
+ mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Void", "TYPE", "Ljava/lang/Class;");
+ break;
+ case WILDCARD_TYPE:
+ visitLdc(mv, jandexType.asWildcardType().extendsBound());
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown jandex type: " + jandexType);
+ }
+ }
+
+ public static void boxIfRequired(MethodVisitor mv, Type jandexType) {
+ if (jandexType.kind() == Kind.PRIMITIVE) {
+ switch (jandexType.asPrimitiveType().primitive()) {
+ case BOOLEAN:
+ mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false);
+ break;
+ case BYTE:
+ mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false);
+ break;
+ case CHAR:
+ mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;",
+ false);
+ break;
+ case DOUBLE:
+ mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false);
+ break;
+ case FLOAT:
+ mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false);
+ break;
+ case INT:
+ mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
+ break;
+ case LONG:
+ mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
+ break;
+ case SHORT:
+ mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown primitive type: " + jandexType);
+ }
+ }
+ }
+
+ public static int getLoadOpcode(Type jandexType) {
+ if (jandexType.kind() == Kind.PRIMITIVE) {
+ switch (jandexType.asPrimitiveType().primitive()) {
+ case BOOLEAN:
+ case BYTE:
+ case SHORT:
+ case INT:
+ case CHAR:
+ return Opcodes.ILOAD;
+ case DOUBLE:
+ return Opcodes.DLOAD;
+ case FLOAT:
+ return Opcodes.FLOAD;
+ case LONG:
+ return Opcodes.LLOAD;
+ default:
+ throw new IllegalArgumentException("Unknown primitive type: " + jandexType);
+ }
+ }
+ return Opcodes.ALOAD;
+ }
+
+ public static void unboxIfRequired(MethodVisitor mv, Type jandexType) {
+ if (jandexType.kind() == Kind.PRIMITIVE) {
+ switch (jandexType.asPrimitiveType().primitive()) {
+ case BOOLEAN:
+ unbox(mv, "java/lang/Boolean", "booleanValue", "Z");
+ break;
+ case BYTE:
+ unbox(mv, "java/lang/Byte", "byteValue", "B");
+ break;
+ case CHAR:
+ unbox(mv, "java/lang/Character", "charValue", "C");
+ break;
+ case DOUBLE:
+ unbox(mv, "java/lang/Double", "doubleValue", "D");
+ break;
+ case FLOAT:
+ unbox(mv, "java/lang/Float", "floatValue", "F");
+ break;
+ case INT:
+ unbox(mv, "java/lang/Integer", "intValue", "I");
+ break;
+ case LONG:
+ unbox(mv, "java/lang/Long", "longValue", "J");
+ break;
+ case SHORT:
+ unbox(mv, "java/lang/Short", "shortValue", "S");
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown primitive type: " + jandexType);
}
}
- return false;
}
- static Type toClassType(String type) {
- return Type.create(DotName.createSimple(type), Type.Kind.CLASS);
+ private static void unbox(MethodVisitor mv, String owner, String methodName, String returnTypeSignature) {
+ mv.visitTypeInsn(Opcodes.CHECKCAST, owner);
+ mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, owner, methodName, "()" + returnTypeSignature, false);
}
- public static boolean containsMethod(ClassInfo classInfo, MethodInfo methodInfo) {
- if (classInfo.methods().contains(methodInfo)) {
- return true;
+ public static Type[] getParameterTypes(String methodDescriptor) {
+ String argsSignature = methodDescriptor.substring(methodDescriptor.indexOf('(') + 1, methodDescriptor.lastIndexOf(')'));
+ List args = new ArrayList<>();
+ char[] chars = argsSignature.toCharArray();
+ int dimensions = 0;
+ int start = 0;
+ for (int i = 0; i < chars.length; i++) {
+ char c = chars[i];
+ switch (c) {
+ case 'Z':
+ args.add(Type.create(DotName.createSimple("boolean"),
+ dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE));
+ dimensions = 0;
+ start = i + 1;
+ break;
+ case 'B':
+ args.add(Type.create(DotName.createSimple("byte"),
+ dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE));
+ dimensions = 0;
+ start = i + 1;
+ break;
+ case 'C':
+ args.add(Type.create(DotName.createSimple("char"),
+ dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE));
+ dimensions = 0;
+ start = i + 1;
+ break;
+ case 'D':
+ args.add(Type.create(DotName.createSimple("double"),
+ dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE));
+ dimensions = 0;
+ start = i + 1;
+ break;
+ case 'F':
+ args.add(Type.create(DotName.createSimple("float"),
+ dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE));
+ dimensions = 0;
+ start = i + 1;
+ break;
+ case 'I':
+ args.add(Type.create(DotName.createSimple("int"),
+ dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE));
+ dimensions = 0;
+ start = i + 1;
+ break;
+ case 'J':
+ args.add(Type.create(DotName.createSimple("long"),
+ dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE));
+ dimensions = 0;
+ start = i + 1;
+ break;
+ case 'S':
+ args.add(Type.create(DotName.createSimple("short"),
+ dimensions > 0 ? Kind.ARRAY : Kind.PRIMITIVE));
+ dimensions = 0;
+ start = i + 1;
+ break;
+ case 'L':
+ int end = argsSignature.indexOf(';', i);
+ String binaryName = argsSignature.substring(i + 1, end);
+ // arrays take the entire signature
+ if (dimensions > 0) {
+ args.add(Type.create(DotName.createSimple(argsSignature.substring(start, end + 1)), Kind.ARRAY));
+ dimensions = 0;
+ } else {
+ // class names take only the binary name
+ args.add(Type.create(DotName.createSimple(binaryName.replace('/', '.')), Kind.CLASS));
+ }
+ i = end; // we will have a ++ to get after the ;
+ start = i + 1;
+ break;
+ case '[':
+ dimensions++;
+ break;
+ default:
+ throw new IllegalStateException("Invalid signature char: " + c);
+ }
}
- // MethodInfo may not belong to the same declaring class. Check signature
- for (MethodInfo classMethodInfo : classInfo.methods()) {
- if (classMethodInfo.name().equals(methodInfo.name()) &&
- classMethodInfo.parameters().equals(methodInfo.parameters())) {
- return true;
+ return args.toArray(new Type[0]);
+ }
+
+ public static int getParameterSize(Type paramType) {
+ if (paramType.kind() == Kind.PRIMITIVE) {
+ switch (paramType.asPrimitiveType().primitive()) {
+ case DOUBLE:
+ case LONG:
+ return 2;
}
}
- return false;
+ return 1;
}
}
diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java
index 947706411ef4d..e8f5f1bf9a1f9 100644
--- a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java
+++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java
@@ -1,5 +1,7 @@
package io.quarkus.panache.common.deployment;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -45,10 +47,13 @@ public abstract class PanacheEntityEnhancer methodCustomizers;
- public PanacheEntityEnhancer(IndexView index, DotName panacheEntityBaseName) {
+ public PanacheEntityEnhancer(IndexView index, DotName panacheEntityBaseName,
+ List methodCustomizers) {
this.panacheEntityBaseClassInfo = index.getClassByName(panacheEntityBaseName);
this.indexView = index;
+ this.methodCustomizers = methodCustomizers;
}
@Override
@@ -58,16 +63,18 @@ protected abstract static class PanacheEntityClassVisitor fields;
- // set of name + "/" + descriptor (only for suspected accessor names)
- private Set methods = new HashSet<>();
+ // set of name + "/" + descriptor
+ private Set userMethods = new HashSet<>();
private MetamodelInfo> modelInfo;
private ClassInfo panacheEntityBaseClassInfo;
protected ClassInfo entityInfo;
+ protected List methodCustomizers;
public PanacheEntityClassVisitor(String className, ClassVisitor outputClassVisitor,
MetamodelInfo extends EntityModel extends EntityFieldType>> modelInfo,
ClassInfo panacheEntityBaseClassInfo,
- ClassInfo entityInfo) {
+ ClassInfo entityInfo,
+ List methodCustomizers) {
super(Gizmo.ASM_API_VERSION, outputClassVisitor);
thisClass = Type.getType("L" + className.replace('.', '/') + ";");
this.modelInfo = modelInfo;
@@ -75,6 +82,7 @@ public PanacheEntityClassVisitor(String className, ClassVisitor outputClassVisit
fields = entityModel != null ? entityModel.fields : null;
this.panacheEntityBaseClassInfo = panacheEntityBaseClassInfo;
this.entityInfo = entityInfo;
+ this.methodCustomizers = methodCustomizers;
}
@Override
@@ -118,12 +126,21 @@ public void visitEnd() {
@Override
public MethodVisitor visitMethod(int access, String methodName, String descriptor, String signature,
String[] exceptions) {
- if (methodName.startsWith("get")
- || methodName.startsWith("set")
- || methodName.startsWith("is")) {
- methods.add(methodName + "/" + descriptor);
- }
+ userMethods.add(methodName + "/" + descriptor);
MethodVisitor superVisitor = super.visitMethod(access, methodName, descriptor, signature, exceptions);
+ if (Modifier.isStatic(access)
+ && Modifier.isPublic(access)
+ && (access & Opcodes.ACC_SYNTHETIC) == 0
+ && !methodCustomizers.isEmpty()) {
+ org.jboss.jandex.Type[] argTypes = JandexUtil.getParameterTypes(descriptor);
+ MethodInfo method = this.entityInfo.method(methodName, argTypes);
+ if (method == null) {
+ throw new IllegalStateException(
+ "Could not find indexed method: " + thisClass + "." + methodName + " with descriptor " + descriptor
+ + " and arg types " + Arrays.toString(argTypes));
+ }
+ superVisitor = new PanacheMethodCustomizerVisitor(superVisitor, method, thisClass, methodCustomizers);
+ }
return new PanacheFieldAccessMethodVisitor(superVisitor, thisClass.getInternalName(), methodName, descriptor,
modelInfo);
}
@@ -134,7 +151,8 @@ public void visitEnd() {
for (MethodInfo method : panacheEntityBaseClassInfo.methods()) {
// Do not generate a method that already exists
- if (!JandexUtil.containsMethod(entityInfo, method)) {
+ String descriptor = JandexUtil.getDescriptor(method, name -> null);
+ if (!userMethods.contains(method.name() + "/" + descriptor)) {
AnnotationInstance bridge = method.annotation(JandexUtil.DOTNAME_GENERATE_BRIDGE);
if (bridge != null) {
generateMethod(method, bridge.value("targetReturnTypeErased"));
@@ -165,6 +183,9 @@ private void generateMethod(MethodInfo method, AnnotationValue targetReturnTypeE
mv.visitParameter(method.parameterName(i), 0 /* modifiers */);
}
mv.visitCode();
+ for (PanacheMethodCustomizer customizer : methodCustomizers) {
+ customizer.customize(thisClass, method, mv);
+ }
// inject model
injectModel(mv);
for (int i = 0; i < parameters.size(); i++) {
@@ -202,7 +223,7 @@ private void generateAccessors() {
// Getter
String getterName = field.getGetterName();
String getterDescriptor = "()" + field.descriptor;
- if (!methods.contains(getterName + "/" + getterDescriptor)) {
+ if (!userMethods.contains(getterName + "/" + getterDescriptor)) {
MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC,
getterName, getterDescriptor, field.signature == null ? null : "()" + field.signature, null);
mv.visitCode();
@@ -226,7 +247,7 @@ private void generateAccessors() {
// Setter
String setterName = field.getSetterName();
String setterDescriptor = "(" + field.descriptor + ")V";
- if (!methods.contains(setterName + "/" + setterDescriptor)) {
+ if (!userMethods.contains(setterName + "/" + setterDescriptor)) {
MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC,
setterName, setterDescriptor, field.signature == null ? null : "(" + field.signature + ")V", null);
mv.visitCode();
diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizer.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizer.java
new file mode 100644
index 0000000000000..bf9a86b488bf8
--- /dev/null
+++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizer.java
@@ -0,0 +1,11 @@
+package io.quarkus.panache.common.deployment;
+
+import org.jboss.jandex.MethodInfo;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Type;
+
+public interface PanacheMethodCustomizer {
+
+ public void customize(Type entityClassSignature, MethodInfo method, MethodVisitor mv);
+
+}
diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerBuildItem.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerBuildItem.java
new file mode 100644
index 0000000000000..293d6cf78669a
--- /dev/null
+++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerBuildItem.java
@@ -0,0 +1,18 @@
+package io.quarkus.panache.common.deployment;
+
+import io.quarkus.builder.item.MultiBuildItem;
+
+/**
+ * Build item to declare that a {@link PanacheMethodCustomizer} should be used on Panache-enhanced methods.
+ */
+public final class PanacheMethodCustomizerBuildItem extends MultiBuildItem {
+ private PanacheMethodCustomizer methodCustomizer;
+
+ public PanacheMethodCustomizerBuildItem(PanacheMethodCustomizer methodCustomizer) {
+ this.methodCustomizer = methodCustomizer;
+ }
+
+ public PanacheMethodCustomizer getMethodCustomizer() {
+ return methodCustomizer;
+ }
+}
diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerVisitor.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerVisitor.java
new file mode 100644
index 0000000000000..4fe968a6b3a53
--- /dev/null
+++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerVisitor.java
@@ -0,0 +1,32 @@
+package io.quarkus.panache.common.deployment;
+
+import java.util.List;
+
+import org.jboss.jandex.MethodInfo;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Type;
+
+import io.quarkus.gizmo.Gizmo;
+
+public class PanacheMethodCustomizerVisitor extends MethodVisitor {
+
+ private List methodCustomizers;
+ private MethodInfo method;
+ private Type thisClass;
+
+ public PanacheMethodCustomizerVisitor(MethodVisitor superVisitor, MethodInfo method, Type thisClass,
+ List methodCustomizers) {
+ super(Gizmo.ASM_API_VERSION, superVisitor);
+ this.thisClass = thisClass;
+ this.method = method;
+ this.methodCustomizers = methodCustomizers;
+ }
+
+ @Override
+ public void visitCode() {
+ super.visitCode();
+ for (PanacheMethodCustomizer customizer : methodCustomizers) {
+ customizer.customize(thisClass, method, mv);
+ }
+ }
+}
diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheRepositoryEnhancer.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheRepositoryEnhancer.java
index 9cdb35fc1cfc1..b5a20bc5e8adc 100644
--- a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheRepositoryEnhancer.java
+++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheRepositoryEnhancer.java
@@ -1,7 +1,11 @@
package io.quarkus.panache.common.deployment;
import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.function.BiFunction;
import org.jboss.jandex.AnnotationInstance;
@@ -37,10 +41,15 @@ protected static abstract class PanacheRepositoryClassVisitor extends ClassVisit
protected Type entityType;
protected String entitySignature;
protected String entityBinaryType;
+ protected String idSignature;
+ protected String idBinaryType;
protected String daoBinaryName;
protected ClassInfo daoClassInfo;
protected ClassInfo panacheRepositoryBaseClassInfo;
protected IndexView indexView;
+ protected Map typeArguments = new HashMap<>();
+ // set of name + "/" + descriptor
+ protected Set userMethods = new HashSet<>();
public PanacheRepositoryClassVisitor(String className, ClassVisitor outputClassVisitor,
ClassInfo panacheRepositoryBaseClassInfo, IndexView indexView) {
@@ -67,30 +76,37 @@ public void visit(int version, int access, String name, String signature, String
final String repositoryClassName = name.replace('/', '.');
- String foundEntityType = findEntityBinaryTypeForPanacheRepository(repositoryClassName,
- getPanacheRepositoryDotName());
+ String[] foundTypeArguments = findEntityTypeArgumentsForPanacheRepository(repositoryClassName,
+ getPanacheRepositoryBaseDotName());
- if (foundEntityType == null) {
- foundEntityType = findEntityBinaryTypeForPanacheRepository(repositoryClassName,
- getPanacheRepositoryBaseDotName());
- }
-
- entityBinaryType = foundEntityType;
+ entityBinaryType = foundTypeArguments[0];
entitySignature = "L" + entityBinaryType + ";";
entityType = Type.getType(entitySignature);
+ idBinaryType = foundTypeArguments[1];
+ idSignature = "L" + idBinaryType + ";";
+
+ typeArguments.put("Entity", entitySignature);
+ typeArguments.put("Id", idSignature);
+ }
+
+ @Override
+ public MethodVisitor visitMethod(int access, String methodName, String descriptor, String signature,
+ String[] exceptions) {
+ userMethods.add(methodName + "/" + descriptor);
+ return super.visitMethod(access, methodName, descriptor, signature, exceptions);
}
- private String findEntityBinaryTypeForPanacheRepository(String repositoryClassName, DotName repositoryDotName) {
+ private String[] findEntityTypeArgumentsForPanacheRepository(String repositoryClassName, DotName repositoryDotName) {
for (ClassInfo classInfo : indexView.getAllKnownImplementors(repositoryDotName)) {
if (repositoryClassName.equals(classInfo.name().toString())) {
- return recursivelyFindEntityTypeFromClass(classInfo.name(), repositoryDotName);
+ return recursivelyFindEntityTypeArgumentsFromClass(classInfo.name(), repositoryDotName);
}
}
return null;
}
- private String recursivelyFindEntityTypeFromClass(DotName clazz, DotName repositoryDotName) {
+ private String[] recursivelyFindEntityTypeArgumentsFromClass(DotName clazz, DotName repositoryDotName) {
if (clazz.equals(OBJECT_DOT_NAME)) {
return null;
}
@@ -101,26 +117,105 @@ private String recursivelyFindEntityTypeFromClass(DotName clazz, DotName reposit
throw new IllegalStateException(
"Failed to find supertype " + repositoryDotName + " from entity class " + clazz);
org.jboss.jandex.Type entityType = typeParameters.get(0);
- return entityType.name().toString().replace('.', '/');
+ org.jboss.jandex.Type idType = typeParameters.get(1);
+ return new String[] {
+ entityType.name().toString().replace('.', '/'),
+ idType.name().toString().replace('.', '/')
+ };
}
@Override
public void visitEnd() {
for (MethodInfo method : panacheRepositoryBaseClassInfo.methods()) {
// Do not generate a method that already exists
- if (!JandexUtil.containsMethod(daoClassInfo, method)) {
+ String descriptor = JandexUtil.getDescriptor(method, name -> typeArguments.get(name));
+ if (!userMethods.contains(method.name() + "/" + descriptor)) {
AnnotationInstance bridge = method.annotation(JandexUtil.DOTNAME_GENERATE_BRIDGE);
if (bridge != null) {
- generateMethod(method, bridge.value("targetReturnTypeErased"));
+ generateModelBridge(method, bridge.value("targetReturnTypeErased"));
+ if (needsJvmBridge(method)) {
+ generateJvmBridge(method);
+ }
}
}
}
super.visitEnd();
}
- private void generateMethod(MethodInfo method, AnnotationValue targetReturnTypeErased) {
- String descriptor = JandexUtil.getDescriptor(method, name -> name.equals("Entity") ? entitySignature : null);
- String signature = JandexUtil.getSignature(method, name -> name.equals("Entity") ? entitySignature : null);
+ private boolean needsJvmBridge(MethodInfo method) {
+ if (needsJvmBridge(method.returnType()))
+ return true;
+ for (org.jboss.jandex.Type paramType : method.parameters()) {
+ if (needsJvmBridge(paramType))
+ return true;
+ }
+ return false;
+ }
+
+ private boolean needsJvmBridge(org.jboss.jandex.Type type) {
+ if (type.kind() == Kind.TYPE_VARIABLE) {
+ String typeParamName = type.asTypeVariable().identifier();
+ return typeArguments.containsKey(typeParamName);
+ }
+ return false;
+ }
+
+ private void generateJvmBridge(MethodInfo method) {
+ // get a bounds-erased descriptor
+ String descriptor = JandexUtil.getDescriptor(method, name -> null);
+ // make sure we need a bridge
+ if (!userMethods.contains(method.name() + "/" + descriptor)) {
+ MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_BRIDGE,
+ method.name(),
+ descriptor,
+ null,
+ null);
+ List parameters = method.parameters();
+ for (int i = 0; i < parameters.size(); i++) {
+ mv.visitParameter(method.parameterName(i), 0 /* modifiers */);
+ }
+ mv.visitCode();
+ // this
+ mv.visitIntInsn(Opcodes.ALOAD, 0);
+ // each param
+ for (int i = 0; i < parameters.size(); i++) {
+ org.jboss.jandex.Type paramType = parameters.get(i);
+ if (paramType.kind() == Kind.PRIMITIVE)
+ throw new IllegalStateException("BUG: Don't know how to generate JVM bridge method for " + method
+ + ": has primitive parameters");
+ mv.visitIntInsn(Opcodes.ALOAD, i + 1);
+ if (paramType.kind() == Kind.TYPE_VARIABLE) {
+ String typeParamName = paramType.asTypeVariable().identifier();
+ switch (typeParamName) {
+ case "Entity":
+ mv.visitTypeInsn(Opcodes.CHECKCAST, entityBinaryType);
+ break;
+ case "Id":
+ mv.visitTypeInsn(Opcodes.CHECKCAST, idBinaryType);
+ break;
+ }
+ }
+ }
+
+ String targetDescriptor = JandexUtil.getDescriptor(method, name -> typeArguments.get(name));
+ mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
+ daoBinaryName,
+ method.name(),
+ targetDescriptor, false);
+ String targetReturnTypeDescriptor = targetDescriptor.substring(targetDescriptor.indexOf(')') + 1);
+ mv.visitInsn(JandexUtil.getReturnInstruction(targetReturnTypeDescriptor));
+ mv.visitMaxs(0, 0);
+ mv.visitEnd();
+ }
+
+ }
+
+ private void generateModelBridge(MethodInfo method, AnnotationValue targetReturnTypeErased) {
+ String descriptor = JandexUtil.getDescriptor(method, name -> typeArguments.get(name));
+ // JpaOperations erases the Id type to Object
+ String descriptorForJpaOperations = JandexUtil.getDescriptor(method,
+ name -> name.equals("Entity") ? entitySignature : null);
+ String signature = JandexUtil.getSignature(method, name -> typeArguments.get(name));
List parameters = method.parameters();
String castTo = null;
@@ -134,7 +229,8 @@ private void generateMethod(MethodInfo method, AnnotationValue targetReturnTypeE
castTo = type.name().toString('/');
}
- MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC,
+ // Note: we can't use SYNTHETIC here because otherwise Mockito will never mock these methods
+ MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC,
method.name(),
descriptor,
signature,
@@ -148,7 +244,7 @@ private void generateMethod(MethodInfo method, AnnotationValue targetReturnTypeE
mv.visitIntInsn(Opcodes.ALOAD, i + 1);
}
// inject Class
- String forwardingDescriptor = "(" + getModelDescriptor() + descriptor.substring(1);
+ String forwardingDescriptor = "(" + getModelDescriptor() + descriptorForJpaOperations.substring(1);
if (castTo != null) {
// return type is erased to Object
int lastParen = forwardingDescriptor.lastIndexOf(')');
diff --git a/extensions/panache/panache-mock/pom.xml b/extensions/panache/panache-mock/pom.xml
new file mode 100644
index 0000000000000..df5f35d23e27d
--- /dev/null
+++ b/extensions/panache/panache-mock/pom.xml
@@ -0,0 +1,37 @@
+
+
+
+ quarkus-build-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../../../build-parent/pom.xml
+
+ 4.0.0
+
+ quarkus-panache-mock
+ Quarkus - Panache - Mock
+ Mocking with Panache
+
+
+ io.quarkus
+ quarkus-core
+
+
+ io.quarkus
+ quarkus-arc
+
+
+ io.quarkus
+ quarkus-panache-common-deployment
+
+
+ io.quarkus
+ quarkus-junit5-mockito
+
+
+ org.mockito
+ mockito-core
+
+
+
diff --git a/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/PanacheMock.java b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/PanacheMock.java
new file mode 100644
index 0000000000000..1c9748ef5ff27
--- /dev/null
+++ b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/PanacheMock.java
@@ -0,0 +1,110 @@
+package io.quarkus.panache.mock;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.mockito.Mockito;
+import org.mockito.internal.debugging.LocationImpl;
+import org.mockito.internal.invocation.DefaultInvocationFactory;
+import org.mockito.internal.invocation.InterceptedInvocation;
+import org.mockito.internal.invocation.RealMethod;
+import org.mockito.internal.util.MockUtil;
+import org.mockito.invocation.MockHandler;
+import org.mockito.mock.MockCreationSettings;
+import org.mockito.verification.VerificationMode;
+
+public class PanacheMock {
+
+ public static volatile boolean IsMockEnabled = false;
+
+ private final static Map, Object> mocks = new ConcurrentHashMap<>();
+
+ @SuppressWarnings("unchecked")
+ public static T getMock(Class klass) {
+ return (T) mocks.get(klass);
+ }
+
+ public static Object[] getMocks(Class>... classes) {
+ Object[] mocks = new Object[classes.length];
+ for (int i = 0; i < classes.length; i++) {
+ mocks[i] = getMock(classes[i]);
+ }
+ return mocks;
+ }
+
+ public static void mock(Class>... classes) {
+ for (Class> klass : classes) {
+ mocks.computeIfAbsent(klass, v -> Mockito.mock(klass));
+ }
+ IsMockEnabled = !mocks.isEmpty();
+ }
+
+ public static void reset() {
+ mocks.clear();
+ IsMockEnabled = false;
+ }
+
+ public static boolean isMocked(Class> klass) {
+ return mocks.containsKey(klass);
+ }
+
+ public static Object mockMethod(Class> klass, String methodName, Class>[] parameterTypes, Object[] args)
+ throws InvokeRealMethodException {
+ try {
+ Method invokedMethod = klass.getDeclaredMethod(methodName, parameterTypes);
+ Object mock = getMock(klass);
+ MockCreationSettings> settings = MockUtil.getMockSettings(mock);
+ MyRealMethod myRealMethod = new MyRealMethod();
+ InterceptedInvocation invocation = DefaultInvocationFactory.createInvocation(mock, invokedMethod, args,
+ myRealMethod, settings, new LocationImpl(new Throwable(), true));
+ MockHandler> handler = MockUtil.getMockHandler(mock);
+ return handler.handle(invocation);
+ } catch (InvokeRealMethodException e) {
+ throw e;
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ //
+ // Delegating
+
+ public static T verify(Class klass) {
+ return Mockito.verify(getMock(klass));
+ }
+
+ public static T verify(Class klass, VerificationMode verificationMode) {
+ return Mockito.verify(getMock(klass), verificationMode);
+ }
+
+ public static void verifyNoMoreInteractions(Class>... classes) {
+ Mockito.verifyNoMoreInteractions(getMocks(classes));
+ }
+
+ public static void verifyNoInteractions(Class>... classes) {
+ Mockito.verifyNoInteractions(getMocks(classes));
+ }
+
+ @SuppressWarnings("serial")
+ public static class InvokeRealMethodException extends Exception {
+ }
+
+ @SuppressWarnings("serial")
+ public static class MyRealMethod implements RealMethod {
+
+ @Override
+ public boolean isInvokable() {
+ return true;
+ }
+
+ @Override
+ public Object invoke() throws Throwable {
+ throw new InvokeRealMethodException();
+ }
+
+ }
+
+}
diff --git a/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockAfterEachTest.java b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockAfterEachTest.java
new file mode 100644
index 0000000000000..53a3b60cf959f
--- /dev/null
+++ b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockAfterEachTest.java
@@ -0,0 +1,13 @@
+package io.quarkus.panache.mock.impl;
+
+import io.quarkus.panache.mock.PanacheMock;
+import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback;
+
+public class PanacheMockAfterEachTest implements QuarkusTestAfterEachCallback {
+
+ @Override
+ public void afterEach(Object testInstance) {
+ PanacheMock.reset();
+ }
+
+}
diff --git a/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockBuildChainCustomizer.java b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockBuildChainCustomizer.java
new file mode 100644
index 0000000000000..46334e1b6555b
--- /dev/null
+++ b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockBuildChainCustomizer.java
@@ -0,0 +1,37 @@
+package io.quarkus.panache.mock.impl;
+
+import java.util.function.Consumer;
+
+import org.jboss.jandex.Index;
+
+import io.quarkus.builder.BuildChainBuilder;
+import io.quarkus.builder.BuildContext;
+import io.quarkus.builder.BuildStep;
+import io.quarkus.deployment.builditem.LaunchModeBuildItem;
+import io.quarkus.panache.common.deployment.PanacheMethodCustomizerBuildItem;
+import io.quarkus.runtime.LaunchMode;
+import io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer;
+
+public final class PanacheMockBuildChainCustomizer implements TestBuildChainCustomizerProducer {
+
+ @Override
+ public Consumer produce(Index testClassesIndex) {
+ return new Consumer() {
+
+ @Override
+ public void accept(BuildChainBuilder buildChainBuilder) {
+ buildChainBuilder.addBuildStep(new BuildStep() {
+ @Override
+ public void execute(BuildContext context) {
+ LaunchModeBuildItem launchMode = context.consume(LaunchModeBuildItem.class);
+ if (launchMode.getLaunchMode() == LaunchMode.TEST) {
+ context.produce(new PanacheMethodCustomizerBuildItem(new PanacheMockMethodCustomizer()));
+ }
+ }
+ }).produces(PanacheMethodCustomizerBuildItem.class)
+ .consumes(LaunchModeBuildItem.class)
+ .build();
+ }
+ };
+ }
+}
diff --git a/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockMethodCustomizer.java b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockMethodCustomizer.java
new file mode 100644
index 0000000000000..00463e6a58df6
--- /dev/null
+++ b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockMethodCustomizer.java
@@ -0,0 +1,125 @@
+package io.quarkus.panache.mock.impl;
+
+import org.jboss.jandex.MethodInfo;
+import org.jboss.jandex.Type.Kind;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import io.quarkus.panache.common.deployment.JandexUtil;
+import io.quarkus.panache.common.deployment.PanacheMethodCustomizer;
+import io.quarkus.panache.mock.PanacheMock;
+
+public class PanacheMockMethodCustomizer implements PanacheMethodCustomizer {
+
+ private final static String PANACHE_MOCK_BINARY_NAME = PanacheMock.class.getName().replace('.', '/');
+ private final static String PANACHE_MOCK_INVOKE_REAL_METHOD_EXCEPTION_BINARY_NAME = PanacheMock.InvokeRealMethodException.class
+ .getName().replace('.', '/');
+
+ @Override
+ public void customize(Type entityClassSignature, MethodInfo method, MethodVisitor mv) {
+ /*
+ * Generated code:
+ *
+ * if(PanacheMock.IsMockEnabled && PanacheMock.isMocked(TestClass.class)) {
+ * try {
+ * return (int)PanacheMock.mockMethod(TestClass.class, "foo", new Class>[] {int.class}, new Object[] {arg});
+ * } catch (PanacheMock.InvokeRealMethodException e) {
+ * // fall-through
+ * }
+ * }
+ *
+ * Bytecode approx:
+ *
+ * 0: getstatic #16 // Field PanacheMock.IsMockEnabled:Z
+ * 3: ifeq 50
+ * 6: ldc #1 // class MyTestMockito$TestClass
+ * 8: invokestatic #22 // Method PanacheMock.isMocked:(Ljava/lang/Class;)Z
+ * 11: ifeq 50
+ * 14: ldc #1 // class MyTestMockito$TestClass
+ * 16: ldc #26 // String foo
+ *
+ * 18: iconst_1
+ * 19: anewarray #27 // class java/lang/Class
+ * 22: dup
+ * 23: iconst_0
+ * 24: getstatic #29 // Field java/lang/Integer.TYPE:Ljava/lang/Class;
+ * 27: aastore
+ *
+ * 28: iconst_1
+ * 29: anewarray #3 // class java/lang/Object
+ * 32: dup
+ * 33: iconst_0
+ * 34: iload_0
+ * 35: invokestatic #35 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
+ * 38: aastore
+ *
+ * 39: invokestatic #39 // Method
+ * PanacheMock.mockMethod:(Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Class;[Ljava/lang/Object;)Ljava/lang/Object;
+ * 42: checkcast #30 // class java/lang/Integer
+ * 45: invokevirtual #43 // Method java/lang/Integer.intValue:()I
+ * 48: ireturn
+ * 49: astore_1
+ */
+ Label realMethodLabel = new Label();
+
+ mv.visitFieldInsn(Opcodes.GETSTATIC, PANACHE_MOCK_BINARY_NAME, "IsMockEnabled", "Z");
+ mv.visitJumpInsn(Opcodes.IFEQ, realMethodLabel);
+
+ mv.visitLdcInsn(entityClassSignature);
+ mv.visitMethodInsn(Opcodes.INVOKESTATIC, PANACHE_MOCK_BINARY_NAME, "isMocked", "(Ljava/lang/Class;)Z", false);
+ mv.visitJumpInsn(Opcodes.IFEQ, realMethodLabel);
+
+ Label tryStart = new Label();
+ Label tryEnd = new Label();
+ Label tryHandler = new Label();
+ mv.visitTryCatchBlock(tryStart, tryEnd, tryHandler, PANACHE_MOCK_INVOKE_REAL_METHOD_EXCEPTION_BINARY_NAME);
+ mv.visitLabel(tryStart);
+
+ mv.visitLdcInsn(entityClassSignature);
+ mv.visitLdcInsn(method.name());
+
+ mv.visitLdcInsn(method.parameters().size());
+ mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Class");
+
+ int i = 0;
+ for (org.jboss.jandex.Type paramType : method.parameters()) {
+ mv.visitInsn(Opcodes.DUP);
+ mv.visitLdcInsn(i);
+ JandexUtil.visitLdc(mv, paramType);
+ mv.visitInsn(Opcodes.AASTORE);
+ i++;
+ }
+
+ mv.visitLdcInsn(method.parameters().size());
+ mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object");
+
+ i = 0;
+ int paramSlot = 0;
+ for (org.jboss.jandex.Type paramType : method.parameters()) {
+ mv.visitInsn(Opcodes.DUP);
+ mv.visitLdcInsn(i);
+ mv.visitVarInsn(JandexUtil.getLoadOpcode(paramType), paramSlot);
+ JandexUtil.boxIfRequired(mv, paramType);
+ mv.visitInsn(Opcodes.AASTORE);
+ i++;
+ paramSlot += JandexUtil.getParameterSize(paramType);
+ }
+
+ mv.visitMethodInsn(Opcodes.INVOKESTATIC, PANACHE_MOCK_BINARY_NAME, "mockMethod",
+ "(Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Class;[Ljava/lang/Object;)Ljava/lang/Object;", false);
+ JandexUtil.unboxIfRequired(mv, method.returnType());
+ if (method.returnType().kind() != Kind.PRIMITIVE) {
+ mv.visitTypeInsn(Opcodes.CHECKCAST, method.returnType().name().toString('/'));
+ }
+
+ mv.visitInsn(JandexUtil.getReturnInstruction(method.returnType()));
+
+ mv.visitLabel(tryHandler);
+ mv.visitInsn(Opcodes.POP);
+ mv.visitLabel(tryEnd);
+
+ mv.visitLabel(realMethodLabel);
+ }
+}
diff --git a/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer b/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer
new file mode 100644
index 0000000000000..8e2b635300aac
--- /dev/null
+++ b/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer
@@ -0,0 +1 @@
+io.quarkus.panache.mock.impl.PanacheMockBuildChainCustomizer
diff --git a/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback b/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback
new file mode 100644
index 0000000000000..fc04ccca8c2d8
--- /dev/null
+++ b/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback
@@ -0,0 +1 @@
+io.quarkus.panache.mock.impl.PanacheMockAfterEachTest
diff --git a/extensions/panache/pom.xml b/extensions/panache/pom.xml
index 34220535ae042..8a36507c66f7f 100644
--- a/extensions/panache/pom.xml
+++ b/extensions/panache/pom.xml
@@ -15,6 +15,7 @@
pom
panache-common
+ panache-mock
hibernate-orm-panache
mongodb-panache
panacheql
diff --git a/integration-tests/hibernate-orm-panache/pom.xml b/integration-tests/hibernate-orm-panache/pom.xml
index 9006bfdc8c368..df67b02643d5c 100644
--- a/integration-tests/hibernate-orm-panache/pom.xml
+++ b/integration-tests/hibernate-orm-panache/pom.xml
@@ -78,6 +78,17 @@
quarkus-jackson
test
+
+ io.quarkus
+ quarkus-panache-mock
+ test
+
+
+ net.bytebuddy
+ byte-buddy
+
+
+
com.fasterxml.jackson.module
jackson-module-jaxb-annotations
diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/MockablePersonRepository.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/MockablePersonRepository.java
new file mode 100644
index 0000000000000..a83ca6dcecaa1
--- /dev/null
+++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/MockablePersonRepository.java
@@ -0,0 +1,14 @@
+package io.quarkus.it.panache;
+
+import java.util.List;
+
+import javax.enterprise.context.ApplicationScoped;
+
+import io.quarkus.hibernate.orm.panache.PanacheRepository;
+
+@ApplicationScoped
+public class MockablePersonRepository implements PanacheRepository {
+ public List findOrdered() {
+ return find("ORDER BY name").list();
+ }
+}
diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Person.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Person.java
index 8042ebc531645..5afa058e8dbb1 100644
--- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Person.java
+++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Person.java
@@ -41,7 +41,7 @@ public class Person extends PanacheEntity {
@Transient
public int serialisationTrick;
- public static List findOrdered() {
+ public static List findOrdered() {
return find("ORDER BY name").list();
}
@@ -55,4 +55,8 @@ public int getSerialisationTrick() {
public void setSerialisationTrick(int serialisationTrick) {
this.serialisationTrick = serialisationTrick;
}
+
+ public static long methodWithPrimitiveParams(boolean b, byte bb, short s, int i, long l, float f, double d, char c) {
+ return 0;
+ }
}
diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/PersonRepository.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/PersonRepository.java
index 65eaca454e303..805f66f4e3db9 100644
--- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/PersonRepository.java
+++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/PersonRepository.java
@@ -1,9 +1,14 @@
package io.quarkus.it.panache;
+import java.util.List;
+
import javax.enterprise.context.ApplicationScoped;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
@ApplicationScoped
public class PersonRepository implements PanacheRepository {
+ public List findOrdered() {
+ return find("ORDER BY name").list();
+ }
}
diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java
index 3b51441e9a840..eca1f32696730 100644
--- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java
+++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java
@@ -43,6 +43,10 @@
@Path("test")
public class TestEndpoint {
+ // fake unused injection point to force ArC to not remove this otherwise I can't mock it in the tests
+ @Inject
+ MockablePersonRepository mockablePersonRepository;
+
@GET
@Path("model")
@Transactional
diff --git a/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheMockingTest.java b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheMockingTest.java
new file mode 100644
index 0000000000000..722070bdc989b
--- /dev/null
+++ b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheMockingTest.java
@@ -0,0 +1,136 @@
+package io.quarkus.it.panache;
+
+import java.util.Collections;
+import java.util.Optional;
+
+import javax.inject.Inject;
+import javax.persistence.LockModeType;
+import javax.ws.rs.WebApplicationException;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
+import io.quarkus.panache.mock.PanacheMock;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.mockito.InjectMock;
+
+@QuarkusTest
+public class PanacheMockingTest {
+
+ @Test
+ @Order(1)
+ public void testPanacheMocking() {
+ PanacheMock.mock(Person.class);
+
+ Assertions.assertEquals(0, Person.count());
+
+ Mockito.when(Person.count()).thenReturn(23l);
+ Assertions.assertEquals(23, Person.count());
+
+ Mockito.when(Person.count()).thenReturn(42l);
+ Assertions.assertEquals(42, Person.count());
+
+ Mockito.when(Person.count()).thenCallRealMethod();
+ Assertions.assertEquals(0, Person.count());
+
+ PanacheMock.verify(Person.class, Mockito.times(4)).count();
+
+ Person p = new Person();
+ Mockito.when(Person.findById(12l)).thenReturn(p);
+ Assertions.assertSame(p, Person.findById(12l));
+ Assertions.assertNull(Person.findById(42l));
+
+ Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException());
+ try {
+ Person.findById(12l);
+ Assertions.fail();
+ } catch (WebApplicationException x) {
+ }
+
+ Mockito.when(Person.findOrdered()).thenReturn(Collections.emptyList());
+ Assertions.assertTrue(Person.findOrdered().isEmpty());
+
+ PanacheMock.verify(Person.class).findOrdered();
+ 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'));
+ }
+
+ @Test
+ @Order(2)
+ public void testPanacheMockingWasCleared() {
+ Assertions.assertFalse(PanacheMock.IsMockEnabled);
+ }
+
+ @InjectMock
+ MockablePersonRepository mockablePersonRepository;
+
+ @Test
+ public void testPanacheRepositoryMocking() throws Throwable {
+ Assertions.assertEquals(0, mockablePersonRepository.count());
+
+ Mockito.when(mockablePersonRepository.count()).thenReturn(23l);
+ Assertions.assertEquals(23, mockablePersonRepository.count());
+
+ Mockito.when(mockablePersonRepository.count()).thenReturn(42l);
+ Assertions.assertEquals(42, mockablePersonRepository.count());
+
+ Mockito.when(mockablePersonRepository.count()).thenCallRealMethod();
+ Assertions.assertEquals(0, mockablePersonRepository.count());
+
+ Mockito.verify(mockablePersonRepository, Mockito.times(4)).count();
+
+ Person p = new Person();
+ Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(p);
+ Assertions.assertSame(p, mockablePersonRepository.findById(12l));
+ Assertions.assertNull(mockablePersonRepository.findById(42l));
+
+ Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException());
+ try {
+ mockablePersonRepository.findById(12l);
+ Assertions.fail();
+ } catch (WebApplicationException x) {
+ }
+
+ Mockito.when(mockablePersonRepository.findOrdered()).thenReturn(Collections.emptyList());
+ Assertions.assertTrue(mockablePersonRepository.findOrdered().isEmpty());
+
+ Mockito.verify(mockablePersonRepository).findOrdered();
+ Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any());
+ Mockito.verifyNoMoreInteractions(mockablePersonRepository);
+ }
+
+ @Inject
+ PersonRepository realPersonRepository;
+
+ @Test
+ public void testPanacheRepositoryBridges() {
+ // normal method call
+ Assertions.assertNull(realPersonRepository.findById(0l));
+ // bridge call
+ Assertions.assertNull(((PanacheRepositoryBase) realPersonRepository).findById(0l));
+ // normal method call
+ Assertions.assertNull(realPersonRepository.findById(0l, LockModeType.NONE));
+ // bridge call
+ Assertions.assertNull(((PanacheRepositoryBase) realPersonRepository).findById(0l, LockModeType.NONE));
+
+ // normal method call
+ Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l));
+ // bridge call
+ Assertions.assertEquals(Optional.empty(), ((PanacheRepositoryBase) realPersonRepository).findByIdOptional(0l));
+ // normal method call
+ Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l, LockModeType.NONE));
+ // bridge call
+ Assertions.assertEquals(Optional.empty(),
+ ((PanacheRepositoryBase) realPersonRepository).findByIdOptional(0l, LockModeType.NONE));
+
+ // normal method call
+ Assertions.assertEquals(false, realPersonRepository.deleteById(0l));
+ // bridge call
+ Assertions.assertEquals(false, ((PanacheRepositoryBase) realPersonRepository).deleteById(0l));
+ }
+}
diff --git a/integration-tests/mongodb-panache/pom.xml b/integration-tests/mongodb-panache/pom.xml
index c5be2c1c9709a..fe0c950cfb7ab 100755
--- a/integration-tests/mongodb-panache/pom.xml
+++ b/integration-tests/mongodb-panache/pom.xml
@@ -44,6 +44,11 @@
quarkus-junit5
test
+
+ io.quarkus
+ quarkus-panache-mock
+ test
+
io.rest-assured
rest-assured
diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/MockablePersonRepository.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/MockablePersonRepository.java
new file mode 100644
index 0000000000000..51d645b204e97
--- /dev/null
+++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/MockablePersonRepository.java
@@ -0,0 +1,15 @@
+package io.quarkus.it.mongodb.panache.person;
+
+import java.util.List;
+
+import javax.enterprise.context.ApplicationScoped;
+
+import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase;
+import io.quarkus.panache.common.Sort;
+
+@ApplicationScoped
+public class MockablePersonRepository implements PanacheMongoRepositoryBase {
+ public List findOrdered() {
+ return findAll(Sort.by("lastname", "firstname")).list();
+ }
+}
diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java
index 4a146d9d9c1a5..be33769b45e73 100644
--- a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java
+++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java
@@ -1,12 +1,19 @@
package io.quarkus.it.mongodb.panache.person;
+import java.util.List;
+
import org.bson.codecs.pojo.annotations.BsonId;
import io.quarkus.mongodb.panache.PanacheMongoEntityBase;
+import io.quarkus.panache.common.Sort;
public class PersonEntity extends PanacheMongoEntityBase {
@BsonId
public Long id;
public String firstname;
public String lastname;
+
+ public static List findOrdered() {
+ return findAll(Sort.by("lastname", "firstname")).list();
+ }
}
diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java
index e84584094bc04..8c7e1a54e3741 100644
--- a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java
+++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java
@@ -15,6 +15,10 @@
@Consumes(MediaType.APPLICATION_JSON)
public class PersonRepositoryResource {
+ // fake unused injection point to force ArC to not remove this otherwise I can't mock it in the tests
+ @Inject
+ MockablePersonRepository mockablePersonRepository;
+
@Inject
PersonRepository personRepository;
diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheMockingTest.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheMockingTest.java
new file mode 100644
index 0000000000000..1f5d7ad5f72be
--- /dev/null
+++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheMockingTest.java
@@ -0,0 +1,131 @@
+package io.quarkus.it.mongodb.panache;
+
+import java.util.Collections;
+import java.util.Optional;
+
+import javax.inject.Inject;
+import javax.ws.rs.WebApplicationException;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import io.quarkus.it.mongodb.panache.person.MockablePersonRepository;
+import io.quarkus.it.mongodb.panache.person.PersonEntity;
+import io.quarkus.it.mongodb.panache.person.PersonRepository;
+import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase;
+import io.quarkus.panache.mock.PanacheMock;
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.mockito.InjectMock;
+
+@QuarkusTest
+@QuarkusTestResource(MongoTestResource.class)
+public class MongodbPanacheMockingTest {
+
+ @Test
+ @Order(1)
+ public void testPanacheMocking() {
+ PanacheMock.mock(PersonEntity.class);
+
+ Assertions.assertEquals(0, PersonEntity.count());
+
+ Mockito.when(PersonEntity.count()).thenReturn(23l);
+ Assertions.assertEquals(23, PersonEntity.count());
+
+ Mockito.when(PersonEntity.count()).thenReturn(42l);
+ Assertions.assertEquals(42, PersonEntity.count());
+
+ Mockito.when(PersonEntity.count()).thenCallRealMethod();
+ Assertions.assertEquals(0, PersonEntity.count());
+
+ PanacheMock.verify(PersonEntity.class, Mockito.times(4)).count();
+
+ PersonEntity p = new PersonEntity();
+
+ Mockito.when(PersonEntity.findById(12l)).thenReturn(p);
+ Assertions.assertSame(p, PersonEntity.findById(12l));
+ Assertions.assertNull(PersonEntity.findById(42l));
+
+ Mockito.when(PersonEntity.findById(12l)).thenThrow(new WebApplicationException());
+ try {
+ PersonEntity.findById(12l);
+ Assertions.fail();
+ } catch (WebApplicationException x) {
+ }
+
+ Mockito.when(PersonEntity.findOrdered()).thenReturn(Collections.emptyList());
+ Assertions.assertTrue(PersonEntity.findOrdered().isEmpty());
+
+ PanacheMock.verify(PersonEntity.class).findOrdered();
+ PanacheMock.verify(PersonEntity.class, Mockito.atLeastOnce()).findById(Mockito.any());
+ PanacheMock.verifyNoMoreInteractions(PersonEntity.class);
+ }
+
+ @Test
+ @Order(2)
+ public void testPanacheMockingWasCleared() {
+ Assertions.assertFalse(PanacheMock.IsMockEnabled);
+ }
+
+ @InjectMock
+ MockablePersonRepository mockablePersonRepository;
+
+ @Test
+ public void testPanacheRepositoryMocking() throws Throwable {
+ Assertions.assertEquals(0, mockablePersonRepository.count());
+
+ Mockito.when(mockablePersonRepository.count()).thenReturn(23l);
+ Assertions.assertEquals(23, mockablePersonRepository.count());
+
+ Mockito.when(mockablePersonRepository.count()).thenReturn(42l);
+ Assertions.assertEquals(42, mockablePersonRepository.count());
+
+ Mockito.when(mockablePersonRepository.count()).thenCallRealMethod();
+ Assertions.assertEquals(0, mockablePersonRepository.count());
+
+ Mockito.verify(mockablePersonRepository, Mockito.times(4)).count();
+
+ PersonEntity p = new PersonEntity();
+ Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(p);
+ Assertions.assertSame(p, mockablePersonRepository.findById(12l));
+ Assertions.assertNull(mockablePersonRepository.findById(42l));
+
+ Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException());
+ try {
+ mockablePersonRepository.findById(12l);
+ Assertions.fail();
+ } catch (WebApplicationException x) {
+ }
+
+ Mockito.when(mockablePersonRepository.findOrdered()).thenReturn(Collections.emptyList());
+ Assertions.assertTrue(mockablePersonRepository.findOrdered().isEmpty());
+
+ Mockito.verify(mockablePersonRepository).findOrdered();
+ Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any());
+ Mockito.verifyNoMoreInteractions(mockablePersonRepository);
+ }
+
+ @Inject
+ PersonRepository realPersonRepository;
+
+ @Test
+ public void testPanacheRepositoryBridges() {
+ // normal method call
+ Assertions.assertNull(realPersonRepository.findById(0l));
+ // bridge call
+ Assertions.assertNull(((PanacheMongoRepositoryBase) realPersonRepository).findById(0l));
+
+ // normal method call
+ Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l));
+ // bridge call
+ Assertions.assertEquals(Optional.empty(), ((PanacheMongoRepositoryBase) realPersonRepository).findByIdOptional(0l));
+
+ // normal method call
+ Assertions.assertEquals(false, realPersonRepository.deleteById(0l));
+ // bridge call
+ Assertions.assertEquals(false, ((PanacheMongoRepositoryBase) realPersonRepository).deleteById(0l));
+ }
+
+}