diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 870b76c1234f9..ee2ece8a083ab 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -182,7 +182,7 @@ 1.15.1 3.44.0 2.28.0 - 0.27.1 + 0.27.0 1.44.2 2.1 4.7.6 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java index b0c007df14313..58e502a49d66e 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java @@ -49,6 +49,7 @@ public enum Feature { JDBC_MSSQL, JDBC_MYSQL, JDBC_ORACLE, + JFR, KAFKA_CLIENT, KAFKA_STREAMS, KEYCLOAK_AUTHORIZATION, diff --git a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java index 1d080ab8be9f8..4d8b4b9315749 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java @@ -240,7 +240,7 @@ public TemplateHtmlBuilder staticResourcePath(String title, String description) } public TemplateHtmlBuilder servletMapping(String title) { - return resourcePath(title, false, false, null); + return resourcePath(title, false, true, null); } private TemplateHtmlBuilder resourcePath(String title, boolean withListStart, boolean withAnchor, String description) { diff --git a/docs/src/main/asciidoc/flyway.adoc b/docs/src/main/asciidoc/flyway.adoc index ecdee7ee18944..0aa4178ef3956 100644 --- a/docs/src/main/asciidoc/flyway.adoc +++ b/docs/src/main/asciidoc/flyway.adoc @@ -28,9 +28,7 @@ In your build file, add the following dependencies: * the Flyway extension * your JDBC driver extension (`quarkus-jdbc-postgresql`, `quarkus-jdbc-h2`, `quarkus-jdbc-mariadb`, ...) -* the MariaDB/MySQL support is now in a separate dependency, MariaDB/MySQL users need to add the `flyway-mysql` dependency from now on. -* the Microsoft SQL Server support is now in a separate dependency, Microsoft SQL Server users need to add the `flyway-sqlserver` dependency from now on. -* the Oracle support is now in a separate dependency, Oracle users need to add the `flyway-database-oracle` dependency from now on. +* unless you're using in-memory or file databases (such as H2 or SQLite), you need to add a flyway module dependency corresponding to the database you're using. (https://github.com/flyway/flyway/issues/3780[for more details]) [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml @@ -41,6 +39,12 @@ In your build file, add the following dependencies: quarkus-flyway + + + io.quarkus + quarkus-jdbc-postgresql + + org.flywaydb @@ -59,11 +63,84 @@ In your build file, add the following dependencies: flyway-database-oracle - + - io.quarkus - quarkus-jdbc-postgresql + org.flywaydb + flyway-database-postgresql + + + + + org.flywaydb + flyway-database-db2 + + + + + org.flywaydb + flyway-database-derby + + + + + org.flywaydb + flyway-database-hsqldb + + + + + org.flywaydb + flyway-database-informix + + + + + org.flywaydb + flyway-database-redshift + + + + + org.flywaydb + flyway-database-saphana + + + + + org.flywaydb + flyway-database-snowflake + + + + + org.flywaydb + flyway-database-sybasease + + + + + org.flywaydb + flyway-firebird + + + + + org.flywaydb + flyway-gcp-bigquery + + + + + org.flywaydb + flyway-gcp-spanner + + + + + org.flywaydb + flyway-singlestore + ---- [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] @@ -71,14 +148,41 @@ In your build file, add the following dependencies: ---- // Flyway specific dependencies implementation("io.quarkus:quarkus-flyway") +// JDBC driver dependencies +implementation("io.quarkus:quarkus-jdbc-postgresql") // Flyway SQL Server specific dependencies implementation("org.flywaydb:flyway-sqlserver") // Flyway MariaDB/MySQL specific dependencies implementation("org.flywaydb:flyway-mysql") // Flyway Oracle specific dependencies implementation("org.flywaydb:flyway-database-oracle") -// JDBC driver dependencies -implementation("io.quarkus:quarkus-jdbc-postgresql") +// Flyway Postgres specific dependencies +implementation("org.flywaydb:flyway-database-postgresql") +// Flyway DB2 specific dependencies +implementation("org.flywaydb:flyway-database-db2") +// Flyway Derby specific dependencies +implementation("org.flywaydb:flyway-database-derby") +// HSQLDB specific dependencies +implementation("org.flywaydb:flyway-database-hsqldb") +// Informix specific dependencies +implementation("org.flywaydb:flyway-database-informix") +// Redshift specific dependencies +implementation("org.flywaydb:flyway-database-redshift") +// Saphana specific dependencies +implementation("org.flywaydb:flyway-database-saphana") +// Snowflake specific dependencies +implementation("org.flywaydb:flyway-database-snowflake") +// Sybasease specific dependencies +implementation("org.flywaydb:flyway-database-sybasease") +// Firebird specific dependencies +implementation("org.flywaydb:flyway-firebird") +// BigQuery specific dependencies +implementation("org.flywaydb:flyway-gcp-bigquery") +// Spanner specific dependencies +implementation("org.flywaydb:flyway-gcp-spanner") +// Singlestore specific dependencies +implementation("org.flywaydb:flyway-singlestore:10.15.0") + ---- Flyway support relies on the Quarkus datasource config. diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 25a273ea77dca..3cf9de3d8d005 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -1682,7 +1682,8 @@ Finally, the CDI request context is activated and terminated per each test metho Test class fields annotated with `@jakarta.inject.Inject` and `@io.quarkus.test.InjectMock` are injected after a test instance is created. Dependent beans injected into these fields are correctly destroyed before a test instance is destroyed. -Parameters of a test method for which a matching bean exists are resolved unless annotated with `@io.quarkus.test.component.SkipInject`. +Parameters of a test method for which a matching bean exists are resolved unless annotated with `@io.quarkus.test.component.SkipInject` or `@org.mockito.Mock`. +There are also some JUnit built-in parameters, such as `RepetitionInfo` and `TestInfo`, which are skipped automatically. Dependent beans injected into the test method arguments are correctly destroyed after the test method completes. NOTE: Arguments of a `@ParameterizedTest` method that are provided by an `ArgumentsProvider`, for example with `@org.junit.jupiter.params.provider.ValueArgumentsProvider`, must be annotated with `@SkipInject`. @@ -1695,6 +1696,34 @@ The bean has the `@Singleton` scope so it's shared across all injection points w The injected reference is an _unconfigured_ Mockito mock. You can inject the mock in your test using the `io.quarkus.test.InjectMock` annotation and leverage the Mockito API to configure the behavior. +[NOTE] +==== +`@InjectMock` is not intended as a universal replacement for functionality provided by the Mockito JUnit extension. +It's meant to be used for configuration of unsatisfied dependencies of CDI beans. +You can use the `QuarkusComponentTest` and `MockitoExtension` side by side. + +[source, java] +---- +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +@QuarkusComponentTest +public class FooTest { + + @TestConfigProperty(key = "bar", value = "true") + @Test + public void testPing(Foo foo, @InjectMock Charlie charlieMock, @Mock Ping ping) { + Mockito.when(ping.pong()).thenReturn("OK"); + Mockito.when(charlieMock.ping()).thenReturn(ping); + assertEquals("OK", foo.ping()); + } +} +---- + +==== + === Custom Mocks For Unsatisfied Dependencies Sometimes you need the full control over the bean attributes and maybe even configure the default mock behavior. diff --git a/docs/src/main/asciidoc/jfr.adoc b/docs/src/main/asciidoc/jfr.adoc index c5ada66c7025a..34eebbdcd58c8 100644 --- a/docs/src/main/asciidoc/jfr.adoc +++ b/docs/src/main/asciidoc/jfr.adoc @@ -3,20 +3,20 @@ This guide is maintained in the main Quarkus repository and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// -= Using JDK Flight Recorder +[id="jfr"] += Using Java Flight Recorder include::_attributes.adoc[] :categories: observability -:summary: This guide explains how JDK Flight Recorder can be extended to provide insight into your Quarkus application. +:summary: This guide explains how Java Flight Recorder (JFR) can be extended to provide additional insight into your Quarkus application. :topics: observability,jfr :extensions: io.quarkus:quarkus-jfr -This guide explains how https://openjdk.org/jeps/328[JDK Flight Recorder] (JFR) can be extended to provide insight into your Quarkus application. -insight into itself. +This guide explains how https://openjdk.org/jeps/328[Java Flight Recorder] (JFR) can be extended to provide additional insight into your Quarkus application. JFR records various information from the Java standard API and JVM as events. -By adding this extension, you can add custom Quarkus events to JFR. This will help you solve problems in your application. +By adding the Quarkus JFR extension, you can add custom Quarkus events to JFR. This will help you solve potential problems in your application. JFR can be preconfigured to dump a file, and when the application exits, JFR will output the file. -The file will contain the contents of the JFR event stream to which Quarkus custom events have also been added. +The file will contain the content of the JFR event stream to which Quarkus custom events have also been added. You can, of course, get this file at any time you want, even if your application exits unexpectedly. == Prerequisites @@ -26,7 +26,7 @@ include::{includes}/prerequisites.adoc[] == Architecture -In this guide, we create a straightforward REST application to demonstrate JFR. +In this guide, we create a straightforward REST application to demonstrate JFR usage. == Creating the Maven project @@ -36,10 +36,10 @@ First, we need a new project. Create a new project with the following command: :create-app-extensions: quarkus-rest,quarkus-jfr include::{includes}/devtools/create-app.adoc[] -This command generates the Maven project and imports the `quarkus-jfr` extension, +This command generates the Maven project and imports the Quarkus JFR extension, which includes the default JFR support. -If you already have your Quarkus project configured, you can add the `quarkus-jfr` extension +If you already have your Quarkus project configured, you can add the JFR extension to your project by running the following command in your project base directory: :add-extension-extensions: quarkus-jfr @@ -97,7 +97,7 @@ We can launch the application with JFR configured to be enabled from the startup :dev-additional-parameters: -Djvm.args="-XX:StartFlightRecording=name=quarkus,dumponexit=true,filename=myrecording.jfr" include::{includes}/devtools/dev.adoc[] -With the JDK Flight Recorder and the application running, we can make a request to the provided endpoint: +With the Java Flight Recorder and the application running, we can make a request to the provided endpoint: [source,shell] ---- @@ -105,43 +105,48 @@ $ curl http://localhost:8080/hello hello ---- -This is all that was needed to write the information to the JFR. +This is all that is needed to write the information to JFR. -=== Save the JFR to a file -As mentioned above, the Quarkus application was configured to also start JFR at startup and dump it to a `myrecording.jfr` when it terminates. +=== Save the JFR events to a file + +As mentioned above, the Quarkus application was configured to also start JFR at startup and dump it to a `myrecording.jfr` when it terminates. So we can get the file when we hit `CTRL+C` or type `q` to stop the application. -Or, we can also dump with the jcmd command. +Or, we can also dump it with the `jcmd` command. +[source,shell] ---- jcmd JFR.dump name=quarkus filename=myrecording.jfr ---- [NOTE] ==== -Running jcmd command give us a list of running Java processes and the PID of each process. +Running the `jcmd` command give us a list of running Java processes and the PID of each process. ==== == Open JFR dump file -We can open a JFR dump using two tools: Jfr CLI and JDK Mission Control (JMC). -It is also possible to read them using JFR APIs, but we won't go into that here. +We can open a JFR dump using two tools: the `jfr` CLI and JDK Mission Control (JMC). +It is also possible to read them using JFR APIs, but we won't go into that in this guide. === jfr CLI -The jfr CLI is a tool included in OpenJDK. The executable file is `$JAVA_HOME/bin/jfr`. -We can use the jfr CLI to see a list of events limited to those related to Quarkus in the dump file by doing the following. +The `jfr` CLI is a tool included in OpenJDK. The executable file is `$JAVA_HOME/bin/jfr`. +We can use the jfr CLI to see a list of events limited to those related to Quarkus in the dump file by running the following command: +[source,shell] ---- jfr print --categories quarkus myrecording.jfr ---- === JDK Mission Control -JMC is essentially a GUI viewer for JFR. -Some distributions include JMC in OpenJDK binary, but if not, we need to download it manually. -To see a list of events using the JMC, first we load the JFR file in the JMC as follows. +JMC is essentially a GUI for JFR. +Some Java distributions include JMC, but if not, you need to download it manually. +To see a list of events using JMC, first we load the JFR file in JMC as follows. + +[source,shell] ---- jmc -open myrecording.jfr ---- @@ -149,11 +154,11 @@ jmc -open myrecording.jfr After opening the JFR file, we have two options. One is to view the events as a tabular list, and the other is to view the events on the threads in which they occurred, in chronological order. -To view Quarkus events in tabular style, select the Event Browser on the left side of the JMC, then open the Quarkus event type tree on the right side of the JMC. +To view Quarkus events in tabular style, select the Event Browser on the left side of JMC, then open the Quarkus event type tree on the right side of JMC. image::jfr-event-browser.png[alt=JDK Mission Control Event Browser view,role="center"] -To see Quarkus events in chronological order on a thread, select the `Java application` and `Threads` on the left side of the JMC. +To see Quarkus events in chronological order on a thread, select the `Java application` and `Threads` on the left side of JMC. image::jfr-java-ap-thread.png[alt=JDK Mission Control thread view,role="center"] @@ -172,16 +177,18 @@ image::jfr-thread.png[alt=JDK Mission Control thread view,role="center"] [NOTE] ==== -Non-blocking is where multiple processes are processed apparently simultaneously in a single thread. -Therefore, this extension records multiple JFR events concurrently, and a number of events might overlap on the JMC. +This extension is able to records multiple JFR events concurrently (emitted by different threads or from the same thread in the case of the reactive execution model). +Thus, events might overlap in JMC. This could make it difficult for you to see the events you want to see. -To avoid this, we recommend to use xref:#identifying-requests[Request ID] to filter events so that you only see the information about the requests you want to see. +To avoid this, we recommend to use <> to filter events so that you only see the information about the requests you want to see. ==== == Events +[[identifying-requests]] === Identifying Requests -This extension works with the OpenTelemetry extension. + +This extension collaborates with the OpenTelemetry extension. The events recorded by this extension have a trace ID and a span ID. These are recorded with the OpenTelemetry IDs respectively. This means that after we identify the trace and span IDs of interest from the UI provided by the OpenTelemetry implementation, we can immediately jump to the details in JFR using those IDs. @@ -189,9 +196,10 @@ This means that after we identify the trace and span IDs of interest from the UI If we have not enabled the OpenTelemetry extension, this extension creates an ID for each request and links it to JFR events as a traceId. In this case, the span ID will be null. -For now, Quarkus only has REST events, but we plan to use this ID to link each event to each other as we add more events in the future. +For now, Quarkus only records REST events, but we plan to use this ID to link each event to each other as we add more events in the future. === Event Implementation Policy + When JFR starts recording an event, the event does not record to JFR yet, but when it commits that event, the event is recorded. Therefore, events that have started recording at dump time but have not yet been committed are not dumped. This is unavoidable due to the design of JFR. @@ -200,21 +208,24 @@ Therefore, you will not be aware of prolonged processing. To solve this problem, Quarkus can also record start and end events at the beginning and end of processing. These events are disabled by default. -However, we can enable these events on JFR.(described below) +However, we can enable these events in JFR, as described below. === REST API Event -This event is recorded when either `quarkus-rest` or `resteasy-classic` extension is enabled. + +This event is recorded when either the Quarkus REST or RESTEasy extension is enabled. The following three JFR events are recorded as soon as REST server processing is complete. -- REST -- REST Start -- REST End +REST:: + + Records the time period from the start of the REST server process to the end of the REST server process. -REST Event records the time period from the start of the REST process to the end of the REST server process. +RestStart:: -REST Start Event records the start of the REST server process. + Records the start of the REST server process. -REST End Event records the end of the REST server process. +RestEnd:: + + Records the end of the REST server process. These events have the following information. @@ -239,68 +250,82 @@ We can check if the Resource Method was executed as expected by the HTTP Method Client records information about the accessing client. === Native Image -Native image supports JDK Flight Recorder. -This extension also supports native images. -To enable JFR on Native image, it is usually built with `--enable-monitoring`. -However, we can enable JFR in Quarkus Native images by adding `jfr` to the configuration `quarkus.native.monitoring`. -There are two ways to set up this configuration: by including it in `application.properties` or by specifying it at build time. + +Native executables supports Java Flight Recorder. +This extension also supports native executables. + +To enable JFR on native executables, it is usually built with `--enable-monitoring`. +However, we can enable JFR in Quarkus native executables by adding `jfr` to the configuration property `quarkus.native.monitoring`. +There are two ways to set up this configuration: by including it in the `application.properties` or by specifying it at build time. The first method is to first configure settings in `application.properties`. -application.properties -``` +[source,properties] +---- quarkus.native.monitoring=jfr -``` -Next, simply build as `./mvnw package -Dnative`. +---- + +Then build your native executable as usual: -The second way is to give `-Dquarkus.native.monitoring=jfr` at build time and build as `./mvnw package -Dnative -Dquarkus.native.monitoring=jfr`. +include::{includes}/devtools/build-native.adoc[] -Once we have finished building the Native image, we can run the native application with JFR as follows +The second way is to pass `-Dquarkus.native.monitoring=jfr` at build time: -``` +:build-additional-parameters: -Dquarkus.native.monitoring=jfr +include::{includes}/devtools/build-native.adoc[] +:build-additional-parameters: + +Once you have finished building the native executable, you can run the native application with JFR as follows: + +[source,shell] +---- target/your-application-runner -XX:StartFlightRecording=name=quarkus,dumponexit=true,filename=myrecording.jfr -``` +---- [NOTE] ==== -Note that at this time, GraalVM is not possible to record JFR on Windows native images. +Note that at this time, Mandrel and GraalVM cannot record JFR for Windows native executables. ==== == JFR configuration We can use the JFR CLI to configure the events that JFR will record. -The configuration file, JFC file, is in XML format, so we can modify with a text editor. -However, we should use `jfr configure`, which is included in OpenJDK by default. +The configuration file, JFC file, is in XML format, so we can modify it with a text editor. +However, it is recommended to use `jfr configure`, which is included in OpenJDK by default. + +Here we create a configuration file in which REST Start and REST End events are recorded (they are not recorded by default): -Here we create a configuration file in which RestStart and RestEnd events are recorded, which are not recorded by default. +[source,shell] ---- jfr configure --input default.jfc +quarkus.RestStart#enabled=true +quarkus.RestEnd#enabled=true --output custom-rest.jfc ---- -This creates `custom-rest.jfc` as a configuration file with RestStart and RestEnd enabled. -Now we are ready to run our application with new settings. We launch the application with JFR configured to be enabled from the startup of the Java Virtual Machine. +This creates `custom-rest.jfc` as a configuration file with recording for RestStart and RestEnd enabled. + +Now we are ready to run our application with the new settings. +We launch the application with JFR configured so that it is enabled from the startup of the Java Virtual Machine. :dev-additional-parameters: -Djvm.args="-XX:StartFlightRecording=name=quarkus,settings=./custom-rest.jfc,dumponexit=true,filename=myrecording.jfr" include::{includes}/devtools/dev.adoc[] +:dev-additional-parameters: +== Adding new events -== Developing new events into quarkus-jfr extension. - -This section is for those who would like to add new events with this extension. +This section is for those who would like to add new events to this extension. We recommend that new events be associated with existing events. Associations are useful when looking at the details of a process that is taking a long time. -For example, a general REST application retrieves the data needed for processing from a data store. +For example, a general REST application retrieves the data needed for processing from a datastore. If REST events are not associated with datastore events, it is impossible to know which datastore events were processed in each REST request. When the two events are associated, we know immediately which datastore event was processed in each REST request. [NOTE] ==== -Data store events are not implemented yet. +Datastore events are not implemented yet. ==== -The quarkus-jfr extension provides a Request ID for event association. -See Identifying Requests for more information on Request IDs. +The Quarkus JFR extension provides a Request ID for event association. +See <> for more information on Request IDs. In specific code, the following two steps are required. First, implement `traceId` and `spanId` on the new event as follows @@ -331,7 +356,7 @@ public class NewEvent extends Event { } ---- -Then you get the information to store in them from the `IdProducer` object's `getTraceId()` and `getSpanId()`. +Then you get the information to store in the event from the `IdProducer` object's `getTraceId()` and `getSpanId()`. [source,java] ---- @@ -359,6 +384,6 @@ public class NewInterceptor { } ---- -== quarkus-jfr Configuration Reference +== Configuration Reference include::{generated-dir}/config/quarkus-jfr.adoc[leveloffset=+1, opts=optional] \ No newline at end of file diff --git a/docs/src/main/asciidoc/telemetry-micrometer.adoc b/docs/src/main/asciidoc/telemetry-micrometer.adoc index e88295d9afb9f..c986d7b46c186 100644 --- a/docs/src/main/asciidoc/telemetry-micrometer.adoc +++ b/docs/src/main/asciidoc/telemetry-micrometer.adoc @@ -606,7 +606,7 @@ class, method, and exception (either "none" or the simple class name of a detect Parameters to `@Counted` and `@Timed` can be annotated with `@MeterTag` to dynamically assign meaningful tag values. `MeterTag.resolver` can be used to extract a tag from a method parameter, by creating a bean -implementing `io.micrometer.common.annotation.ValueResolver` and referring to this class: `@MeterTag(resolver=CustomResolver.class) +implementing `io.micrometer.common.annotation.ValueResolver` and referring to this class: `@MeterTag(resolver=CustomResolver.class)` `MeterTag.expression` is also supported, but you have to implement the evaluation of the expression by creating a bean implementing `io.micrometer.common.annotation.ValueExpressionResolver` that can evaluate expressions. diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SplitPackageProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SplitPackageProcessor.java index 998c040af65d4..f68b4256900b6 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SplitPackageProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SplitPackageProcessor.java @@ -39,6 +39,16 @@ public class SplitPackageProcessor { private static final Logger LOGGER = Logger.getLogger(SplitPackageProcessor.class); + private static final Predicate IGNORE_PACKAGE = new Predicate<>() { + + @Override + public boolean test(String packageName) { + // Remove the elements from this list when the original issue is fixed + // so that we can detect further issues. + return packageName.startsWith("io.fabric8.kubernetes"); + } + }; + @BuildStep void splitPackageDetection(ApplicationArchivesBuildItem archivesBuildItem, ArcConfig config, @@ -72,6 +82,9 @@ void splitPackageDetection(ApplicationArchivesBuildItem archivesBuildItem, // - "com.me.app.sub" found in [archiveA, archiveB] StringBuilder splitPackagesWarning = new StringBuilder(); for (String packageName : packageToArchiveMap.keySet()) { + if (IGNORE_PACKAGE.test(packageName)) { + continue; + } // skip packages based on pre-built predicates boolean skipEvaluation = false; diff --git a/extensions/caffeine/runtime/src/main/java/io/quarkus/caffeine/runtime/graal/CacheConstructorsFeature.java b/extensions/caffeine/runtime/src/main/java/io/quarkus/caffeine/runtime/graal/CacheConstructorsFeature.java index ac2bf2db5b498..fede7ebd6e7a8 100644 --- a/extensions/caffeine/runtime/src/main/java/io/quarkus/caffeine/runtime/graal/CacheConstructorsFeature.java +++ b/extensions/caffeine/runtime/src/main/java/io/quarkus/caffeine/runtime/graal/CacheConstructorsFeature.java @@ -77,6 +77,7 @@ public static String[] typesNeedingConstructorsRegistered() { return new String[] { "com.github.benmanes.caffeine.cache.PDMS", "com.github.benmanes.caffeine.cache.PSA", + "com.github.benmanes.caffeine.cache.PSAW", "com.github.benmanes.caffeine.cache.PSMS", "com.github.benmanes.caffeine.cache.PSW", "com.github.benmanes.caffeine.cache.PSMW", @@ -86,6 +87,7 @@ public static String[] typesNeedingConstructorsRegistered() { "com.github.benmanes.caffeine.cache.PSWMW", "com.github.benmanes.caffeine.cache.SILMS", "com.github.benmanes.caffeine.cache.SSA", + "com.github.benmanes.caffeine.cache.SSAW", "com.github.benmanes.caffeine.cache.SSLA", "com.github.benmanes.caffeine.cache.SSLMS", "com.github.benmanes.caffeine.cache.SSMS", @@ -106,7 +108,8 @@ public static String[] typesNeedingConstructorsRegisteredWhenRecordingStats() { "com.github.benmanes.caffeine.cache.SSSMSA", "com.github.benmanes.caffeine.cache.SSSMSW", "com.github.benmanes.caffeine.cache.SSSMSAW", - "com.github.benmanes.caffeine.cache.SSSW" + "com.github.benmanes.caffeine.cache.SSSW", + "com.github.benmanes.caffeine.cache.SSSAW" }; } } diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java index 01f72dc80cec1..44b3a109c820e 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java @@ -98,9 +98,9 @@ public class JibProcessor { private static final String OPENJDK_PREFIX = "openjdk"; private static final String RUNTIME_SUFFIX = "runtime"; - private static final String JAVA_21_BASE_IMAGE = String.format("%s/%s-21-%s:1.18", UBI8_PREFIX, OPENJDK_PREFIX, + private static final String JAVA_21_BASE_IMAGE = String.format("%s/%s-21-%s:1.19", UBI8_PREFIX, OPENJDK_PREFIX, RUNTIME_SUFFIX); - private static final String JAVA_17_BASE_IMAGE = String.format("%s/%s-17-%s:1.18", UBI8_PREFIX, OPENJDK_PREFIX, + private static final String JAVA_17_BASE_IMAGE = String.format("%s/%s-17-%s:1.19", UBI8_PREFIX, OPENJDK_PREFIX, RUNTIME_SUFFIX); // The source for this can be found at https://github.com/jboss-container-images/openjdk/blob/ubi8/modules/run/artifacts/opt/jboss/container/java/run/run-java.sh diff --git a/extensions/jfr/deployment/pom.xml b/extensions/jfr/deployment/pom.xml index d224bcc3c8311..b12e3229d802a 100644 --- a/extensions/jfr/deployment/pom.xml +++ b/extensions/jfr/deployment/pom.xml @@ -9,7 +9,7 @@ 999-SNAPSHOT quarkus-jfr-deployment - Quarkus - Jfr - Deployment + Quarkus - JFR - Deployment io.quarkus diff --git a/extensions/jfr/deployment/src/main/java/io/quarkus/jfr/deployment/JfrProcessor.java b/extensions/jfr/deployment/src/main/java/io/quarkus/jfr/deployment/JfrProcessor.java index 14936fd93c4cb..d5494245b1874 100644 --- a/extensions/jfr/deployment/src/main/java/io/quarkus/jfr/deployment/JfrProcessor.java +++ b/extensions/jfr/deployment/src/main/java/io/quarkus/jfr/deployment/JfrProcessor.java @@ -3,6 +3,7 @@ import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; +import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.*; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.FeatureBuildItem; @@ -21,11 +22,9 @@ @BuildSteps public class JfrProcessor { - private static final String FEATURE = "jfr"; - @BuildStep FeatureBuildItem feature() { - return new FeatureBuildItem(FEATURE); + return new FeatureBuildItem(Feature.JFR); } @BuildStep @@ -51,7 +50,7 @@ void registerRequestIdProducer(Capabilities capabilities, } @BuildStep - void registerReactiveResteasyIntegration(Capabilities capabilities, + void registerRestIntegration(Capabilities capabilities, BuildProducer filterBeans, BuildProducer additionalBeans) { diff --git a/extensions/jfr/pom.xml b/extensions/jfr/pom.xml index 60f24088b364e..ccf6252a37a5d 100644 --- a/extensions/jfr/pom.xml +++ b/extensions/jfr/pom.xml @@ -11,7 +11,7 @@ quarkus-jfr-parent pom - Quarkus - Jfr - Parent + Quarkus - JFR deployment runtime diff --git a/extensions/jfr/runtime/pom.xml b/extensions/jfr/runtime/pom.xml index 78c99cab07e42..efa6681a02da4 100644 --- a/extensions/jfr/runtime/pom.xml +++ b/extensions/jfr/runtime/pom.xml @@ -9,7 +9,8 @@ 999-SNAPSHOT quarkus-jfr - Quarkus - Jfr - Runtime + Quarkus - JFR - Runtime + Monitor your applications with Java Flight Recorder io.quarkus @@ -33,7 +34,6 @@ jakarta.ws.rs jakarta.ws.rs-api - 3.1.0 diff --git a/extensions/jfr/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/jfr/runtime/src/main/resources/META-INF/quarkus-extension.yaml index e56bb4fb031d5..66628d400ec81 100644 --- a/extensions/jfr/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/jfr/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -1,9 +1,11 @@ -name: Jfr -#description: Do something useful. +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Java Flight Recorder (JFR)" metadata: -# keywords: -# - jfr -# guide: ... # To create and publish this guide, see https://github.com/quarkiverse/quarkiverse/wiki#documenting-your-extension -# categories: -# - "miscellaneous" -# status: "preview" + keywords: + - "jfr" + - "java flight recorder" + guide: "https://quarkus.io/guides/jfr" + status: "preview" + categories: + - "observability" \ No newline at end of file diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/VertxRedisClientFactory.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/VertxRedisClientFactory.java index 02f8171f3ad7c..a4ea37284e844 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/VertxRedisClientFactory.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/VertxRedisClientFactory.java @@ -94,7 +94,7 @@ public static Redis create(String name, Vertx vertx, RedisClientConfig config, T config.replicas().ifPresent(options::setUseReplicas); options.setNetClientOptions(toNetClientOptions(config)); - configureTLS(name, config, tlsRegistry, options.getNetClientOptions()); + configureTLS(name, config, tlsRegistry, options.getNetClientOptions(), hosts); options.setPoolName(name); // Use the convention defined by Quarkus Micrometer Vert.x metrics to create metrics prefixed with redis. @@ -180,10 +180,18 @@ public static RedisHostsProvider findProvider(String name) { } private static void configureTLS(String name, RedisClientConfig config, TlsConfigurationRegistry tlsRegistry, - NetClientOptions net) { + NetClientOptions net, List hosts) { TlsConfiguration configuration = null; boolean defaultTrustAll = false; + boolean tlsFromHosts = false; + for (URI uri : hosts) { + if ("rediss".equals(uri.getScheme())) { + tlsFromHosts = true; + break; + } + } + // Check if we have a named TLS configuration or a default configuration: if (config.tlsConfigurationName().isPresent()) { Optional maybeConfiguration = tlsRegistry.get(config.tlsConfigurationName().get()); @@ -200,10 +208,15 @@ private static void configureTLS(String name, RedisClientConfig config, TlsConfi } } + if (configuration != null && !tlsFromHosts) { + LOGGER.warnf("The Redis client %s is configured with a named TLS configuration but the hosts are not " + + "using the `rediss://` scheme - Disabling TLS", name); + } + // Apply the configuration if (configuration != null) { // This part is often the same (or close) for every Vert.x client: - net.setSsl(true); + net.setSsl(tlsFromHosts); if (configuration.getTrustStoreOptions() != null) { net.setTrustOptions(configuration.getTrustStoreOptions()); @@ -244,7 +257,7 @@ private static void configureTLS(String name, RedisClientConfig config, TlsConfi } else { net.setHostnameVerificationAlgorithm(verificationAlgorithm); } - net.setSsl(config.tls().enabled() || defaultTrustAll); + net.setSsl(config.tls().enabled() || tlsFromHosts); net.setTrustAll(config.tls().trustAll() || defaultTrustAll); configurePemTrustOptions(net, config.tls().trustCertificatePem()); diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java index 245f60035d1a2..9de7cb651a3b1 100644 --- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java @@ -40,7 +40,7 @@ import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpCompressionHandler; import io.quarkus.vertx.http.runtime.HttpConfiguration; -import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundHandler; +import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundData; import io.quarkus.vertx.http.runtime.devmode.RouteDescription; import io.quarkus.vertx.http.runtime.devmode.RouteMethodDescription; import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.DefaultAuthFailureHandler; @@ -107,7 +107,7 @@ public Handler vertxRequestHandler(Supplier vertx, Execut if (LaunchMode.current() == LaunchMode.DEVELOPMENT) { // For Not Found Screen Registry registry = deployment.getRegistry(); - ResourceNotFoundHandler.runtimeRoutes = fromBoundResourceInvokers(registry, nonJaxRsClassNameToMethodPaths); + ResourceNotFoundData.setRuntimeRoutes(fromBoundResourceInvokers(registry, nonJaxRsClassNameToMethodPaths)); } return handler; diff --git a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java index 7d0a6f1231ae1..fc52b7a35c0e6 100644 --- a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java +++ b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java @@ -69,7 +69,7 @@ import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; -import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundHandler; +import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundData; import io.quarkus.vertx.http.runtime.devmode.RouteDescription; import io.quarkus.vertx.http.runtime.devmode.RouteMethodDescription; import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.DefaultAuthFailureHandler; @@ -158,7 +158,7 @@ closeTaskHandler, contextFactory, new ArcThreadSetupAction(beanContainer.request if (LaunchMode.current() == LaunchMode.DEVELOPMENT) { // For Not Found Screen - ResourceNotFoundHandler.runtimeRoutes = fromClassMappers(deployment.getClassMappers()); + ResourceNotFoundData.setRuntimeRoutes(fromClassMappers(deployment.getClassMappers())); // For Dev UI Screen RuntimeResourceVisitor.visitRuntimeResources(deployment.getClassMappers(), ScoreSystem.ScoreVisitor); } diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusNotFoundServlet.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusNotFoundServlet.java new file mode 100644 index 0000000000000..b3160e9acf59c --- /dev/null +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusNotFoundServlet.java @@ -0,0 +1,33 @@ +package io.quarkus.undertow.runtime; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import jakarta.enterprise.inject.spi.CDI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundData; +import io.vertx.core.json.Json; + +public class QuarkusNotFoundServlet extends HttpServlet { + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + ResourceNotFoundData resourceNotFoundData = CDI.current().select(ResourceNotFoundData.class).get(); + String accept = req.getHeader("Accept"); + if (accept != null && accept.contains("application/json")) { + resp.setContentType("application/json"); + resp.setCharacterEncoding(StandardCharsets.UTF_8.name()); + resp.getWriter().write(Json.encodePrettily(resourceNotFoundData.getJsonContent())); + } else { + //We default to HTML representation + resp.setContentType("text/html"); + resp.setCharacterEncoding(StandardCharsets.UTF_8.name()); + resp.getWriter().write(resourceNotFoundData.getHTMLContent()); + } + } + +} diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java index a0ccfe2a2d83d..fb00e1a9bd6cb 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java @@ -54,7 +54,7 @@ import io.quarkus.vertx.http.runtime.HttpCompressionHandler; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.quarkus.vertx.http.runtime.VertxHttpRecorder; -import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundHandler; +import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundData; import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.undertow.httpcore.BufferAllocator; import io.undertow.httpcore.StatusCodes; @@ -278,7 +278,7 @@ public void addServletMapping(RuntimeValue info, String name, St if (sv != null) { sv.addMapping(mapping); if (LaunchMode.current() == LaunchMode.DEVELOPMENT) { - ResourceNotFoundHandler.addServlet(mapping); + ResourceNotFoundData.addServlet(mapping); } } } @@ -463,20 +463,27 @@ public DeploymentManager bootServletContainer(RuntimeValue info, if (info.getValue().getExceptionHandler() == null) { //if a 500 error page has not been mapped we change the default to our more modern one, with a UID in the //log. If this is not production we also include the stack trace - boolean alreadyMapped = false; + boolean alreadyMapped500 = false; + boolean alreadyMapped404 = false; for (ErrorPage i : info.getValue().getErrorPages()) { if (i.getErrorCode() != null && i.getErrorCode() == StatusCodes.INTERNAL_SERVER_ERROR) { - alreadyMapped = true; - break; + alreadyMapped500 = true; + } else if (i.getErrorCode() != null && i.getErrorCode() == StatusCodes.NOT_FOUND) { + alreadyMapped404 = true; } } - if (!alreadyMapped || launchMode.isDevOrTest()) { + if (!alreadyMapped500 || launchMode.isDevOrTest()) { info.getValue().setExceptionHandler(new QuarkusExceptionHandler()); info.getValue().addErrorPage(new ErrorPage("/@QuarkusError", StatusCodes.INTERNAL_SERVER_ERROR)); info.getValue().addServlet(new ServletInfo("@QuarkusError", QuarkusErrorServlet.class) .addMapping("/@QuarkusError").setAsyncSupported(true) .addInitParam(QuarkusErrorServlet.SHOW_STACK, Boolean.toString(launchMode.isDevOrTest()))); } + if (!alreadyMapped404 && launchMode.equals(LaunchMode.DEVELOPMENT)) { + info.getValue().addErrorPage(new ErrorPage("/@QuarkusNotFound", StatusCodes.NOT_FOUND)); + info.getValue().addServlet(new ServletInfo("@QuarkusNotFound", QuarkusNotFoundServlet.class) + .addMapping("/@QuarkusNotFound").setAsyncSupported(true)); + } } setupRequestScope(info.getValue(), beanContainer); diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/EndpointsProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/EndpointsProcessor.java index 50ac913d76a67..84b9b46399985 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/EndpointsProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/EndpointsProcessor.java @@ -3,13 +3,16 @@ import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.devui.deployment.InternalPageBuildItem; +import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; import io.quarkus.devui.spi.page.Page; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundData; /** * This creates Endpoints Page */ public class EndpointsProcessor { + private static final String NAMESPACE = "devui-endpoints"; private static final String DEVUI = "dev-ui"; @BuildStep(onlyIf = IsDevelopment.class) @@ -23,17 +26,22 @@ InternalPageBuildItem createEndpointsPage(NonApplicationRootPathBuildItem nonApp // Page endpointsPage.addPage(Page.webComponentPageBuilder() - .namespace("devui-endpoints") + .namespace(NAMESPACE) .title("Endpoints") .icon("font-awesome-solid:plug") .componentLink("qwc-endpoints.js")); endpointsPage.addPage(Page.webComponentPageBuilder() - .namespace("devui-endpoints") + .namespace(NAMESPACE) .title("Routes") .icon("font-awesome-solid:route") .componentLink("qwc-routes.js")); return endpointsPage; } + + @BuildStep(onlyIf = IsDevelopment.class) + JsonRPCProvidersBuildItem createJsonRPCService() { + return new JsonRPCProvidersBuildItem(NAMESPACE, ResourceNotFoundData.class); + } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/NotFoundProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/NotFoundProcessor.java index 4fb738557cb9d..787b570fa123d 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/NotFoundProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/NotFoundProcessor.java @@ -11,6 +11,8 @@ import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; @@ -20,6 +22,7 @@ import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.VertxWebRouterBuildItem; import io.quarkus.vertx.http.runtime.devmode.AdditionalRouteDescription; +import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundData; import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundRecorder; import io.quarkus.vertx.http.runtime.devmode.RouteDescription; import io.vertx.core.Handler; @@ -29,11 +32,19 @@ public class NotFoundProcessor { private static final String META_INF_RESOURCES = "META-INF/resources"; + @BuildStep(onlyIf = IsDevelopment.class) + AdditionalBeanBuildItem resourceNotFoundDataAvailable() { + return AdditionalBeanBuildItem.builder() + .addBeanClass(ResourceNotFoundData.class) + .setUnremovable().build(); + } + @BuildStep(onlyIf = IsDevelopment.class) @Record(RUNTIME_INIT) void routeNotFound(ResourceNotFoundRecorder recorder, VertxWebRouterBuildItem router, HttpRootPathBuildItem httpRoot, + BeanContainerBuildItem beanContainer, LaunchModeBuildItem launchMode, ApplicationArchivesBuildItem applicationArchivesBuildItem, List routeDescriptions, @@ -66,6 +77,7 @@ void routeNotFound(ResourceNotFoundRecorder recorder, router.getHttpRouter(), router.getMainRouter(), router.getManagementRouter(), + beanContainer.getValue(), getBaseUrl(launchMode), httpRoot.getRootPath(), routes, diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-endpoints.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-endpoints.js index efc01c57d7364..efe3b43c30a22 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-endpoints.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-endpoints.js @@ -1,14 +1,15 @@ import { LitElement, html, css} from 'lit'; -import { basepath } from 'devui-data'; import '@vaadin/progress-bar'; import '@vaadin/grid'; import { columnBodyRenderer } from '@vaadin/grid/lit.js'; import '@vaadin/grid/vaadin-grid-sort-column.js'; +import { JsonRpc } from 'jsonrpc'; /** * This component show all available endpoints */ export class QwcEndpoints extends LitElement { + jsonRpc = new JsonRpc(this); static styles = css` .infogrid { @@ -44,25 +45,15 @@ export class QwcEndpoints extends LitElement { this._info = null; } - async connectedCallback() { + connectedCallback() { super.connectedCallback(); - await this.load(); + this.jsonRpc.getJsonContent().then(jsonRpcResponse => { + this._info = jsonRpcResponse.result; + }); } - - async load() { - const response = await fetch("/quarkus404", { - method: 'GET', - headers: { - 'Accept': 'application/json' - } - }); - const data = await response.json(); - this._info = data; - } - + render() { if (this._info) { - const typeTemplates = []; for (const [type, list] of Object.entries(this._info)) { typeTemplates.push(html`${this._renderType(type,list)}`); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java new file mode 100644 index 0000000000000..1469c5c6c8289 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java @@ -0,0 +1,287 @@ +package io.quarkus.vertx.http.runtime.devmode; + +import static io.quarkus.runtime.TemplateHtmlBuilder.adjustRoot; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.logging.Logger; + +import io.quarkus.runtime.TemplateHtmlBuilder; +import io.quarkus.runtime.util.ClassPathUtils; +import io.smallrye.common.annotation.NonBlocking; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +@ApplicationScoped +public class ResourceNotFoundData { + private static final Logger LOG = Logger.getLogger(ResourceNotFoundData.class); + private static volatile List runtimeRoutes = null; + private static volatile List servletMappings = new ArrayList<>(); + private static final String META_INF_RESOURCES = "META-INF/resources"; + + private String baseUrl; + private String httpRoot; + private List endpointRoutes; + private Set staticRoots; + private List additionalEndpoints; + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public void setHttpRoot(String httpRoot) { + this.httpRoot = httpRoot; + } + + public void setEndpointRoutes(List endpointRoutes) { + this.endpointRoutes = endpointRoutes; + } + + public void setStaticRoots(Set staticRoots) { + this.staticRoots = staticRoots; + } + + public void setAdditionalEndpoints(List additionalEndpoints) { + this.additionalEndpoints = additionalEndpoints; + } + + public static void addServlet(String mapping) { + servletMappings.add(mapping); + } + + public static void setRuntimeRoutes(List routeDescriptions) { + runtimeRoutes = routeDescriptions; + } + + public String getHTMLContent() { + + List combinedRoutes = getCombinedRoutes(); + TemplateHtmlBuilder builder = new TemplateHtmlBuilder(this.baseUrl, + "404 - Resource Not Found", "", "Resources overview"); + + builder.resourcesStart(RESOURCE_ENDPOINTS); + + for (RouteDescription resource : combinedRoutes) { + builder.resourcePath(adjustRoot(this.httpRoot, resource.getBasePath())); + for (RouteMethodDescription method : resource.getCalls()) { + builder.method(method.getHttpMethod(), + adjustRoot(this.httpRoot, method.getFullPath())); + if (method.getJavaMethod() != null) { + builder.listItem(method.getJavaMethod()); + } + if (method.getConsumes() != null) { + builder.consumes(method.getConsumes()); + } + if (method.getProduces() != null) { + builder.produces(method.getProduces()); + } + builder.methodEnd(); + } + builder.resourceEnd(); + } + if (combinedRoutes.isEmpty()) { + builder.noResourcesFound(); + } + builder.resourcesEnd(); + + if (!servletMappings.isEmpty()) { + builder.resourcesStart(SERVLET_MAPPINGS); + for (String servletMapping : servletMappings) { + builder.servletMapping(adjustRoot(this.httpRoot, servletMapping)); + } + builder.resourcesEnd(); + } + + // Static Resources + if (!this.staticRoots.isEmpty()) { + List resources = findRealResources(); + if (!resources.isEmpty()) { + builder.resourcesStart(STATIC_RESOURCES); + for (String staticResource : resources) { + builder.staticResourcePath(adjustRoot(this.httpRoot, staticResource)); + } + builder.resourcesEnd(); + } + } + + // Additional Endpoints + if (!this.additionalEndpoints.isEmpty()) { + builder.resourcesStart(ADDITIONAL_ENDPOINTS); + for (AdditionalRouteDescription additionalEndpoint : this.additionalEndpoints) { + builder.staticResourcePath(additionalEndpoint.getUri(), additionalEndpoint.getDescription()); + } + builder.resourcesEnd(); + } + + return builder.toString(); + } + + @NonBlocking + public JsonObject getJsonContent() { + List combinedRoutes = getCombinedRoutes(); + JsonObject infoMap = new JsonObject(); + + // REST Endpoints + if (!combinedRoutes.isEmpty()) { + JsonArray r = new JsonArray(); + for (RouteDescription resource : combinedRoutes) { + String path = adjustRoot(this.httpRoot, resource.getBasePath()); + + for (RouteMethodDescription method : resource.getCalls()) { + String description = method.getHttpMethod(); + if (method.getConsumes() != null) { + description = description + " (consumes: " + method.getConsumes() + ")"; + } + if (method.getProduces() != null) { + description = description + " (produces:" + method.getProduces() + ")"; + } + if (method.getJavaMethod() != null) { + description = description + " (java:" + method.getJavaMethod() + ")"; + } + r.add(JsonObject.of(URI, adjustRoot(this.httpRoot, method.getFullPath()), + DESCRIPTION, description)); + } + } + infoMap.put(RESOURCE_ENDPOINTS, r); + } + + // Servlets + if (!servletMappings.isEmpty()) { + JsonArray sm = new JsonArray(); + for (String servletMapping : servletMappings) { + sm.add(JsonObject.of(URI, adjustRoot(this.httpRoot, servletMapping), DESCRIPTION, + EMPTY)); + } + infoMap.put(SERVLET_MAPPINGS, sm); + } + + // Static Resources + if (!this.staticRoots.isEmpty()) { + List resources = findRealResources(); + if (!resources.isEmpty()) { + JsonArray sr = new JsonArray(); + for (String staticResource : resources) { + sr.add(JsonObject.of(URI, adjustRoot(this.httpRoot, staticResource), DESCRIPTION, + EMPTY)); + } + infoMap.put(STATIC_RESOURCES, sr); + } + } + + // Additional Endpoints + if (!this.additionalEndpoints.isEmpty()) { + JsonArray ae = new JsonArray(); + for (AdditionalRouteDescription additionalEndpoint : this.additionalEndpoints) { + ae.add(JsonObject.of(URI, additionalEndpoint.getUri(), DESCRIPTION, additionalEndpoint.getDescription())); + } + infoMap.put(ADDITIONAL_ENDPOINTS, ae); + } + + return infoMap; + + } + + private List getCombinedRoutes() { + // Endpoints + List combinedRoutes = new ArrayList<>(); + if (this.runtimeRoutes != null) { + combinedRoutes.addAll(this.runtimeRoutes); + } + if (endpointRoutes != null) { + combinedRoutes.addAll(this.endpointRoutes); + } + return combinedRoutes; + } + + private List findRealResources() { + + //we need to check for web resources in order to get welcome files to work + //this kinda sucks + Set knownFiles = new HashSet<>(); + for (String staticResourceRoot : this.staticRoots) { + if (staticResourceRoot != null) { + Path resource = Paths.get(staticResourceRoot); + if (Files.exists(resource)) { + try (Stream fileTreeElements = Files.walk(resource)) { + fileTreeElements.forEach(new Consumer() { + @Override + public void accept(java.nio.file.Path path) { + // Skip META-INF/resources entry + if (resource.equals(path)) { + return; + } + java.nio.file.Path rel = resource.relativize(path); + if (!Files.isDirectory(path)) { + knownFiles.add("/" + rel.toString()); + } + } + }); + } catch (IOException e) { + LOG.error("Failed to read static resources", e); + } + } + } + } + try { + ClassPathUtils.consumeAsPaths(META_INF_RESOURCES, p -> { + collectKnownPaths(p, knownFiles); + }); + } catch (IOException e) { + LOG.error("Failed to read static resources", e); + } + + //limit to 1000 to not have to many files to display + return knownFiles.stream().filter(this::isHtmlFileName).limit(1000).distinct().sorted(Comparator.naturalOrder()) + .collect(Collectors.toList()); + } + + private void collectKnownPaths(java.nio.file.Path resource, Set knownPaths) { + try { + Files.walkFileTree(resource, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(java.nio.file.Path p, BasicFileAttributes attrs) + throws IOException { + String file = resource.relativize(p).toString(); + // Windows has a backslash + file = file.replace('\\', '/'); + if (!file.startsWith("_static/") && !file.startsWith("webjars/")) { + knownPaths.add("/" + file); + } + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private boolean isHtmlFileName(String fileName) { + return fileName.endsWith(".html") || fileName.endsWith(".htm") || fileName.endsWith(".xhtml"); + } + + private static final String RESOURCE_ENDPOINTS = "Resource Endpoints"; + private static final String SERVLET_MAPPINGS = "Servlet mappings"; + private static final String STATIC_RESOURCES = "Static resources"; + private static final String ADDITIONAL_ENDPOINTS = "Additional endpoints"; + private static final String URI = "uri"; + private static final String DESCRIPTION = "description"; + private static final String EMPTY = ""; + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundHandler.java index 46fec0b2f11c8..8dc6e6cf029e1 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundHandler.java @@ -1,291 +1,43 @@ package io.quarkus.vertx.http.runtime.devmode; -import static io.quarkus.runtime.TemplateHtmlBuilder.adjustRoot; +import jakarta.enterprise.inject.spi.CDI; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.jboss.logging.Logger; - -import io.quarkus.runtime.TemplateHtmlBuilder; -import io.quarkus.runtime.util.ClassPathUtils; import io.vertx.core.Handler; import io.vertx.core.json.Json; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; /** * Lists all routes when no route matches the path in the dev mode. */ public class ResourceNotFoundHandler implements Handler { - private static final Logger LOG = Logger.getLogger(ResourceNotFoundHandler.class); - protected static final String META_INF_RESOURCES = "META-INF/resources"; - - public static volatile List runtimeRoutes; - private static volatile List servletMappings = new ArrayList<>(); + private final ResourceNotFoundData resourceNotFoundData; - private final String baseUrl; - private final String httpRoot; - private final List routes; - private final Set staticResourceRoots; - private final List additionalEndpoints; - - public ResourceNotFoundHandler(String baseUrl, - String httpRoot, - List routes, - Set staticResourceRoots, - List additionalEndpoints) { - this.baseUrl = baseUrl; - this.httpRoot = httpRoot; - this.routes = routes; - this.staticResourceRoots = staticResourceRoots; - this.additionalEndpoints = additionalEndpoints; + public ResourceNotFoundHandler() { + this.resourceNotFoundData = CDI.current().select(ResourceNotFoundData.class).get(); } @Override public void handle(RoutingContext routingContext) { - // Endpoints - List combinedRoutes = new ArrayList<>(); - if (runtimeRoutes != null) { - combinedRoutes.addAll(runtimeRoutes); - } - if (routes != null) { - combinedRoutes.addAll(routes); - } - String header = routingContext.request().getHeader("Accept"); if (header != null && header.startsWith("application/json")) { - handleJson(routingContext, combinedRoutes); + handleJson(routingContext); } else { - handleHTML(routingContext, combinedRoutes); + handleHTML(routingContext); } } - private void handleJson(RoutingContext routingContext, List combinedRoutes) { + private void handleJson(RoutingContext routingContext) { routingContext.response() .setStatusCode(404) .putHeader("content-type", "application/json; charset=utf-8") - .end(Json.encodePrettily(getJsonContent(combinedRoutes))); + .end(Json.encodePrettily(resourceNotFoundData.getJsonContent())); } - private void handleHTML(RoutingContext routingContext, List combinedRoutes) { + private void handleHTML(RoutingContext routingContext) { routingContext.response() .setStatusCode(404) .putHeader("content-type", "text/html; charset=utf-8") - .end(getHTMLContent(combinedRoutes)); - } - - private List findRealResources() { - - //we need to check for web resources in order to get welcome files to work - //this kinda sucks - Set knownFiles = new HashSet<>(); - for (String staticResourceRoot : staticResourceRoots) { - if (staticResourceRoot != null) { - Path resource = Paths.get(staticResourceRoot); - if (Files.exists(resource)) { - try (Stream fileTreeElements = Files.walk(resource)) { - fileTreeElements.forEach(new Consumer() { - @Override - public void accept(java.nio.file.Path path) { - // Skip META-INF/resources entry - if (resource.equals(path)) { - return; - } - java.nio.file.Path rel = resource.relativize(path); - if (!Files.isDirectory(path)) { - knownFiles.add("/" + rel.toString()); - } - } - }); - } catch (IOException e) { - LOG.error("Failed to read static resources", e); - } - } - } - } - try { - ClassPathUtils.consumeAsPaths(META_INF_RESOURCES, p -> { - collectKnownPaths(p, knownFiles); - }); - } catch (IOException e) { - LOG.error("Failed to read static resources", e); - } - - //limit to 1000 to not have to many files to display - return knownFiles.stream().filter(this::isHtmlFileName).limit(1000).distinct().sorted(Comparator.naturalOrder()) - .collect(Collectors.toList()); - } - - private void collectKnownPaths(java.nio.file.Path resource, Set knownPaths) { - try { - Files.walkFileTree(resource, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(java.nio.file.Path p, BasicFileAttributes attrs) - throws IOException { - String file = resource.relativize(p).toString(); - // Windows has a backslash - file = file.replace('\\', '/'); - if (!file.startsWith("_static/") && !file.startsWith("webjars/")) { - knownPaths.add("/" + file); - } - return FileVisitResult.CONTINUE; - } - }); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - private boolean isHtmlFileName(String fileName) { - return fileName.endsWith(".html") || fileName.endsWith(".htm") || fileName.endsWith(".xhtml"); - } - - public static void addServlet(String mapping) { - servletMappings.add(mapping); - } - - private String getHTMLContent(List combinedRoutes) { - TemplateHtmlBuilder builder = new TemplateHtmlBuilder(baseUrl, "404 - Resource Not Found", "", "Resources overview"); - - builder.resourcesStart(RESOURCE_ENDPOINTS); - - for (RouteDescription resource : combinedRoutes) { - builder.resourcePath(adjustRoot(httpRoot, resource.getBasePath())); - for (RouteMethodDescription method : resource.getCalls()) { - builder.method(method.getHttpMethod(), adjustRoot(httpRoot, method.getFullPath())); - if (method.getJavaMethod() != null) { - builder.listItem(method.getJavaMethod()); - } - if (method.getConsumes() != null) { - builder.consumes(method.getConsumes()); - } - if (method.getProduces() != null) { - builder.produces(method.getProduces()); - } - builder.methodEnd(); - } - builder.resourceEnd(); - } - if (combinedRoutes.isEmpty()) { - builder.noResourcesFound(); - } - builder.resourcesEnd(); - - if (!servletMappings.isEmpty()) { - builder.resourcesStart(SERVLET_MAPPINGS); - for (String servletMapping : servletMappings) { - builder.servletMapping(adjustRoot(httpRoot, servletMapping)); - } - builder.resourcesEnd(); - } - - // Static Resources - if (!staticResourceRoots.isEmpty()) { - List resources = findRealResources(); - if (!resources.isEmpty()) { - builder.resourcesStart(STATIC_RESOURCES); - for (String staticResource : resources) { - builder.staticResourcePath(adjustRoot(httpRoot, staticResource)); - } - builder.resourcesEnd(); - } - } - - // Additional Endpoints - if (!additionalEndpoints.isEmpty()) { - builder.resourcesStart(ADDITIONAL_ENDPOINTS); - for (AdditionalRouteDescription additionalEndpoint : additionalEndpoints) { - builder.staticResourcePath(additionalEndpoint.getUri(), additionalEndpoint.getDescription()); - } - builder.resourcesEnd(); - } - - return builder.toString(); - } - - private JsonObject getJsonContent(List combinedRoutes) { - - JsonObject infoMap = new JsonObject(); - - // REST Endpoints - if (!combinedRoutes.isEmpty()) { - JsonArray r = new JsonArray(); - for (RouteDescription resource : combinedRoutes) { - String path = adjustRoot(httpRoot, resource.getBasePath()); - - for (RouteMethodDescription method : resource.getCalls()) { - String description = method.getHttpMethod(); - if (method.getConsumes() != null) { - description = description + " (consumes: " + method.getConsumes() + ")"; - } - if (method.getProduces() != null) { - description = description + " (produces:" + method.getProduces() + ")"; - } - if (method.getJavaMethod() != null) { - description = description + " (java:" + method.getJavaMethod() + ")"; - } - r.add(JsonObject.of(URI, adjustRoot(httpRoot, method.getFullPath()), DESCRIPTION, description)); - } - } - infoMap.put(RESOURCE_ENDPOINTS, r); - } - - // Servlets - if (!servletMappings.isEmpty()) { - JsonArray sm = new JsonArray(); - for (String servletMapping : servletMappings) { - sm.add(JsonObject.of(URI, adjustRoot(httpRoot, servletMapping), DESCRIPTION, EMPTY)); - } - infoMap.put(SERVLET_MAPPINGS, sm); - } - - // Static Resources - if (!staticResourceRoots.isEmpty()) { - List resources = findRealResources(); - if (!resources.isEmpty()) { - JsonArray sr = new JsonArray(); - for (String staticResource : resources) { - sr.add(JsonObject.of(URI, adjustRoot(httpRoot, staticResource), DESCRIPTION, EMPTY)); - } - infoMap.put(STATIC_RESOURCES, sr); - } - } - - // Additional Endpoints - if (!additionalEndpoints.isEmpty()) { - JsonArray ae = new JsonArray(); - for (AdditionalRouteDescription additionalEndpoint : additionalEndpoints) { - ae.add(JsonObject.of(URI, additionalEndpoint.getUri(), DESCRIPTION, additionalEndpoint.getDescription())); - } - infoMap.put(ADDITIONAL_ENDPOINTS, ae); - } - - return infoMap; - + .end(resourceNotFoundData.getHTMLContent()); } - - private static final String RESOURCE_ENDPOINTS = "Resource Endpoints"; - private static final String SERVLET_MAPPINGS = "Servlet mappings"; - private static final String STATIC_RESOURCES = "Static resources"; - private static final String ADDITIONAL_ENDPOINTS = "Additional endpoints"; - private static final String URI = "uri"; - private static final String DESCRIPTION = "description"; - private static final String EMPTY = ""; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundRecorder.java index 6e848021a7f5f..ef5b60f70e65f 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundRecorder.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Set; +import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import io.vertx.core.Handler; @@ -15,14 +16,21 @@ public class ResourceNotFoundRecorder { public Handler registerNotFoundHandler(RuntimeValue httpRouter, RuntimeValue mainRouter, RuntimeValue managementRouter, + BeanContainer beanContainer, String baseUrl, String httpRoot, List endpointRoutes, Set staticRoots, List additionalEndpoints) { - ResourceNotFoundHandler rbfh = new ResourceNotFoundHandler(baseUrl, httpRoot, endpointRoutes, staticRoots, - additionalEndpoints); + ResourceNotFoundData resourceNotFoundData = beanContainer.beanInstance(ResourceNotFoundData.class); + resourceNotFoundData.setBaseUrl(baseUrl); + resourceNotFoundData.setHttpRoot(httpRoot); + resourceNotFoundData.setEndpointRoutes(endpointRoutes); + resourceNotFoundData.setStaticRoots(staticRoots); + resourceNotFoundData.setAdditionalEndpoints(additionalEndpoints); + + ResourceNotFoundHandler rbfh = new ResourceNotFoundHandler(); addErrorHandler(mainRouter, rbfh); addErrorHandler(httpRouter, rbfh); diff --git a/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java b/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java index c367691098caa..4ed22a9985603 100644 --- a/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java +++ b/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java @@ -1065,7 +1065,8 @@ private static ResultHandle uniOnFailureDoOnError(ResultHandle endpointThis, Byt private static ResultHandle encodeMessage(ResultHandle endpointThis, BytecodeCreator method, Callback callback, GlobalErrorHandlersBuildItem globalErrorHandlers, WebSocketEndpointBuildItem endpoint, ResultHandle value) { - if (callback.acceptsBinaryMessage()) { + if (callback.acceptsBinaryMessage() + || isOnOpenWithBinaryReturnType(callback)) { // ---------------------- // === Binary message === // ---------------------- @@ -1119,7 +1120,7 @@ private static ResultHandle encodeMessage(ResultHandle endpointThis, BytecodeCre value, fun.getInstance()); } else { - // return sendBinary(buffer,broadcast); + // return sendBinary(encodeBuffer(b),broadcast); ResultHandle buffer = encodeBuffer(method, callback.returnType(), value, endpointThis, callback); return method.invokeVirtualMethod(MethodDescriptor.ofMethod(WebSocketEndpointBase.class, "sendBinary", Uni.class, Buffer.class, boolean.class), endpointThis, buffer, @@ -1407,4 +1408,16 @@ static boolean isByteArray(Type type) { static String methodToString(MethodInfo method) { return method.declaringClass().name() + "#" + method.name() + "()"; } + + private static boolean isOnOpenWithBinaryReturnType(Callback callback) { + if (callback.isOnOpen()) { + Type returnType = callback.returnType(); + if (callback.isReturnTypeUni() || callback.isReturnTypeMulti()) { + returnType = callback.returnType().asParameterizedType().arguments().get(0); + } + return WebSocketDotNames.BUFFER.equals(returnType.name()) + || (returnType.kind() == Kind.ARRAY && PrimitiveType.BYTE.equals(returnType.asArrayType().constituent())); + } + return false; + } } diff --git a/extensions/websockets-next/deployment/src/main/resources/dev-ui/qwc-wsn-endpoints.js b/extensions/websockets-next/deployment/src/main/resources/dev-ui/qwc-wsn-endpoints.js index 1b6d808339c9f..b18e57b0665d1 100644 --- a/extensions/websockets-next/deployment/src/main/resources/dev-ui/qwc-wsn-endpoints.js +++ b/extensions/websockets-next/deployment/src/main/resources/dev-ui/qwc-wsn-endpoints.js @@ -32,7 +32,6 @@ export class QwcWebSocketNextEndpoints extends LitElement { cursor: pointer; } .top-bar { - display: flex; align-items: baseline; gap: 20px; padding-left: 20px; @@ -62,7 +61,8 @@ export class QwcWebSocketNextEndpoints extends LitElement { _selectedEndpoint: {state: true}, _selectedConnection: {state: true}, _endpointsAndConnections: {state: true}, - _textMessages: {state: true} + _textMessages: {state: true}, + _connectionMessagesLimit: {state: false} }; constructor() { @@ -83,6 +83,7 @@ export class QwcWebSocketNextEndpoints extends LitElement { e.connections = jsonResponse.result[e.generatedClazz]; return e; }); + this._connectionMessagesLimit = jsonResponse.result.connectionMessageLimit; }) .then(() => { this._conntectionStatusStream = this.jsonRpc.connectionStatus().onNext(jsonResponse => { @@ -219,7 +220,7 @@ export class QwcWebSocketNextEndpoints extends LitElement { Back -

${this._selectedEndpoint.clazz} · Open Connections

+

Open connections for endpoint: ${this._selectedEndpoint.clazz}

`; } @@ -234,11 +235,15 @@ export class QwcWebSocketNextEndpoints extends LitElement { Close connection + + Connection messages limit: ${this._connectionMessagesLimit} + Clear messages -

${this._selectedEndpoint.clazz} · Dev UI Connection · ${this._selectedConnection.handshakePath}

+

Connection: ${this._selectedConnection.id}

+

Endpoint: ${this._selectedEndpoint.clazz}  |  Handshake path: ${this._selectedConnection.handshakePath}

`; } @@ -286,7 +291,7 @@ export class QwcWebSocketNextEndpoints extends LitElement { _renderClazz(endpoint) { return html` - ${endpoint.clazz} + ${endpoint.clazz} `; } diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/onopenreturntypes/OnOpenReturnTypesTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/onopenreturntypes/OnOpenReturnTypesTest.java new file mode 100644 index 0000000000000..aa5ab58469ab9 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/onopenreturntypes/OnOpenReturnTypesTest.java @@ -0,0 +1,71 @@ +package io.quarkus.websockets.next.test.onopenreturntypes; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.WebSocketConnection; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.quarkus.websockets.next.test.utils.WSClient.ReceiverMode; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; + +public class OnOpenReturnTypesTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(EndpointText.class, EndpointBinary.class, WSClient.class); + }); + + @Inject + Vertx vertx; + + @TestHTTPResource("end-text") + URI endText; + + @TestHTTPResource("end-binary") + URI endBinary; + + @Test + void testReturnTypes() throws Exception { + try (WSClient textClient = WSClient.create(vertx, ReceiverMode.TEXT).connect(endText)) { + textClient.waitForMessages(1); + assertEquals("/end-text", textClient.getMessages().get(0).toString()); + } + try (WSClient binaryClient = WSClient.create(vertx, ReceiverMode.BINARY).connect(endBinary)) { + binaryClient.waitForMessages(1); + assertEquals("/end-binary", binaryClient.getMessages().get(0).toString()); + } + } + + @WebSocket(path = "/end-text") + public static class EndpointText { + + @OnOpen + String open(WebSocketConnection connection) { + return connection.handshakeRequest().path(); + } + + } + + @WebSocket(path = "/end-binary") + public static class EndpointBinary { + + @OnOpen + Buffer open(WebSocketConnection connection) { + return Buffer.buffer(connection.handshakeRequest().path()); + } + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java index 955eb9c1b315c..2f1974089db90 100644 --- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java @@ -17,16 +17,26 @@ public class WSClient implements AutoCloseable { + public static WSClient create(Vertx vertx) { + return new WSClient(vertx); + } + + public static WSClient create(Vertx vertx, ReceiverMode mode) { + return new WSClient(vertx, mode); + } + private final WebSocketClient client; private AtomicReference socket = new AtomicReference<>(); private List messages = new CopyOnWriteArrayList<>(); + private final ReceiverMode mode; - public WSClient(Vertx vertx) { + public WSClient(Vertx vertx, ReceiverMode mode) { this.client = vertx.createWebSocketClient(); + this.mode = mode; } - public static WSClient create(Vertx vertx) { - return new WSClient(vertx); + public WSClient(Vertx vertx) { + this(vertx, ReceiverMode.ALL); } public static URI toWS(URI uri, String path) { @@ -52,7 +62,19 @@ public WSClient connect(WebSocketConnectOptions options, URI url) { uri.append("?").append(url.getQuery()); } ClientWebSocket webSocket = client.webSocket(); - webSocket.handler(b -> messages.add(b)); + switch (mode) { + case ALL: + webSocket.handler(b -> messages.add(b)); + break; + case BINARY: + webSocket.binaryMessageHandler(b -> messages.add(b)); + break; + case TEXT: + webSocket.textMessageHandler(b -> messages.add(Buffer.buffer(b))); + break; + default: + throw new IllegalStateException(); + } await(webSocket.connect(options.setPort(url.getPort()).setHost(url.getHost()).setURI(uri.toString()))); var prev = socket.getAndSet(webSocket); if (prev != null) { @@ -135,4 +157,10 @@ public void close() { disconnect(); } + public enum ReceiverMode { + BINARY, + TEXT, + ALL + } + } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java index 3d4b71a427dd0..650067a60aa41 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java @@ -59,6 +59,11 @@ public interface WebSocketsServerRuntimeConfig { */ Security security(); + /** + * Dev mode configuration. + */ + DevMode devMode(); + /** * Traffic logging config. */ @@ -75,4 +80,15 @@ interface Security { } + interface DevMode { + + /** + * The limit of messages kept for a Dev UI connection. If less than zero then no messages are stored and sent to the Dev + * UI view. + */ + @WithDefault("1000") + long connectionMessagesLimit(); + + } + } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/devui/WebSocketNextJsonRPCService.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/devui/WebSocketNextJsonRPCService.java index 810da7a9568fa..5df2e1d395b28 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/devui/WebSocketNextJsonRPCService.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/devui/WebSocketNextJsonRPCService.java @@ -4,7 +4,6 @@ import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.ListIterator; @@ -18,6 +17,7 @@ import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.quarkus.websockets.next.WebSocketConnection; +import io.quarkus.websockets.next.WebSocketsServerRuntimeConfig; import io.quarkus.websockets.next.runtime.ConnectionManager; import io.quarkus.websockets.next.runtime.ConnectionManager.ConnectionListener; import io.smallrye.mutiny.Multi; @@ -51,12 +51,16 @@ public class WebSocketNextJsonRPCService implements ConnectionListener { private final HttpConfiguration httpConfig; - WebSocketNextJsonRPCService(ConnectionManager connectionManager, Vertx vertx, HttpConfiguration httpConfig) { + private final WebSocketsServerRuntimeConfig.DevMode devModeConfig; + + WebSocketNextJsonRPCService(ConnectionManager connectionManager, Vertx vertx, HttpConfiguration httpConfig, + WebSocketsServerRuntimeConfig config) { this.connectionStatus = BroadcastProcessor.create(); this.connectionMessages = BroadcastProcessor.create(); this.connectionManager = connectionManager; this.vertx = vertx; this.httpConfig = httpConfig; + this.devModeConfig = config.devMode(); this.sockets = new ConcurrentHashMap<>(); connectionManager.addListener(this); } @@ -80,6 +84,7 @@ public JsonObject getConnections(List endpoints) { } json.put(endpoint, array); } + json.put("connectionMessagesLimit", devModeConfig.connectionMessagesLimit()); return json; } @@ -87,8 +92,9 @@ public JsonArray getMessages(String connectionKey) { DevWebSocket socket = sockets.get(connectionKey); if (socket != null) { JsonArray ret = new JsonArray(); - synchronized (socket.messages) { - for (ListIterator it = socket.messages.listIterator(socket.messages.size()); it.hasPrevious();) { + List messages = socket.messages; + synchronized (messages) { + for (ListIterator it = messages.listIterator(messages.size()); it.hasPrevious();) { ret.add(it.previous().toJsonObject()); } } @@ -112,13 +118,18 @@ public Uni openDevConnection(String path, String endpointPath) { .addHeader(DEVUI_SOCKET_KEY_HEADER, connectionKey))); return uni.onItem().transform(s -> { LOG.debugf("Opened Dev UI connection with key %s to %s", connectionKey, path); - List messages = Collections.synchronizedList(new ArrayList<>()); + List messages = new ArrayList<>(); s.textMessageHandler(m -> { - TextMessage t = new TextMessage(true, m, LocalDateTime.now()); - messages.add(t); - connectionMessages - .onNext(t.toJsonObject() - .put("key", connectionKey)); + synchronized (messages) { + if (messages.size() < devModeConfig.connectionMessagesLimit()) { + TextMessage t = new TextMessage(true, m, LocalDateTime.now()); + messages.add(t); + connectionMessages.onNext(t.toJsonObject().put("key", connectionKey)); + } else { + LOG.debugf("Opened Dev UI connection [%s] received a message but the limit [%s] has been reached", + connectionKey, devModeConfig.connectionMessagesLimit()); + } + } }); sockets.put(connectionKey, new DevWebSocket(s, messages)); return new JsonObject().put("success", true).put("key", connectionKey); @@ -187,12 +198,18 @@ public Uni sendTextMessage(String connectionKey, String message) { if (socket != null) { Uni uni = UniHelper.toUni(socket.socket.writeTextMessage(message)); return uni.onItem().transform(v -> { - LOG.debugf("Sent text message to connection with key %s", connectionKey); - TextMessage t = new TextMessage(false, message, LocalDateTime.now()); - socket.messages.add(t); - connectionMessages - .onNext(t.toJsonObject() - .put("key", connectionKey)); + List messages = socket.messages; + synchronized (messages) { + if (messages.size() < devModeConfig.connectionMessagesLimit()) { + TextMessage t = new TextMessage(false, message, LocalDateTime.now()); + messages.add(t); + connectionMessages.onNext(t.toJsonObject().put("key", connectionKey)); + LOG.debugf("Sent text message to connection with key %s", connectionKey); + } else { + LOG.debugf("Sent text message to connection [%s] but the limit [%s] has been reached", + connectionKey, devModeConfig.connectionMessagesLimit()); + } + } return new JsonObject().put("success", true); }).onFailure().recoverWithItem(t -> { LOG.errorf(t, "Unable to send text message to connection with key %s", connectionKey); @@ -205,7 +222,7 @@ public Uni sendTextMessage(String connectionKey, String message) { public JsonObject clearMessages(String connectionKey) { DevWebSocket socket = sockets.get(connectionKey); if (socket != null) { - socket.messages.clear(); + socket.clearMessages(); return new JsonObject().put("success", true); } return new JsonObject().put("success", false); @@ -240,6 +257,12 @@ JsonObject toJsonObject(String endpoint, WebSocketConnection c) { } record DevWebSocket(WebSocket socket, List messages) { + + void clearMessages() { + synchronized (messages) { + messages.clear(); + } + } } record TextMessage(boolean incoming, String text, LocalDateTime timestamp) { @@ -252,6 +275,7 @@ JsonObject toJsonObject() { .put("className", incoming ? "incoming" : "outgoing") .put("userAbbr", incoming ? "IN" : "OUT"); } + } } diff --git a/extensions/websockets-next/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/websockets-next/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 3efa575b24631..b04f00776f141 100644 --- a/extensions/websockets-next/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/websockets-next/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -10,6 +10,7 @@ metadata: - "http" categories: - "web" + guide: https://quarkus.io/guides/websockets-next-reference status: "experimental" config: - "quarkus.websockets.next" \ No newline at end of file diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java index d0d87e32b1d79..b5eb22584600c 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java @@ -680,6 +680,12 @@ static boolean hasQualifier(BeanDeployment beanDeployment, AnnotationInstance re //as this is called in a tight loop we only do it if necessary values = new ArrayList<>(); Set nonBindingFields = beanDeployment.getQualifierNonbindingMembers(requiredQualifier.name()); + if (requiredClazz == null) { + throw new IllegalStateException("Failed to find bean qualifier class with name " + + requiredQualifier.name() + " in application index. Make sure the class is part of " + + "the Jandex index. Classes that are not subject to discovery can be registered via " + + "AdditionalBeanBuildItem and non-qualifier annotations can use QualifierRegistrarBuildItem"); + } for (AnnotationValue val : requiredQualifier.valuesWithDefaults(beanDeployment.getBeanArchiveIndex())) { if (!requiredClazz.method(val.name()).hasAnnotation(DotNames.NONBINDING) && !nonBindingFields.contains(val.name())) { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/InsertSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/InsertSectionHelper.java index b018033f111ea..73b79ffd3e98e 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/InsertSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/InsertSectionHelper.java @@ -15,12 +15,38 @@ public InsertSectionHelper(String name, SectionBlock defaultBlock) { @Override public CompletionStage resolve(SectionResolutionContext context) { - SectionBlock extending = context.resolutionContext().getExtendingBlock(name); + // Note that {#insert} is evaluated on the current resolution context + // Therefore, we need to try to find the "correct" parent context to avoid stack + // overflow errors when using the same block names + ResolutionContext rc = findParentResolutionContext(context.resolutionContext()); + if (rc == null) { + // No parent context found - use the current + rc = context.resolutionContext(); + } + SectionBlock extending = rc.getExtendingBlock(name); if (extending != null) { - return context.execute(extending, context.resolutionContext()); + return context.execute(extending, rc); } else { - return context.execute(defaultBlock, context.resolutionContext()); + return context.execute(defaultBlock, rc); + } + } + + private ResolutionContext findParentResolutionContext(ResolutionContext context) { + if (context.getParent() == null) { + return null; } + // Let's iterate over all extending blocks and try to find the "correct" parent context + // The "correct" parent context is the parent of a context that contains this helper + // instance in any of its extending block + SectionBlock block = context.getCurrentExtendingBlock(name); + if (block != null && block.findNode(this::containsThisHelperInstance) != null) { + return context.getParent(); + } + return findParentResolutionContext(context.getParent()); + } + + private boolean containsThisHelperInstance(TemplateNode node) { + return node.isSection() && ((SectionNode) node).helper == this; } public static class Factory implements SectionHelperFactory { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContext.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContext.java index ea293a2ca9ad8..7fd6d0a2d5ed3 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContext.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContext.java @@ -4,7 +4,7 @@ import java.util.concurrent.CompletionStage; /** - * + * The resolution context holds the current context object. */ public interface ResolutionContext { @@ -46,12 +46,21 @@ public interface ResolutionContext { ResolutionContext getParent(); /** + * If no extending block exists for the given name then the parent context (if present) is queried. * * @param name - * @return the extending block for the specified name or null + * @return the extending block for the specified name or {@code null} */ SectionBlock getExtendingBlock(String name); + /** + * Unlike {@link #getExtendingBlock(String)} this method never queries the parent context. + * + * @param name + * @return the extending block for the specified name or {@code null} + */ + SectionBlock getCurrentExtendingBlock(String name); + /** * * @param key diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java index b9eb6e794ecaf..d8714b8cda827 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java @@ -52,6 +52,11 @@ public SectionBlock getExtendingBlock(String name) { return null; } + @Override + public SectionBlock getCurrentExtendingBlock(String name) { + return getExtendingBlock(name); + } + @Override public Object getAttribute(String key) { return attributeFun.apply(key); @@ -116,6 +121,14 @@ public SectionBlock getExtendingBlock(String name) { return null; } + @Override + public SectionBlock getCurrentExtendingBlock(String name) { + if (extendingBlocks != null) { + return extendingBlocks.get(name); + } + return null; + } + @Override public Object getAttribute(String key) { return parent.getAttribute(key); diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java index 316e046642fa9..0482275186af2 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java @@ -251,4 +251,100 @@ public void testIsolation() { assertEquals("NOT_FOUND", engine.parse("{#include foo _isolated /}").data("name", "Dorka").render()); } + @Test + public void testNestedMainBlocks() { + Engine engine = Engine.builder() + .addDefaults() + .build(); + + engine.putTemplate("root", engine.parse(""" + + {#insert /} + + """)); + engine.putTemplate("auth", engine.parse(""" + {#include root} +
+ {#insert /} +
+ {/include} + """)); + assertEquals("
LoginForm
" + + "", engine.parse(""" + {#include auth} +
Login Form
+ {/include} + """).render().replaceAll("\\s+", "")); + + engine.putTemplate("next", engine.parse(""" + {#include auth} + + {#insert /} + + {/include} + """)); + + // 1. top -> push child rc#1 with extending block $default$ + // 2. next -> push child rc#2 with extending block $default$ + // 3. auth -> push child rc#3 with extending block $default$ + // 4. root -> eval {#insert}, looks up $default$ in rc#3 + // 5. auth -> eval {#insert}, looks up $default$ in rc#2 + // 6. next -> eval {#insert}, looks up $default$ in rc#1 + assertEquals("
LoginForm
" + + "", engine.parse(""" + {#include next} +
Login Form
+ {/include} + """).render().replaceAll("\\s+", "")); + } + + @Test + public void testNestedBlocksWithSameName() { + Engine engine = Engine.builder() + .addDefaults() + .build(); + + engine.putTemplate("root", engine.parse(""" + + {#insert foo /} + + """)); + engine.putTemplate("auth", engine.parse(""" + {#include root} + {#foo} +
+ {#insert foo /} +
+ {/foo} + {/include} + """)); + assertEquals("
LoginForm
" + + "", engine.parse(""" + {#include auth} + {#foo} +
Login Form
+ {/foo} + {/include} + """).render().replaceAll("\\s+", "")); + + engine.putTemplate("next", engine.parse(""" + {#include auth} + {#foo} + + {#insert foo /} + + {/foo} + {/include} + """)); + + assertEquals("
LoginForm
" + + "", engine.parse(""" + {#include next} + {#foo} +
Login Form
+ {/foo} + {/include} + """).render().replaceAll("\\s+", "")); + } + } diff --git a/integration-tests/cache/src/main/resources/application.properties b/integration-tests/cache/src/main/resources/application.properties index 9f87c19434340..b9e8c04867772 100644 --- a/integration-tests/cache/src/main/resources/application.properties +++ b/integration-tests/cache/src/main/resources/application.properties @@ -6,6 +6,7 @@ quarkus.hibernate-orm.sql-load-script=import.sql # configure the caches quarkus.cache.caffeine."forest".expire-after-write=10M +quarkus.cache.caffeine."expensiveResourceCache".expire-after-access=10M quarkus.cache.caffeine."expensiveResourceCache".expire-after-write=10M quarkus.cache.caffeine."expensiveResourceCache".metrics-enabled=true quarkus.cache.caffeine."getIfPresentCache".metrics-enabled=true diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java index cd143b17d9abd..fd6dc7f0877f4 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java @@ -85,6 +85,7 @@ import org.junit.jupiter.api.extension.TestInstancePostProcessor; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; +import org.mockito.Mock; import io.quarkus.arc.All; import io.quarkus.arc.Arc; @@ -279,6 +280,8 @@ && isTestMethod(parameterContext.getDeclaringExecutable()) // A method/param annotated with @SkipInject is never supported && !parameterContext.isAnnotated(SkipInject.class) && !parameterContext.getDeclaringExecutable().isAnnotationPresent(SkipInject.class) + // A param annotated with @org.mockito.Mock is never supported + && !parameterContext.isAnnotated(Mock.class) // Skip params covered by built-in extensions && !BUILTIN_PARAMETER.test(parameterContext.getParameter())) { BeanManager beanManager = Arc.container().beanManager(); @@ -498,15 +501,9 @@ private static Set getQualifiers(AnnotatedElement element, C } private ClassLoader initArcContainer(ExtensionContext extensionContext, QuarkusComponentTestConfiguration configuration) { - Class testClass = extensionContext.getRequiredTestClass(); - // Collect all component injection points to define a bean removal exclusion - List injectFields = findInjectFields(testClass); - List injectParams = findInjectParams(testClass); - if (configuration.componentClasses.isEmpty()) { throw new IllegalStateException("No component classes to test"); } - // Make sure Arc is down try { Arc.shutdown(); @@ -528,6 +525,7 @@ private ClassLoader initArcContainer(ExtensionContext extensionContext, QuarkusC throw new IllegalStateException("Failed to create index", e); } + Class testClass = extensionContext.getRequiredTestClass(); ClassLoader testClassClassLoader = testClass.getClassLoader(); // The test class is loaded by the QuarkusClassLoader in continuous testing environment boolean isContinuousTesting = testClassClassLoader instanceof QuarkusClassLoader; @@ -543,6 +541,10 @@ private ClassLoader initArcContainer(ExtensionContext extensionContext, QuarkusC Set interceptorBindings = new HashSet<>(); AtomicReference beanResolver = new AtomicReference<>(); + // Collect all @Inject and @InjectMock test class injection points to define a bean removal exclusion + List injectFields = findInjectFields(testClass); + List injectParams = findInjectParams(testClass); + BeanProcessor.Builder builder = BeanProcessor.builder() .setName(testClass.getName().replace('.', '_')) .addRemovalExclusion(b -> { @@ -1010,7 +1012,6 @@ private List findInjectParams(Class testClass) { for (Method method : testMethods) { for (Parameter param : method.getParameters()) { if (BUILTIN_PARAMETER.test(param) - || param.isAnnotationPresent(InjectMock.class) || param.isAnnotationPresent(SkipInject.class)) { continue; } diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/mockito/MockitoExtensionTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/mockito/MockitoExtensionTest.java new file mode 100644 index 0000000000000..be71bcb797aea --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/mockito/MockitoExtensionTest.java @@ -0,0 +1,58 @@ +package io.quarkus.test.component.mockito; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.component.QuarkusComponentTest; + +@QuarkusComponentTest +public class MockitoExtensionTest { + + // Bar - component under test, real bean + // Baz - mock of the synthetic bean registered to satisfy Bar#baz + // Foo - plain Mockito mock + @ExtendWith(MockitoExtension.class) + @Test + public void testInjectMock(Bar bar, @InjectMock Baz baz, @Mock Foo foo) { + Mockito.when(foo.pong()).thenReturn(false); + Mockito.when(baz.ping()).thenReturn(foo); + assertFalse(bar.ping().pong()); + } + + @Singleton + public static class Bar { + + @Inject + Baz baz; + + Foo ping() { + return baz.ping(); + } + + } + + public static class Baz { + + Foo ping() { + return null; + } + + } + + public static class Foo { + + boolean pong() { + return true; + } + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectMockTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectMockTest.java new file mode 100644 index 0000000000000..3cba305ec54d4 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectMockTest.java @@ -0,0 +1,28 @@ +package io.quarkus.test.component.paraminject; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.component.QuarkusComponentTest; + +@QuarkusComponentTest +public class ParameterInjectMockTest { + + // Foo is mocked even if it's not a dependency of a tested component + @Test + public void testInjectMock(@InjectMock MyFoo foo) { + Mockito.when(foo.ping()).thenReturn(false); + assertFalse(foo.ping()); + } + + public static class MyFoo { + + boolean ping() { + return true; + } + } + +}