diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 19bca0d7cb51f..015aa3bc7dd62 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -105,7 +105,7 @@ 2.0.5.Final 8.0.1.Final - 6.2.1.Final + 6.2.2.Final 7.0.0.Final 2.1 8.0.0.Final @@ -204,7 +204,7 @@ 1.1.1.Final 2.20.0 1.3.0.Final - 1.11.2 + 1.11.3 2.4.3.Final 0.1.18.Final 1.18.3 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/JPMSExportBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/JPMSExportBuildItem.java index 126304bc05ac7..0c41ac1223e65 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/JPMSExportBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/JPMSExportBuildItem.java @@ -12,22 +12,30 @@ public final class JPMSExportBuildItem extends MultiBuildItem { private final String moduleName; private final String packageName; - private final GraalVM.Version exportAfter; + private final GraalVM.Version exportSince; private final GraalVM.Version exportBefore; public JPMSExportBuildItem(String moduleName, String packageName) { this(moduleName, packageName, null, null); } - public JPMSExportBuildItem(String moduleName, String packageName, GraalVM.Version exportAfter) { - this(moduleName, packageName, exportAfter, null); + public JPMSExportBuildItem(String moduleName, String packageName, GraalVM.Version exportSince) { + this(moduleName, packageName, exportSince, null); } - public JPMSExportBuildItem(String moduleName, String packageName, GraalVM.Version exportAfter, + /** + * Creates a build item that indicates that a Java package should be exported for a specific GraalVM version range. + * + * @param moduleName the module name + * @param packageName the package name + * @param exportSince the version of GraalVM since which the package should be exported (inclusive) + * @param exportBefore the version of GraalVM before which the package should be exported (exclusive) + */ + public JPMSExportBuildItem(String moduleName, String packageName, GraalVM.Version exportSince, GraalVM.Version exportBefore) { this.moduleName = moduleName; this.packageName = packageName; - this.exportAfter = exportAfter; + this.exportSince = exportSince; this.exportBefore = exportBefore; } @@ -56,8 +64,8 @@ public int hashCode() { return Objects.hash(moduleName, packageName); } - public GraalVM.Version getExportAfter() { - return exportAfter; + public GraalVM.Version getExportSince() { + return exportSince; } public GraalVM.Version getExportBefore() { @@ -65,7 +73,7 @@ public GraalVM.Version getExportBefore() { } public boolean isRequired(GraalVM.Version current) { - return (exportAfter == null || current.compareTo(exportAfter) > 0) && + return (exportSince == null || current.compareTo(exportSince) >= 0) && (exportBefore == null || current.compareTo(exportBefore) < 0); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java index bf8db1fa84953..9eb7e45c3a085 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java @@ -53,7 +53,7 @@ void addExportsToNativeImage(BuildProducer features) { features.produce(new JPMSExportBuildItem("org.graalvm.sdk", "org.graalvm.nativeimage.impl", null, GraalVM.Version.VERSION_23_1_0)); features.produce(new JPMSExportBuildItem("org.graalvm.nativeimage", "org.graalvm.nativeimage.impl", - GraalVM.Version.VERSION_23_0_0)); + GraalVM.Version.VERSION_23_1_0)); } @BuildStep diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/DurationConverter.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/DurationConverter.java index 925f26fa23051..f0250bc4f8439 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/DurationConverter.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/DurationConverter.java @@ -20,7 +20,7 @@ public class DurationConverter implements Converter, Serializable { private static final String PERIOD = "P"; private static final String PERIOD_OF_TIME = "PT"; public static final Pattern DIGITS = Pattern.compile("^[-+]?\\d+$"); - private static final Pattern DIGITS_AND_UNIT = Pattern.compile("^[-+]?\\d+(?:\\.\\d+)?(?i)[hms]$"); + private static final Pattern DIGITS_AND_UNIT = Pattern.compile("^(?:[-+]?\\d+(?:\\.\\d+)?(?i)[hms])+$"); private static final Pattern DAYS = Pattern.compile("^[-+]?\\d+(?i)d$"); private static final Pattern MILLIS = Pattern.compile("^[-+]?\\d+(?i)ms$"); diff --git a/core/runtime/src/test/java/io/quarkus/runtime/configuration/DurationConverterTestCase.java b/core/runtime/src/test/java/io/quarkus/runtime/configuration/DurationConverterTestCase.java index a3fa126ff7af8..af048140322ae 100644 --- a/core/runtime/src/test/java/io/quarkus/runtime/configuration/DurationConverterTestCase.java +++ b/core/runtime/src/test/java/io/quarkus/runtime/configuration/DurationConverterTestCase.java @@ -68,4 +68,18 @@ public void testValueIsInSec() { Duration actualDuration = durationConverter.convert("2s"); assertEquals(expectedDuration, actualDuration); } + + @Test + public void testValuesWithMultipleUnits() { + Duration expectedDuration = Duration.ofSeconds(150); + Duration actualDuration = durationConverter.convert("2m30s"); + assertEquals(expectedDuration, actualDuration); + } + + @Test + public void testValuesWithMultipleUnitsSigned() { + Duration expectedDuration = Duration.ofSeconds(90); + Duration actualDuration = durationConverter.convert("+2m-30s"); + assertEquals(expectedDuration, actualDuration); + } } diff --git a/docs/src/main/asciidoc/appcds.adoc b/docs/src/main/asciidoc/appcds.adoc index 25486c848647a..5811801c47bb0 100644 --- a/docs/src/main/asciidoc/appcds.adoc +++ b/docs/src/main/asciidoc/appcds.adoc @@ -87,6 +87,11 @@ This results in an archive generation process that on one hand is completely saf As a result, users are expected to get a slightly more effective archive if they manually go through the hoops of generating the AppCDS archive. ==== +[IMPORTANT] +==== +AppCDS has improved significantly in the latest JDK releases. This means that to ensure the best possible improvements from it, make sure your projects targets the highest possible Java version (ideally 17 or 21). +==== + === Usage in containers When building container images using the `quarkus-container-image-jib` extension, Quarkus automatically takes care of all the steps needed to generate the archive diff --git a/docs/src/main/asciidoc/datasource.adoc b/docs/src/main/asciidoc/datasource.adoc index d5afd66cfb894..a87a033b4056d 100644 --- a/docs/src/main/asciidoc/datasource.adoc +++ b/docs/src/main/asciidoc/datasource.adoc @@ -294,6 +294,22 @@ Quarkus offers several reactive clients for a use with reactive datasource. + The installed extension must be consistent with the `quarkus.datasource.db-kind` you define in your datasource configuration. +[WARNING] +==== +The Reactive Oracle datasource depends on the https://docs.oracle.com/en/database/oracle/oracle-database/23/jjdbc/jdbc-reactive-extensions.html[Reactive Extensions] provided by Oracle in its JDBC driver. + +There is a bug in versions 23.2 and 21.11 of the Oracle JDBC driver that prevents from getting any response if: + +* Reactive Extensions are used to execute an update/insert query that ends with an error (e.g. constraint violation), and +* https://vertx.io/docs/vertx-oracle-client/java/#_retrieving_generated_key_values[generated keys retrieval] is enabled. + +It is not known yet when the bug will be fixed. +In the meantime, you can either: + +* change the version of the driver in your project to `com.oracle.database.jdbc:ojdbc11:21.10.0.0`, or +* avoid executing queries with generated keys retrieval (e.g. load sequence value before inserting) +==== + . After adding the driver, configure the connection URL and define a proper size for your connection pool. + [source,properties] diff --git a/docs/src/main/asciidoc/dev-ui-v2.adoc b/docs/src/main/asciidoc/dev-ui-v2.adoc index a5ed2d582e335..c13f494b9d681 100644 --- a/docs/src/main/asciidoc/dev-ui-v2.adoc +++ b/docs/src/main/asciidoc/dev-ui-v2.adoc @@ -24,13 +24,13 @@ image::dev-ui-overview-v2.png[alt=Dev UI overview,role="center"] It allows you to: -- quickly visualise all the extensions currently loaded +- quickly visualize all the extensions currently loaded - view extension statuses and go directly to extension documentation - view and change `Configuration` -- manage and visualise `Continuous Testing` +- manage and visualize `Continuous Testing` - view `Dev Services` information - view the Build information -- view and stream various logs. +- view and stream various logs Each extension used in the application will be listed and you can navigate to the guide for each extension, see some more information on the extension, and view configuration applicable for that extension: @@ -42,7 +42,7 @@ In order to make your extension listed in the Dev UI you don't need to do anythi So you can always start with that :) -Extension can: +Extensions can: - <> - <> @@ -60,7 +60,7 @@ A good example of this is the SmallRye OpenAPI extension that contains links to image::dev-ui-extension-openapi-v2.png[alt=Dev UI extension card,role="center"] -The links to these external references is know at build time, so to get links like this on your card, all you need to do is add the following Build Step in your extension: +The links to these external references is known at build time, so to get links like this on your card, all you need to do is add the following Build Step in your extension: [source,java] ---- @@ -87,9 +87,9 @@ public CardPageBuildItem pages(NonApplicationRootPathBuildItem nonApplicationRoo return cardPageBuildItem; } ---- -<1> Always make sure that this build step only run when dev mode +<1> Always make sure that this build step is only run when in dev mode <2> To add anything on the card, you need to return/produce a `CardPageBuildItem`. -<3> To add a link, you can use the `addPage` method, as all links go to a "page". `Page` has got some builders to assist with building a page. For `external` links, use the `externalPageBuilder` +<3> To add a link, you can use the `addPage` method, as all links go to a "page". `Page` has some builders to assist with building a page. For `external` links, use the `externalPageBuilder` <4> Adding the url of the external link (in this case we use `NonApplicationRootPathBuildItem` to create this link, as this link is under the configurable non application path, default `/q`). Always use `NonApplicationRootPathBuildItem` if your link is available under `/q`. <5> You can (optionally) hint the content type of the content you are navigating to. If there is no hint, a header call will be made to determine the `MediaType`; <6> You can add an icon. All free font-awesome icons are available. @@ -101,15 +101,15 @@ If you find your icon at https://fontawesome.com/search?o=r&m=free[Font awesome] ==== Embedding external content -By default, even external links will render inside (embedded) in Dev UI. In the case of HTML the page will be rendered and any other content will be shown using https://codemirror.net/[code-mirror] to markup the media type, for example the open api schema document in `yaml` format: +By default, even external links will render inside (embedded) in Dev UI. In the case of HTML, the page will be rendered and any other content will be shown using https://codemirror.net/[code-mirror] to markup the media type. For example the open api schema document in `yaml` format: image::dev-ui-extension-openapi-embed-v2.png[alt=Dev UI embedded page,role="center"] -If you do not want to embed the content, you can use the `.doNotEmbed()` on the Page Builder, this will then open a new tab. +If you do not want to embed the content, you can use the `.doNotEmbed()` on the Page Builder, this will then open the link in a new tab. ==== Runtime external links -The example above assumes you know the link to use at build time. There might be cases where you only know this at runtime. In that case you can use a <> Method that returns the link to add and use that when creating the link. Rather than using the `.url` method on the page builder, use the `.dynamicUrlJsonRPCMethodName("yourJsonRPCMethodName")`. +The example above assumes you know the link to use at build time. There might be cases where you only know this at runtime. In that case you can use a <> Method that returns the link to add, and use that when creating the link. Rather than using the `.url` method on the page builder, use the `.dynamicUrlJsonRPCMethodName("yourJsonRPCMethodName")`. ==== Adding labels @@ -189,7 +189,7 @@ image::dev-ui-table-page-v2.png[alt=Dev UI table page,role="center"] ==== Qute data -You can also display your build time data using a qute template. All build time data keys is available to use int the template: +You can also display your build time data using a qute template. All build time data keys are available to use in the template: [source,java] ---- @@ -200,7 +200,7 @@ cardPageBuildItem.addPage(Page.quteDataPageBuilder("Qute data") // <1> <1> Use the `quteDataPageBuilder`. <2> Link to the Qute template in `/deployment/src/main/resources/dev-ui/`. -Using any qute template to display the data, example `qute-jokes-template.html`: +Using any Qute template to display the data, for example `qute-jokes-template.html`: [source,html] ---- @@ -227,7 +227,7 @@ Using any qute template to display the data, example `qute-jokes-template.html`: ==== Web Component page -To build an interactive page with actions and runtime (or built time) data, you need to use the web component page: +To build an interactive page with actions and runtime (or build time) data, you need to use the web component page: [source,java] ---- @@ -237,7 +237,7 @@ cardPageBuildItem.addPage(Page.webComponentPageBuilder() // <1> .staticLabel(String.valueOf(beans.size()))); ---- <1> Use the `webComponentPageBuilder`. -<2> Link to the Web Component in `/deployment/src/main/resources/dev-ui/`. The title can also be defined (using `.title("My title")` in the builder), but if not the title will be assumed from the componentLink, that should always have the format `qwc` (stands for Quarkus Web Component) dash `extensionName` (example, `arc` in this case ) dash `page title` ("Beans" in this case) +<2> Link to the Web Component in `/deployment/src/main/resources/dev-ui/`. The title can also be defined (using `.title("My title")` in the builder), but if not the title will be assumed from the componentLink, which should always have the format `qwc` (stands for Quarkus Web Component) dash `extensionName` (example, `arc` in this case ) dash `page title` ("Beans" in this case) Dev UI uses https://lit.dev/[Lit] to make building these web components easier. You can read more about Web Components and Lit: @@ -292,14 +292,14 @@ customElements.define('qwc-arc-beans', QwcArcBeans); // <10> ---- <1> You can import Classes and/or functions from other libraries. -In this case we use `LitElement` class and `html` & `css` functions from `Lit` -<2> Build time data as defined in the Build step can be imported using the key and always from `build-time-data`. All keys added in your Build step will be available. +In this case we use the `LitElement` class and `html` & `css` functions from `Lit` +<2> Build time data as defined in the Build step and can be imported using the key and always from `build-time-data`. All keys added in your Build step will be available. <3> The component should be named in the following format: Qwc (stands for Quarkus Web Component) then Extension Name then Page Title, all concatenated with Camel Case. This will also match the file name format as described earlier. The component should also extend `LitComponent`. <4> CSS styles can be added using the `css` function, and these styles only apply to your component. <5> Styles can reference globally defined CSS variables to make sure your page renders correctly, especially when switching between light and dark mode. You can find all CSS variables in the Vaadin documentation (https://vaadin.com/docs/latest/styling/lumo/lumo-style-properties/color[Color], https://vaadin.com/docs/latest/styling/lumo/lumo-style-properties/size-space[Sizing and Spacing], etc) -<6> Properties can be added. Use `_` in front of a property if that property is private. Properties are usually injected in the HTML template, and can be defined as having state, meaning that if that property changes, the component should re-render. In this case, the beans are Build time data and only change on hot-reload, that we will cover later. +<6> Properties can be added. Use `_` in front of a property if that property is private. Properties are usually injected in the HTML template, and can be defined as having state, meaning that if that property changes, the component should re-render. In this case, the beans are Build time data and only change on hot-reload, which will be covered later. <7> Constructors (optional) should always call `super` first, and then set the default values for the properties. -<8> The render method (comes from `LitElement` will be called to render the page). In this method you return the markup of the page you want. You can use the `html` function from `Lit`, that gives you a template language to output the HTML you want. Once the template is created, you only need to set/change the properties to re-render the page content. Read more about https://lit.dev/docs/components/rendering/[Lit html] +<8> The render method (from `LitElement`) will be called to render the page. In this method you return the markup of the page you want. You can use the `html` function from `Lit`, that gives you a template language to output the HTML you want. Once the template is created, you only need to set/change the properties to re-render the page content. Read more about https://lit.dev/docs/components/rendering/[Lit html] <9> You can use the built-in template functions to do conditional, list, etc. Read more about https://lit.dev/docs/templates/overview/[Lit Templates] <10> You always need to register your Web component as a custom element, with a unique tag. Here the tag will follow the same format as the filename (`qwc` dash `extension name` dash `page title` ); @@ -458,11 +458,11 @@ customElements.define('qwc-arc-beans', QwcArcBeans); ---- <1> Import the Vaadin component you want to use <2> You can also import other functions if needed -<3> There are some internal UI component that you can use, described below +<3> There are some internal UI components that you can use, described below ===== Using internal UI components -Some https://github.com/quarkusio/quarkus/tree/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui[internal UI components] (under the `qui` namespace) is available to make certain things easier: +Some https://github.com/quarkusio/quarkus/tree/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui[internal UI components] (under the `qui` namespace) are available to make certain things easier: - Card - Badge @@ -501,7 +501,7 @@ image::dev-ui-qui-badge-v2.png[alt=Dev UI Badge,role="center"] import 'qui-badge'; ---- -You can use any combination of small, primary, pill, with icon and clickable with any level of `default`, `success`, `warning`, `error`, `contrast` or set your own colours. +You can use any combination of small, primary, pill, with icon and clickable with any level of `default`, `success`, `warning`, `error`, `contrast` or set your own colors. [source,html] ---- @@ -603,7 +603,7 @@ https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d73 ====== Alert -Alerts is modelled around the Bootstrap alerts. Click https://getbootstrap.com/docs/4.0/components/alerts[here] for more info. +Alerts are modeled around the Bootstrap alerts. Click https://getbootstrap.com/docs/4.0/components/alerts[here] for more info. Also see Notification controller below as an alternative. @@ -733,7 +733,7 @@ https://github.com/quarkusio/quarkus/blob/582f1f78806d2268885faea7aa8f5a4d2b3f5b ===== Using internal controllers -Some https://github.com/quarkusio/quarkus/tree/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller[internal controllers] is available to make certain things easier: +Some https://github.com/quarkusio/quarkus/tree/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller[internal controllers] are available to make certain things easier: - Notifier - Storage @@ -776,7 +776,7 @@ https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d73 An easy way to access the local storage in a safe way. This will store values in the local storage, scoped to your extension. This way you do not have to worry that you might clash with another extension. -Local storage is useful to remember user preferences or state. Example, the footer remembers the state (open/close) and the size when open of the bottom drawer. +Local storage is useful to remember user preferences or state. For example, the footer remembers the state (open/close) and the size when open of the bottom drawer. [source,javascript] ---- @@ -836,7 +836,7 @@ https://github.com/quarkusio/quarkus/blob/main/extensions/vertx-http/dev-ui-reso ====== Router -The router is mostly used internally. This uses https://github.com/vaadin/router[Vaadin Router] under the covers to route urls to the correct page/section within the SPA. It will update the navigation and allow history (back button). This also creates the sub-menu available on extensions that has multiple pages. +The router is mostly used internally. This uses https://github.com/vaadin/router[Vaadin Router] under the covers to route URLs to the correct page/section within the SPA. It will update the navigation and allow history (back button). This also creates the sub-menu available on extensions that have multiple pages. See the https://github.com/quarkusio/quarkus/blob/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/router-controller.js[controller] for some methods that might be useful. @@ -860,7 +860,7 @@ JsonRPCProvidersBuildItem createJsonRPCServiceForCache() {// <2> ---- <1> Always only do this in Dev Mode <2> Produce / return a `JsonRPCProvidersBuildItem` -<3> Define the class in your runtime module that will contain methods that makes data available in the UI +<3> Define the class in your runtime module that will contain methods that make data available in the UI https://github.com/quarkusio/quarkus/blob/main/extensions/cache/deployment/src/main/java/io/quarkus/cache/deployment/devui/CacheDevUiProcessor.java[Example code] @@ -930,13 +930,13 @@ connectedCallback() { ---- <1> Note the method `getAll` corresponds to the method in your Java Service. This method returns a https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise[Promise] with the JsonRPC result. -<2> in this case the result is an array, so we can loop over it. +<2> In this case the result is an array, so we can loop over it. JsonArray (or any Java collection) in either blocking or non-blocking will return an array, else a JsonObject will be returned. https://github.com/quarkusio/quarkus/blob/main/extensions/cache/deployment/src/main/resources/dev-ui/qwc-cache-caches.js[Example code] -You can also pass in parameters in the method being called, example: +You can also pass in parameters in the method being called, for example: (In the Runtime Java code) [source,java] @@ -1047,7 +1047,7 @@ export class QwcMyExtensionPage extends QwcHotReloadElement { == Custom cards -You can customise the card that is being displayed on the extension page if you do not want to use the default built-in card. +You can customize the card that is being displayed on the extension page if you do not want to use the default built-in card. To do this, you need to provide a Webcomponent that will be loaded in the place of the provided card and register this in the Java Processor: @@ -1063,7 +1063,7 @@ On the Javascript side, you have access to all the pages (in case you want to cr import { pages } from 'build-time-data'; ---- -And the following properties will be passes in: +And the following properties will be passed in: - extensionName - description diff --git a/docs/src/main/asciidoc/getting-started-dev-services.adoc b/docs/src/main/asciidoc/getting-started-dev-services.adoc index e9daadbc4993d..ace01fc77c6dc 100644 --- a/docs/src/main/asciidoc/getting-started-dev-services.adoc +++ b/docs/src/main/asciidoc/getting-started-dev-services.adoc @@ -3,18 +3,21 @@ This document 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 //// - [id="getting-started-dev-services-tutorial"] = Your second Quarkus application include::_attributes.adoc[] :diataxis-type: tutorial :categories: getting-started, data, core - +:summary: Discover some of the features that make developing with Quarkus a joyful experience. This tutorial shows you how to create an application which writes to and reads from a database. You will use Dev Services, so you will not actually download, configure, or even start the database yourself. You will also use Panache, a layer on top of Hibernate ORM, to make reading and writing data easier. +This guide helps you: + + * Read and write objects to a database + * Develop and test against services with zero configuration == Prerequisites diff --git a/docs/src/main/asciidoc/getting-started.adoc b/docs/src/main/asciidoc/getting-started.adoc index 39c1d9625ded0..f76500792ae0f 100644 --- a/docs/src/main/asciidoc/getting-started.adoc +++ b/docs/src/main/asciidoc/getting-started.adoc @@ -488,8 +488,9 @@ include::{generated-dir}/config/quarkus-info.adoc[opts=optional, leveloffset=+2] This guide covered the creation of an application using Quarkus. However, there is much more. -We recommend continuing the journey with the xref:building-native-image.adoc[building a native executable guide], where you learn about creating a native executable and packaging it in a container. -If you are interested in reactive, we recommend the xref:getting-started-reactive.adoc[Getting Started with Reactive guide], where you can see how to implement reactive applications with Quarkus. +We recommend continuing the journey by creating xref:getting-started-dev-services.adoc[your second Quarkus application], with dev services and persistence. +You can learn about creating a native executable and packaging it in a container with the xref:building-native-image.adoc[building a native executable guide]. +If you are interested in reactive, we recommend the xref:getting-started-reactive.adoc[getting started with reactive guide], where you can see how to implement reactive applications with Quarkus. In addition, the xref:tooling.adoc[tooling guide] document explains how to: diff --git a/docs/src/main/asciidoc/images/oidc-google-authorized-redirects.png b/docs/src/main/asciidoc/images/oidc-google-authorized-redirects.png new file mode 100644 index 0000000000000..844b400d5641c Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-google-authorized-redirects.png differ diff --git a/docs/src/main/asciidoc/images/oidc-google-test-users.png b/docs/src/main/asciidoc/images/oidc-google-test-users.png new file mode 100644 index 0000000000000..42a82269fcf75 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-google-test-users.png differ diff --git a/docs/src/main/asciidoc/security-openid-connect-providers.adoc b/docs/src/main/asciidoc/security-openid-connect-providers.adoc index 2f9e5dac0e7d6..8b7b24c82ea5a 100644 --- a/docs/src/main/asciidoc/security-openid-connect-providers.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-providers.adoc @@ -542,10 +542,68 @@ Finally, you need to configure the Google Calendar address and request the Goo [source,properties] ---- +quarkus.oidc.provider=google +quarkus.oidc.client-id= +quarkus.oidc.credentials.secret= + +# Add a required calendar scope quarkus.oidc.authentication.extra-params.scope=https://www.googleapis.com/auth/calendar + +# Point REST client to Google Calendar endpoint quarkus.rest-client.google-calendar-api.url=https://www.googleapis.com/calendar/v3 ---- +Now you are ready to have users authenticated with Google and support updating their `Google` calendars on their behalf, for example: + +[source,java] +---- +package org.acme.calendar; + +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import io.quarkus.oidc.IdToken; +import io.quarkus.security.Authenticated; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; + +@Path("/calendar") +@Authenticated +public class CalendarService { + + @Inject + @IdToken + JsonWebToken jwt; + + @Inject + @RestClient + GoogleCalendarClient calendarClient; + + @GET + @Path("/event") + @Produces("text/plain") + public Uni get() { + return calendarClient.addEvent(new Event()).onItem() + .transform(c -> ("Hello " + jwt.getName() + ", new event: " + c)); + } +} +---- + +You must update the application registered with the <> provider to list `http://localhost:8080/calendar/event` as one of the authorized redirect URIs if you would like to test this endpoint on the local host, for example: + +image::oidc-google-authorized-redirects.png[role="thumb"] + +You might also have to register one or more test users: + +image::oidc-google-test-users.png[role="thumb"] + +Follow the same approach if the endpoint must access other Google services. + +The pattern of authenticating with a given provider, where the endpoint uses either an ID token or UserInfo (especially if an OAuth2-only provider such as `GitHub` is used) to get some information about the currently authenticated user and using an access token to access some downstream services (provider or application specific ones) on behalf of this user can be universally applied, irrespectively of which provider is used to secure the application. + == HTTPS Redirect URL Some providers will only accept HTTPS-based redirect URLs. Tools such as https://ngrok.com/[ngrok] https://linuxhint.com/set-up-use-ngrok/[can be set up] to help testing such providers with Quarkus endpoints running on localhost in devmode. diff --git a/docs/src/main/asciidoc/smallrye-graphql.adoc b/docs/src/main/asciidoc/smallrye-graphql.adoc index 296dd9e9760ad..15b65f2d3f095 100644 --- a/docs/src/main/asciidoc/smallrye-graphql.adoc +++ b/docs/src/main/asciidoc/smallrye-graphql.adoc @@ -289,7 +289,7 @@ The GraphQL UI can be accessed from http://localhost:8080/q/graphql-ui/ . image:graphql-ui-screenshot01.png[alt=GraphQL UI] -Have a look at the link:security-authorization[Authorization of Web Endpoints] Guide on how to add/remove security for the GraphQL UI. +Have a look at the link:security-authorize-web-endpoints-reference[Authorization of Web Endpoints] Guide on how to add/remove security for the GraphQL UI. == Query the GraphQL API diff --git a/docs/src/main/asciidoc/transaction.adoc b/docs/src/main/asciidoc/transaction.adoc index 4bbf0f4281e14..14ab4857d8026 100644 --- a/docs/src/main/asciidoc/transaction.adoc +++ b/docs/src/main/asciidoc/transaction.adoc @@ -369,7 +369,7 @@ In cloud environments where persistent storage is not available, such as when ap IMPORTANT: While there are several benefits to using a database to store transaction logs, you might notice a reduction in performance compared with using the file system to store the logs. -Quarkus allows the following JDBC-specific configuration of the object store included in `quarkus.transacion-manager.object-store.` properties, where can be: +Quarkus allows the following JDBC-specific configuration of the object store included in `quarkus.transaction-manager.object-store.` properties, where can be: * `type` (_string_): Configure this property to `jdbc` to enable usage of a Quarkus JDBC datasource for transaction logging. The default value is `file-system`. diff --git a/docs/src/main/asciidoc/virtual-threads.adoc b/docs/src/main/asciidoc/virtual-threads.adoc index 946c009dbc637..fea2c14279868 100644 --- a/docs/src/main/asciidoc/virtual-threads.adoc +++ b/docs/src/main/asciidoc/virtual-threads.adoc @@ -147,7 +147,7 @@ For each of them, the object stored in the `ThreadLocal` is created (often large This problem leads to high memory usage. Unfortunately, it requires sophisticated code changes in the libraries themselves. -=== Use @RunVirtualThread with RESTEasy Reactive +=== Use @RunOnVirtualThread with RESTEasy Reactive This section shows a brief example of using the link:{runonvthread}[@RunOnVirtualThread] annotation. It also explains the various development and execution models offered by Quarkus. @@ -296,7 +296,7 @@ Note that all three models can be used in a single application. == Use virtual thread friendly clients -As mentioned in the href:why-not[Why not run everything on virtual threads?] section, the Java ecosystem is not entirely ready for virtual threads. +As mentioned in the xref:why-not[Why not run everything on virtual threads?] section, the Java ecosystem is not entirely ready for virtual threads. So, you need to be careful, especially when using a libraries doing I/O. Fortunately, Quarkus provides a massive ecosystem that is ready to be used in virtual threads. @@ -375,7 +375,7 @@ mvn package === Using a local GraalVM installation -To compile a Quarkus applications leveraging `@RunOnVirtualThreads` into native executable, you must be sure to use a GraalVM / Mandrel `native-image` supporting virtual threads, so providing at least Java 19+. +To compile a Quarkus applications leveraging `@RunOnVirtualThread` into native executable, you must be sure to use a GraalVM / Mandrel `native-image` supporting virtual threads, so providing at least Java 19+. Then, until Java 21, you need to add the following property to your `application.properties` file: @@ -465,4 +465,4 @@ quarkus.virtual-threads.name-prefix= == Additional references -- https://dl.acm.org/doi/10.1145/3583678.3596895[Considerations for integrating virtual threads in a Java framework: a Quarkus example in a resource-constrained environment] \ No newline at end of file +- https://dl.acm.org/doi/10.1145/3583678.3596895[Considerations for integrating virtual threads in a Java framework: a Quarkus example in a resource-constrained environment] diff --git a/extensions/awt/deployment/src/main/java/io/quarkus/awt/deployment/AwtProcessor.java b/extensions/awt/deployment/src/main/java/io/quarkus/awt/deployment/AwtProcessor.java index 46f4b9b7807f5..3a796840a990c 100644 --- a/extensions/awt/deployment/src/main/java/io/quarkus/awt/deployment/AwtProcessor.java +++ b/extensions/awt/deployment/src/main/java/io/quarkus/awt/deployment/AwtProcessor.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.stream.Stream; import io.quarkus.awt.runtime.graal.DarwinAwtFeature; @@ -21,6 +22,8 @@ import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedPackageBuildItem; import io.quarkus.deployment.builditem.nativeimage.UnsupportedOSBuildItem; import io.quarkus.deployment.pkg.builditem.NativeImageRunnerBuildItem; +import io.quarkus.deployment.pkg.builditem.ProcessInheritIODisabled; +import io.quarkus.deployment.pkg.builditem.ProcessInheritIODisabledBuildItem; import io.quarkus.deployment.pkg.steps.GraalVM; import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; @@ -95,7 +98,11 @@ ReflectiveClassBuildItem setupReflectionClassesWithMethods() { void setupAWTInit(BuildProducer jc, BuildProducer jm, BuildProducer jf, - NativeImageRunnerBuildItem nativeImageRunnerBuildItem) { + NativeImageRunnerBuildItem nativeImageRunnerBuildItem, + Optional processInheritIODisabled, + Optional processInheritIODisabledBuildItem) { + nativeImageRunnerBuildItem.getBuildRunner() + .setup(processInheritIODisabled.isPresent() || processInheritIODisabledBuildItem.isPresent()); final GraalVM.Version v = nativeImageRunnerBuildItem.getBuildRunner().getGraalVMVersion(); // Dynamically loading shared objects instead // of baking in static libs: https://github.com/oracle/graal/issues/4921 @@ -118,7 +125,11 @@ void setupAWTInit(BuildProducer jc, } @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) - JniRuntimeAccessBuildItem setupJava2DClasses(NativeImageRunnerBuildItem nativeImageRunnerBuildItem) { + JniRuntimeAccessBuildItem setupJava2DClasses(NativeImageRunnerBuildItem nativeImageRunnerBuildItem, + Optional processInheritIODisabled, + Optional processInheritIODisabledBuildItem) { + nativeImageRunnerBuildItem.getBuildRunner() + .setup(processInheritIODisabled.isPresent() || processInheritIODisabledBuildItem.isPresent()); final GraalVM.Version v = nativeImageRunnerBuildItem.getBuildRunner().getGraalVMVersion(); final List classes = new ArrayList<>(); classes.add("com.sun.imageio.plugins.jpeg.JPEGImageReader"); diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/JpaJandexScavenger.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/JpaJandexScavenger.java index 225b2a406491e..198fd9a5ac411 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/JpaJandexScavenger.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/JpaJandexScavenger.java @@ -59,8 +59,7 @@ */ public final class JpaJandexScavenger { - public static final List EMBEDDED_ANNOTATIONS = Arrays.asList(ClassNames.EMBEDDED_ID, ClassNames.EMBEDDED, - ClassNames.ELEMENT_COLLECTION); + public static final List EMBEDDED_ANNOTATIONS = Arrays.asList(ClassNames.EMBEDDED_ID, ClassNames.EMBEDDED); private static final String XML_MAPPING_DEFAULT_ORM_XML = "META-INF/orm.xml"; private static final String XML_MAPPING_NO_FILE = "no-file"; @@ -313,17 +312,15 @@ private void enlistEmbeddedsAndElementCollections(Collector collector) throws Bu Set embeddedTypes = new HashSet<>(); for (DotName embeddedAnnotation : EMBEDDED_ANNOTATIONS) { - Collection annotations = index.getAnnotations(embeddedAnnotation); - - for (AnnotationInstance annotation : annotations) { + for (AnnotationInstance annotation : index.getAnnotations(embeddedAnnotation)) { AnnotationTarget target = annotation.target(); switch (target.kind()) { case FIELD: - collectEmbeddedTypes(embeddedAnnotation, embeddedTypes, target.asField().type()); + collectEmbeddedType(embeddedTypes, target.asField().type(), true); break; case METHOD: - collectEmbeddedTypes(embeddedAnnotation, embeddedTypes, target.asMethod().returnType()); + collectEmbeddedType(embeddedTypes, target.asMethod().returnType(), true); break; default: throw new IllegalStateException( @@ -333,6 +330,23 @@ private void enlistEmbeddedsAndElementCollections(Collector collector) throws Bu } } + for (AnnotationInstance annotation : index.getAnnotations(ClassNames.ELEMENT_COLLECTION)) { + AnnotationTarget target = annotation.target(); + + switch (target.kind()) { + case FIELD: + collectElementCollectionTypes(embeddedTypes, target.asField().type()); + break; + case METHOD: + collectElementCollectionTypes(embeddedTypes, target.asMethod().returnType()); + break; + default: + throw new IllegalStateException( + "[internal error] " + ClassNames.ELEMENT_COLLECTION + " placed on a unknown element: " + target); + } + + } + for (DotName embeddedType : embeddedTypes) { addClassHierarchyToReflectiveList(collector, embeddedType); } @@ -481,22 +495,44 @@ private static void collectModelType(Collector collector, ClassInfo modelClass) } } - private void collectEmbeddedTypes(DotName embeddedAnnotation, Set embeddedTypes, Type indexType) + private void collectEmbeddedType(Set embeddedTypes, Type embeddedType, boolean validate) + throws BuildException { + DotName className; + switch (embeddedType.kind()) { + case CLASS: + className = embeddedType.asClassType().name(); + break; + case PARAMETERIZED_TYPE: + className = embeddedType.name(); + break; + default: + // do nothing + return; + } + if (validate && !index.getClassByName(className).hasAnnotation(ClassNames.EMBEDDABLE)) { + throw new BuildException( + className + " is used as an embeddable but does not have an @Embeddable annotation."); + } + embeddedTypes.add(embeddedType.name()); + } + + private void collectElementCollectionTypes(Set embeddedTypes, Type indexType) throws BuildException { switch (indexType.kind()) { case CLASS: - DotName className = indexType.asClassType().name(); - validateEmbeddable(embeddedAnnotation, className); - embeddedTypes.add(className); + // Raw collection type, nothing we can do break; case PARAMETERIZED_TYPE: embeddedTypes.add(indexType.name()); - for (Type typeArgument : indexType.asParameterizedType().arguments()) { - collectEmbeddedTypes(embeddedAnnotation, embeddedTypes, typeArgument); + var typeArguments = indexType.asParameterizedType().arguments(); + for (Type typeArgument : typeArguments) { + // We don't validate @Embeddable annotations on element collections at the moment + // See https://github.com/quarkusio/quarkus/pull/35822 + collectEmbeddedType(embeddedTypes, typeArgument, false); } break; case ARRAY: - collectEmbeddedTypes(embeddedAnnotation, embeddedTypes, indexType.asArrayType().constituent()); + collectEmbeddedType(embeddedTypes, indexType.asArrayType().constituent(), true); break; default: // do nothing @@ -504,14 +540,6 @@ private void collectEmbeddedTypes(DotName embeddedAnnotation, Set embed } } - private void validateEmbeddable(DotName embeddedAnnotation, DotName className) throws BuildException { - if ((ClassNames.EMBEDDED.equals(embeddedAnnotation) || ClassNames.EMBEDDED_ID.equals(embeddedAnnotation)) - && !index.getClassByName(className).hasAnnotation(ClassNames.EMBEDDABLE)) { - throw new BuildException( - className + " is used as an embeddable but does not have an @Embeddable annotation."); - } - } - private static boolean isIgnored(DotName classDotName) { String className = classDotName.toString(); if (className.startsWith("java.util.") || className.startsWith("java.lang.") diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/enhancer/HibernateEntityEnhancerPresentEmbeddableTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/enhancer/HibernateEntityEnhancerPresentEmbeddableTest.java new file mode 100644 index 0000000000000..75093df783891 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/enhancer/HibernateEntityEnhancerPresentEmbeddableTest.java @@ -0,0 +1,293 @@ +package io.quarkus.hibernate.orm.enhancer; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import jakarta.inject.Inject; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.OrderColumn; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.hibernate.annotations.SortNatural; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.TransactionTestUtils; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Checks that the missing @Embeddable check doesn't mistakely report + * types that are annotated with @Embeddable (https://github.com/quarkusio/quarkus/issues/35598) + * or generic type parameters on @Embedded field types (https://github.com/quarkusio/quarkus/issues/36065) + */ +public class HibernateEntityEnhancerPresentEmbeddableTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(TransactionTestUtils.class) + .addClasses(EntityWithEmbedded.class, EmbeddableWithAnnotation.class, + ExtendedEmbeddableWithAnnotation.class, + NestingEmbeddableWithAnnotation.class, + GenericEmbeddableWithAnnotation.class)) + .withConfigurationResource("application.properties") + .overrideConfigKey("quarkus.hibernate-orm.implicit-naming-strategy", "component-path"); + + @Inject + EntityManager em; + + // Just test that the generic embeddeds work correctly over a persist/retrieve cycle + @Test + public void smokeTest() { + Long id = QuarkusTransaction.requiringNew().call(() -> { + EntityWithEmbedded entity = new EntityWithEmbedded(); + entity.setName("name"); + entity.setEmbeddedWithAnnotation(new EmbeddableWithAnnotation("simple")); + entity.setExtendedEmbeddedWithAnnotation(new ExtendedEmbeddableWithAnnotation("extended", 42)); + var nesting = new NestingEmbeddableWithAnnotation("nesting"); + entity.setNestingEmbeddedWithAnnotation(nesting); + nesting.setEmbedded(new EmbeddableWithAnnotation("nested")); + entity.setGenericEmbeddedWithAnnotation(new GenericEmbeddableWithAnnotation<>("generic")); + entity.setEmbeddableListWithAnnotation(List.of( + new EmbeddableWithAnnotation("list1"), + new EmbeddableWithAnnotation("list2"))); + entity.setEmbeddableMapValueWithAnnotation(new TreeMap<>(Map.of( + "first", new EmbeddableWithAnnotation("map1"), + "second", new EmbeddableWithAnnotation("map2")))); + em.persist(entity); + return entity.getId(); + }); + + QuarkusTransaction.requiringNew().run(() -> { + EntityWithEmbedded entity = em.find(EntityWithEmbedded.class, id); + assertThat(entity).extracting(e -> e.getName()) + .isEqualTo("name"); + assertThat(entity).extracting(e -> e.getEmbeddedWithAnnotation().getText()) + .isEqualTo("simple"); + assertThat(entity).extracting(e -> e.getExtendedEmbeddedWithAnnotation().getText()) + .isEqualTo("extended"); + assertThat(entity).extracting(e -> e.getExtendedEmbeddedWithAnnotation().getInteger()) + .isEqualTo(42); + assertThat(entity).extracting(e -> e.getNestingEmbeddedWithAnnotation().getText()) + .isEqualTo("nesting"); + assertThat(entity).extracting(e -> e.getNestingEmbeddedWithAnnotation().getEmbedded().getText()) + .isEqualTo("nested"); + assertThat(entity).extracting(e -> e.getGenericEmbeddedWithAnnotation().getValue()) + .isEqualTo("generic"); + assertThat(entity).extracting(e -> e.getEmbeddableListWithAnnotation()) + .asInstanceOf(InstanceOfAssertFactories.list(EmbeddableWithAnnotation.class)) + .extracting(EmbeddableWithAnnotation::getText) + .containsExactly("list1", "list2"); + assertThat(entity).extracting(e -> e.getEmbeddableMapValueWithAnnotation()) + .asInstanceOf(InstanceOfAssertFactories.map(String.class, EmbeddableWithAnnotation.class)) + .extractingFromEntries(e -> e.getValue().getText()) + .containsExactly("map1", "map2"); + }); + } + + @Entity + public static class EntityWithEmbedded { + + @Id + @GeneratedValue + private Long id; + + private String name; + + @Embedded + private EmbeddableWithAnnotation embeddedWithAnnotation; + + @Embedded + private ExtendedEmbeddableWithAnnotation extendedEmbeddedWithAnnotation; + + @Embedded + private NestingEmbeddableWithAnnotation nestingEmbeddedWithAnnotation; + + @Embedded + private GenericEmbeddableWithAnnotation genericEmbeddedWithAnnotation; + + @ElementCollection + @OrderColumn + private List embeddableListWithAnnotation; + + @ElementCollection + @SortNatural + private SortedMap embeddableMapValueWithAnnotation; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public EmbeddableWithAnnotation getEmbeddedWithAnnotation() { + return embeddedWithAnnotation; + } + + public void setEmbeddedWithAnnotation(EmbeddableWithAnnotation embeddedWithAnnotation) { + this.embeddedWithAnnotation = embeddedWithAnnotation; + } + + public ExtendedEmbeddableWithAnnotation getExtendedEmbeddedWithAnnotation() { + return extendedEmbeddedWithAnnotation; + } + + public void setExtendedEmbeddedWithAnnotation(ExtendedEmbeddableWithAnnotation extendedEmbeddedWithAnnotation) { + this.extendedEmbeddedWithAnnotation = extendedEmbeddedWithAnnotation; + } + + public NestingEmbeddableWithAnnotation getNestingEmbeddedWithAnnotation() { + return nestingEmbeddedWithAnnotation; + } + + public void setNestingEmbeddedWithAnnotation(NestingEmbeddableWithAnnotation nestingEmbeddedWithAnnotation) { + this.nestingEmbeddedWithAnnotation = nestingEmbeddedWithAnnotation; + } + + public GenericEmbeddableWithAnnotation getGenericEmbeddedWithAnnotation() { + return genericEmbeddedWithAnnotation; + } + + public void setGenericEmbeddedWithAnnotation(GenericEmbeddableWithAnnotation genericEmbeddedWithAnnotation) { + this.genericEmbeddedWithAnnotation = genericEmbeddedWithAnnotation; + } + + public List getEmbeddableListWithAnnotation() { + return embeddableListWithAnnotation; + } + + public void setEmbeddableListWithAnnotation(List embeddableListWithAnnotation) { + this.embeddableListWithAnnotation = embeddableListWithAnnotation; + } + + public Map getEmbeddableMapValueWithAnnotation() { + return embeddableMapValueWithAnnotation; + } + + public void setEmbeddableMapValueWithAnnotation( + SortedMap embeddableMapValueWithAnnotation) { + this.embeddableMapValueWithAnnotation = embeddableMapValueWithAnnotation; + } + } + + @Embeddable + @MappedSuperclass + public static class EmbeddableWithAnnotation { + private String text; + + protected EmbeddableWithAnnotation() { + // For Hibernate ORM only - it will change the property value through reflection + } + + public EmbeddableWithAnnotation(String text) { + this.text = text; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + } + + @Embeddable + public static class NestingEmbeddableWithAnnotation { + private String text; + + @Embedded + private EmbeddableWithAnnotation embedded; + + protected NestingEmbeddableWithAnnotation() { + // For Hibernate ORM only - it will change the property value through reflection + } + + public NestingEmbeddableWithAnnotation(String text) { + this.text = text; + this.embedded = new EmbeddableWithAnnotation(text); + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public EmbeddableWithAnnotation getEmbedded() { + return embedded; + } + + public void setEmbedded(EmbeddableWithAnnotation embedded) { + this.embedded = embedded; + } + } + + @Embeddable + public static class ExtendedEmbeddableWithAnnotation extends EmbeddableWithAnnotation { + private Integer integer; + + protected ExtendedEmbeddableWithAnnotation() { + // For Hibernate ORM only - it will change the property value through reflection + } + + public ExtendedEmbeddableWithAnnotation(String text, Integer integer) { + super(text); + this.integer = integer; + } + + public Integer getInteger() { + return integer; + } + + public void setInteger(Integer integer) { + this.integer = integer; + } + } + + @Embeddable + public static class GenericEmbeddableWithAnnotation { + private T value; + + protected GenericEmbeddableWithAnnotation() { + // For Hibernate ORM only - it will change the property value through reflection + } + + public GenericEmbeddableWithAnnotation(T value) { + this.value = value; + } + + public T getValue() { + return value; + } + + public void setValue(T value) { + this.value = value; + } + } + +} diff --git a/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js b/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js index 3cd1f9e32c2d2..5960477bd1aa8 100644 --- a/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js +++ b/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js @@ -1,4 +1,5 @@ import { LitElement, html, css} from 'lit'; +import {unsafeHTML} from 'lit/directives/unsafe-html.js'; import { columnBodyRenderer } from '@vaadin/grid/lit.js'; import { infoUrl } from 'build-time-data'; import '@vaadin/progress-bar'; @@ -22,13 +23,20 @@ export class QwcInfo extends LitElement { } .cardContent { display: flex; - align-items: center; padding: 10px; gap: 10px; + height: 100%; } vaadin-icon { font-size: xx-large; } + .table { + height: fit-content; + } + .row-header { + color: var(--lumo-contrast-50pct); + vertical-align: top; + } `; static properties = { @@ -58,8 +66,8 @@ export class QwcInfo extends LitElement { return html` ${this._renderOsInfo(this._info)} ${this._renderJavaInfo(this._info)} - ${this._renderGitInfo(this._info)} ${this._renderBuildInfo(this._info)} + ${this._renderGitInfo(this._info)} `; }else{ return html` @@ -78,9 +86,9 @@ export class QwcInfo extends LitElement {
${this._renderOsIcon(os.name)} - - - + + +
Name${os.name}
Version${os.version}
Arch${os.arch}
Name${os.name}
Version${os.version}
Arch${os.arch}
`; @@ -94,7 +102,7 @@ export class QwcInfo extends LitElement {
- +
Version${java.version}
Version${java.version}
`; @@ -121,25 +129,45 @@ export class QwcInfo extends LitElement {
- - - + + + + ${this._renderOptionalData(git)}
Branch${git.branch}
Commit${git.commit.id}
Time${git.commit.time}
Branch${git.branch}
Commit Id ${this._renderCommitId(git)}
Commit Time${git.commit.time}
`; } } + _renderCommitId(git){ + if(typeof git.commit.id === "string"){ + return html`${git.commit.id}`; + }else { + return html`${git.commit.id.full}`; + } + } + + _renderOptionalData(git){ + if(typeof git.commit.id !== "string"){ + return html`Commit User${git.commit.user.name} <${git.commit.user.email}> + Commit Message${unsafeHTML(this._replaceNewLine(git.commit.id.message.full))}` + } + } + + _replaceNewLine(line){ + return line.replace(new RegExp('\r?\n','g'), '
'); + } + _renderBuildInfo(info){ if(info.build){ let build = info.build; return html`
- - - - + + + +
Group${build.group}
Artifact${build.artifact}
Version${build.version}
Time${build.time}
Group${build.group}
Artifact${build.artifact}
Version${build.version}
Time${build.time}
`; diff --git a/extensions/jaeger/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/jaeger/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 89d19a886def1..4a0f1d0a38eb6 100644 --- a/extensions/jaeger/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/jaeger/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,6 +9,6 @@ metadata: categories: - "observability" unlisted: true - status: "stable" + status: "deprecated" config: - "quarkus.jaeger." diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java index 511a1d5f8f1e8..c294cbbf16f5d 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java @@ -354,8 +354,7 @@ private static Collection createRbacDecorators(String name, } } - // Add service account from extensions: use the one provided by the user always - Optional effectiveServiceAccount = config.getServiceAccount(); + Optional effectiveServiceAccount = Optional.empty(); String effectiveServiceAccountNamespace = null; for (KubernetesServiceAccountBuildItem sa : serviceAccountsFromExtensions) { String saName = Optional.ofNullable(sa.getName()).orElse(name); @@ -382,6 +381,12 @@ private static Collection createRbacDecorators(String name, } } + // The user provided service account should always take precedence + if (config.getServiceAccount().isPresent()) { + effectiveServiceAccount = config.getServiceAccount(); + effectiveServiceAccountNamespace = null; + } + // Prepare default configuration String defaultRoleName = null; boolean defaultClusterWide = false; @@ -699,6 +704,13 @@ public static List createInitJobDecorators(String target, St .map(Optional::get) .collect(Collectors.toList()); + List serviceAccountDecorators = decorators.stream() + .filter(d -> d.getGroup() == null || d.getGroup().equals(target)) + .map(d -> d.getDecorator(ApplyServiceAccountNameDecorator.class)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + items.stream().filter(item -> item.getTarget() == null || item.getTarget().equals(target)).forEach(item -> { for (final AddImagePullSecretDecorator delegate : imagePullSecretDecorators) { @@ -710,6 +722,15 @@ public void andThenVisit(PodSpecBuilder builder, ObjectMeta meta) { })); } + for (final ApplyServiceAccountNameDecorator delegate : serviceAccountDecorators) { + result.add(new DecoratorBuildItem(target, new NamedResourceDecorator("Job", item.getName()) { + @Override + public void andThenVisit(PodSpecBuilder builder, ObjectMeta meta) { + delegate.andThenVisit(builder, meta); + } + })); + } + result.add(new DecoratorBuildItem(target, new NamedResourceDecorator("Job", item.getName()) { @Override public void andThenVisit(ContainerBuilder builder, ObjectMeta meta) { diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/graal/SubstituteEnvironmentValueProvider.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/graal/SubstituteEnvironmentValueProvider.java new file mode 100644 index 0000000000000..2fae996137a87 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/graal/SubstituteEnvironmentValueProvider.java @@ -0,0 +1,20 @@ +package io.quarkus.liquibase.runtime.graal; + +import java.util.Map; + +import com.oracle.svm.core.annotate.Delete; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "liquibase.configuration.core.EnvironmentValueProvider") +final class SubstituteEnvironmentValueProvider { + + @Delete + private Map environment; + + @Substitute + protected Map getMap() { + return System.getenv(); + } + +} \ No newline at end of file diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/GrpcBinderProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/GrpcBinderProcessor.java index 196701b7b243f..fe5199205e449 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/GrpcBinderProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/GrpcBinderProcessor.java @@ -3,10 +3,10 @@ import java.util.function.BooleanSupplier; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; -import io.quarkus.micrometer.runtime.MicrometerRecorder; import io.quarkus.micrometer.runtime.config.MicrometerConfig; /** @@ -22,14 +22,12 @@ public class GrpcBinderProcessor { static final String CLIENT_INTERCEPTOR = "io.grpc.ClientInterceptor"; static final String SERVER_INTERCEPTOR = "io.grpc.ServerInterceptor"; - static final Class CLIENT_INTERCEPTOR_CLASS = MicrometerRecorder.getClassForName(CLIENT_INTERCEPTOR); - static final Class SERVER_INTERCEPTOR_CLASS = MicrometerRecorder.getClassForName(SERVER_INTERCEPTOR); - static class GrpcClientSupportEnabled implements BooleanSupplier { MicrometerConfig mConfig; public boolean getAsBoolean() { - return CLIENT_INTERCEPTOR_CLASS != null && mConfig.checkBinderEnabledWithDefault(mConfig.binder.grpcClient); + return QuarkusClassLoader.isClassPresentAtRuntime(CLIENT_INTERCEPTOR) + && mConfig.checkBinderEnabledWithDefault(mConfig.binder.grpcClient); } } @@ -37,12 +35,14 @@ static class GrpcServerSupportEnabled implements BooleanSupplier { MicrometerConfig mConfig; public boolean getAsBoolean() { - return SERVER_INTERCEPTOR_CLASS != null && mConfig.checkBinderEnabledWithDefault(mConfig.binder.grpcServer); + return QuarkusClassLoader.isClassPresentAtRuntime(SERVER_INTERCEPTOR) + && mConfig.checkBinderEnabledWithDefault(mConfig.binder.grpcServer); } } @BuildStep(onlyIf = GrpcClientSupportEnabled.class) - AdditionalBeanBuildItem addGrpcClientMetricInterceptor() { + AdditionalBeanBuildItem addGrpcClientMetricInterceptor(BuildProducer producer) { + producer.produce(new AdditionalIndexedClassesBuildItem(CLIENT_GRPC_METRICS_INTERCEPTOR)); return AdditionalBeanBuildItem.unremovableOf(CLIENT_GRPC_METRICS_INTERCEPTOR); } diff --git a/extensions/opentelemetry/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/opentelemetry/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 42767c3d7e5ed..10abb559da59b 100644 --- a/extensions/opentelemetry/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/opentelemetry/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -11,6 +11,6 @@ metadata: guide: "https://quarkus.io/guides/opentelemetry" categories: - "observability" - status: "experimental" + status: "stable" config: - "quarkus.opentelemetry." diff --git a/extensions/panache/hibernate-reactive-panache-kotlin/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/kotlin/deployment/HibernateReactivePanacheKotlinProcessor.java b/extensions/panache/hibernate-reactive-panache-kotlin/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/kotlin/deployment/HibernateReactivePanacheKotlinProcessor.java index 6d4adddff02a3..4e5624b3a3b8c 100644 --- a/extensions/panache/hibernate-reactive-panache-kotlin/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/kotlin/deployment/HibernateReactivePanacheKotlinProcessor.java +++ b/extensions/panache/hibernate-reactive-panache-kotlin/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/kotlin/deployment/HibernateReactivePanacheKotlinProcessor.java @@ -14,9 +14,6 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; -import org.jboss.jandex.MethodInfo; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Type; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.arc.deployment.ValidationPhaseBuildItem; @@ -41,18 +38,12 @@ import io.quarkus.panache.common.deployment.PanacheMethodCustomizerBuildItem; import io.quarkus.panache.common.deployment.PanacheRepositoryEnhancer; import io.quarkus.panache.common.deployment.TypeBundle; -import io.smallrye.mutiny.Multi; -import io.smallrye.mutiny.Uni; public class HibernateReactivePanacheKotlinProcessor { private static final String META_INF_PANACHE_ARCHIVE_MARKER = "META-INF/panache-archive.marker"; private static final DotName DOTNAME_REACTIVE_SESSION = DotName.createSimple(Mutiny.Session.class.getName()); private static final DotName DOTNAME_ID = DotName.createSimple(Id.class.getName()); - private static final DotName DOTNAME_UNI = DotName.createSimple(Uni.class.getName()); - private static final DotName DOTNAME_MULTI = DotName.createSimple(Multi.class.getName()); - private static final String CHECK_RETURN_VALUE_BINARY_NAME = "io/smallrye/common/annotation/CheckReturnValue"; - private static final String CHECK_RETURN_VALUE_SIGNATURE = "L" + CHECK_RETURN_VALUE_BINARY_NAME + ";"; private static final TypeBundle TYPE_BUNDLE = ReactiveKotlinJpaTypeBundle.BUNDLE; @BuildStep @@ -171,17 +162,4 @@ public ValidationPhaseBuildItem.ValidationErrorBuildItem validate(ValidationPhas } return null; } - - @BuildStep - PanacheMethodCustomizerBuildItem mutinyReturnTypes() { - return new PanacheMethodCustomizerBuildItem(new PanacheMethodCustomizer() { - @Override - public void customize(Type entityClassSignature, MethodInfo method, MethodVisitor mv) { - DotName returnType = method.returnType().name(); - if (returnType.equals(DOTNAME_UNI) || returnType.equals(DOTNAME_MULTI)) { - mv.visitAnnotation(CHECK_RETURN_VALUE_SIGNATURE, true); - } - } - }); - } } diff --git a/extensions/panache/hibernate-reactive-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheHibernateResourceProcessor.java b/extensions/panache/hibernate-reactive-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheHibernateResourceProcessor.java index d320b3eb85e8a..75aa2098338bd 100644 --- a/extensions/panache/hibernate-reactive-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheHibernateResourceProcessor.java +++ b/extensions/panache/hibernate-reactive-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheHibernateResourceProcessor.java @@ -38,6 +38,7 @@ import io.quarkus.panache.common.deployment.PanacheJpaEntityOperationsEnhancer; import io.quarkus.panache.common.deployment.PanacheMethodCustomizer; import io.quarkus.panache.common.deployment.PanacheMethodCustomizerBuildItem; +import io.smallrye.common.annotation.CheckReturnValue; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; @@ -145,7 +146,8 @@ ValidationPhaseBuildItem.ValidationErrorBuildItem validate(ValidationPhaseBuildI return null; } - private static final String CHECK_RETURN_VALUE_BINARY_NAME = "io/smallrye/common/annotation/CheckReturnValue"; + private static final DotName DOTNAME_CHECK_RETURN_VALUE_CLASS = DotName.createSimple(CheckReturnValue.class); + private static final String CHECK_RETURN_VALUE_BINARY_NAME = CheckReturnValue.class.getName().replace('.', '/'); private static final String CHECK_RETURN_VALUE_SIGNATURE = "L" + CHECK_RETURN_VALUE_BINARY_NAME + ";"; @BuildStep @@ -154,7 +156,8 @@ PanacheMethodCustomizerBuildItem mutinyReturnTypes() { @Override public void customize(Type entityClassSignature, MethodInfo method, MethodVisitor mv) { DotName returnType = method.returnType().name(); - if (returnType.equals(DOTNAME_UNI) || returnType.equals(DOTNAME_MULTI)) { + if ((returnType.equals(DOTNAME_UNI) || returnType.equals(DOTNAME_MULTI)) + && !method.hasDeclaredAnnotation(DOTNAME_CHECK_RETURN_VALUE_CLASS)) { mv.visitAnnotation(CHECK_RETURN_VALUE_SIGNATURE, true); } } diff --git a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusInvocationContextImpl.java b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusInvocationContextImpl.java new file mode 100644 index 0000000000000..6dc5856db69bf --- /dev/null +++ b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusInvocationContextImpl.java @@ -0,0 +1,198 @@ +package io.quarkus.restclient.runtime; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletionException; + +import jakarta.enterprise.inject.spi.InterceptionType; +import jakarta.enterprise.inject.spi.Interceptor; +import jakarta.interceptor.InvocationContext; +import jakarta.ws.rs.client.ResponseProcessingException; + +import org.jboss.resteasy.microprofile.client.ExceptionMapping; + +import io.quarkus.arc.ArcInvocationContext; + +/** + * A Quarkus copy of {@link org.jboss.resteasy.microprofile.client.InvocationContextImpl} which makes it implement + * {@link ArcInvocationContext} instead so that it's compatible with Quarkus interceptors. + */ +public class QuarkusInvocationContextImpl implements ArcInvocationContext { + + private final Object target; + + private final Method method; + + private Object[] args; + + private final int position; + + private final Map contextData; + + private final List chain; + + private final Set interceptorBindings; + + public QuarkusInvocationContextImpl(final Object target, final Method method, final Object[] args, + final List chain, Set interceptorBindings) { + this(target, method, args, chain, 0, interceptorBindings); + } + + private QuarkusInvocationContextImpl(final Object target, final Method method, final Object[] args, + final List chain, final int position, + Set interceptorBindings) { + this.target = target; + this.method = method; + this.args = args; + this.interceptorBindings = interceptorBindings == null ? Collections.emptySet() : interceptorBindings; + this.contextData = new HashMap<>(); + // put in bindings under Arc's specific key + this.contextData.put(ArcInvocationContext.KEY_INTERCEPTOR_BINDINGS, interceptorBindings); + this.position = position; + this.chain = chain; + } + + boolean hasNextInterceptor() { + return position < chain.size(); + } + + protected Object invokeNext() throws Exception { + return chain.get(position).invoke(nextContext()); + } + + private InvocationContext nextContext() { + return new QuarkusInvocationContextImpl(target, method, args, chain, position + 1, interceptorBindings); + } + + protected Object interceptorChainCompleted() throws Exception { + try { + return method.invoke(target, args); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof CompletionException) { + cause = cause.getCause(); + } + if (cause instanceof ExceptionMapping.HandlerException) { + ((ExceptionMapping.HandlerException) cause).mapException(method); + } + if (cause instanceof ResponseProcessingException) { + ResponseProcessingException rpe = (ResponseProcessingException) cause; + // Note that the default client engine leverages a single connection + // MP FT: we need to close the response otherwise we would not be able to retry if the method returns jakarta.ws.rs.core.Response + rpe.getResponse().close(); + cause = rpe.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + } + throw e; + } + } + + @Override + public Object proceed() throws Exception { + try { + if (hasNextInterceptor()) { + return invokeNext(); + } else { + return interceptorChainCompleted(); + } + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Error) { + throw (Error) cause; + } + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw new RuntimeException(cause); + } + } + + @Override + public Object getTarget() { + return target; + } + + @Override + public Method getMethod() { + return method; + } + + @Override + public Constructor getConstructor() { + return null; + } + + @Override + public Object[] getParameters() throws IllegalStateException { + return args; + } + + @Override + public void setParameters(Object[] params) throws IllegalStateException, IllegalArgumentException { + this.args = params; + } + + @Override + public Map getContextData() { + return contextData; + } + + @Override + public Object getTimer() { + return null; + } + + @Override + public Set getInterceptorBindings() { + return interceptorBindings; + } + + @Override + public T findIterceptorBinding(Class annotationType) { + for (Annotation annotation : getInterceptorBindings()) { + if (annotation.annotationType().equals(annotationType)) { + return (T) annotation; + } + } + return null; + } + + @Override + public List findIterceptorBindings(Class annotationType) { + List found = new ArrayList<>(); + for (Annotation annotation : getInterceptorBindings()) { + if (annotation.annotationType().equals(annotationType)) { + found.add((T) annotation); + } + } + return found; + } + + public static class InterceptorInvocation { + + @SuppressWarnings("rawtypes") + private final Interceptor interceptor; + + private final Object interceptorInstance; + + public InterceptorInvocation(final Interceptor interceptor, final Object interceptorInstance) { + this.interceptor = interceptor; + this.interceptorInstance = interceptorInstance; + } + + @SuppressWarnings("unchecked") + Object invoke(InvocationContext ctx) throws Exception { + return interceptor.intercept(InterceptionType.AROUND_INVOKE, interceptorInstance, ctx); + } + } +} diff --git a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java index 2eba8ce96542c..022f4b53305b3 100644 --- a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java +++ b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java @@ -30,7 +30,6 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.client.jaxrs.ResteasyClient; import org.jboss.resteasy.microprofile.client.ExceptionMapping; -import org.jboss.resteasy.microprofile.client.InvocationContextImpl; import org.jboss.resteasy.microprofile.client.RestClientProxy; import org.jboss.resteasy.microprofile.client.header.ClientHeaderFillingException; @@ -52,7 +51,9 @@ public class QuarkusProxyInvocationHandler implements InvocationHandler { private final Set providerInstances; - private final Map> interceptorChains; + private final Map> interceptorChains; + + private final Map> interceptorBindingsMap; private final ResteasyClient client; @@ -70,10 +71,13 @@ public QuarkusProxyInvocationHandler(final Class restClientInterface, this.closed = new AtomicBoolean(); if (beanManager != null) { this.creationalContext = beanManager.createCreationalContext(null); - this.interceptorChains = initInterceptorChains(beanManager, creationalContext, restClientInterface); + this.interceptorBindingsMap = new HashMap<>(); + this.interceptorChains = initInterceptorChains(beanManager, creationalContext, restClientInterface, + interceptorBindingsMap); } else { this.creationalContext = null; this.interceptorChains = Collections.emptyMap(); + this.interceptorBindingsMap = Collections.emptyMap(); } } @@ -152,10 +156,10 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl args = argsReplacement; } - List chain = interceptorChains.get(method); + List chain = interceptorChains.get(method); if (chain != null) { // Invoke business method interceptors - return new InvocationContextImpl(target, method, args, chain).proceed(); + return new QuarkusInvocationContextImpl(target, method, args, chain, interceptorBindingsMap.get(method)).proceed(); } else { try { return method.invoke(target, args); @@ -245,10 +249,11 @@ private static BeanManager getBeanManager(Class restClientInterface) { } } - private static Map> initInterceptorChains( - BeanManager beanManager, CreationalContext creationalContext, Class restClientInterface) { + private static Map> initInterceptorChains( + BeanManager beanManager, CreationalContext creationalContext, Class restClientInterface, + Map> interceptorBindingsMap) { - Map> chains = new HashMap<>(); + Map> chains = new HashMap<>(); // Interceptor as a key in a map is not entirely correct (custom interceptors) but should work in most cases Map, Object> interceptorInstances = new HashMap<>(); @@ -267,12 +272,13 @@ private static Map> in List> interceptors = beanManager.resolveInterceptors(InterceptionType.AROUND_INVOKE, interceptorBindings); if (!interceptors.isEmpty()) { - List chain = new ArrayList<>(); + List chain = new ArrayList<>(); for (Interceptor interceptor : interceptors) { - chain.add(new InvocationContextImpl.InterceptorInvocation(interceptor, + chain.add(new QuarkusInvocationContextImpl.InterceptorInvocation(interceptor, interceptorInstances.computeIfAbsent(interceptor, i -> beanManager.getReference(i, i.getBeanClass(), creationalContext)))); } + interceptorBindingsMap.put(method, Set.of(interceptorBindings)); chains.put(method, chain); } } diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index 6ee601bf8e874..4cecf9feab2e9 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -7,10 +7,13 @@ import static org.jboss.jandex.Type.Kind.PRIMITIVE; import static org.jboss.resteasy.reactive.client.impl.RestClientRequestContext.DEFAULT_CONTENT_TYPE_PROP; import static org.jboss.resteasy.reactive.common.processor.EndpointIndexer.extractProducesConsumesValues; +import static org.jboss.resteasy.reactive.common.processor.JandexUtil.*; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COLLECTION; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COMPLETION_STAGE; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.CONSUMES; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.ENCODED; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.FORM_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MAP; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OBJECT; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PART_TYPE_NAME; @@ -2744,28 +2747,11 @@ private void addQueryParamToWebTarget(BytecodeCreator creator, ResultHandle para } private boolean isCollection(Type type, IndexView index) { - if (type.kind() == Type.Kind.PRIMITIVE) { - return false; - } - ClassInfo classInfo = index.getClassByName(type.name()); - if (classInfo == null) { - return false; - } - return classInfo.interfaceNames().stream().anyMatch(DotName.createSimple(Collection.class.getName())::equals); + return isAssignableFrom(COLLECTION, type.name(), index); } private boolean isMap(Type type, IndexView index) { - if (type.kind() == Type.Kind.PRIMITIVE) { - return false; - } - ClassInfo classInfo = index.getClassByName(type.name()); - if (classInfo == null) { - return false; - } - if (ResteasyReactiveDotNames.MAP.equals(classInfo.name())) { - return true; - } - return classInfo.interfaceNames().stream().anyMatch(DotName.createSimple(Map.class.getName())::equals); + return isAssignableFrom(MAP, type.name(), index); } private void addHeaderParam(BytecodeCreator invoBuilderEnricher, AssignableResultHandle invocationBuilder, diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomObjectMapperTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomObjectMapperTest.java index 99e66a254a9fa..a3d5c8a84382e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomObjectMapperTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomObjectMapperTest.java @@ -1,11 +1,14 @@ package io.quarkus.resteasy.reactive.jackson.deployment.test; import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; import static org.hamcrest.CoreMatchers.equalTo; import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.MediaType; @@ -18,6 +21,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import io.quarkus.arc.Unremovable; import io.quarkus.test.QuarkusUnitTest; @@ -32,26 +36,99 @@ public class CustomObjectMapperTest { * `objectMapper.enable(SerializationFeature.WRAP_ROOT_VALUE);` */ @Test - void serverShouldUnwrapRootElement() { - given().body("{\"Request\":{\"value\":\"good\"}}") + void test() { + given().body("{\"Request\":{\"value\":\"FIRST\"}}") .contentType(ContentType.JSON) - .post("/server") + .post("/server/dummy") .then() .statusCode(HttpStatus.SC_OK) - .body(equalTo("good")); + .body(equalTo("0")); + + // ContextResolver was invoked for both reader and writer + when().get("/server/count") + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("2")); + + given().body("{\"Request2\":{\"value\":\"FIRST\"}}") + .contentType(ContentType.JSON) + .post("/server/dummy2") + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("0")); + + // ContextResolver was invoked for both reader and writer because different types where used + when().get("/server/count") + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("4")); + + given().body("{\"Request\":{\"value\":\"FIRST\"}}") + .contentType(ContentType.JSON) + .post("/server/dummy") + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("0")); + + // ContextResolver was not invoked because the types have already been cached + when().get("/server/count") + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("4")); + + given().body("{\"Request2\":{\"value\":\"FIRST\"}}") + .contentType(ContentType.JSON) + .post("/server/dummy2") + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("0")); + + // ContextResolver was not invoked because the types have already been cached + when().get("/server/count") + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("4")); + } + + private static void doTest() { + } @Path("/server") public static class MyResource { @POST @Consumes(MediaType.APPLICATION_JSON) - public String post(Request request) { - return request.value; + @Path("dummy") + public Dummy dummy(Request request) { + return Dummy.valueOf(request.value); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Path("dummy2") + public Dummy2 dummy2(Request2 request) { + return Dummy2.valueOf(request.value); + } + + @GET + @Path("count") + public long count() { + return CustomObjectMapperContextResolver.COUNT.get(); } } + public enum Dummy { + FIRST, + SECOND + } + + public enum Dummy2 { + FIRST, + SECOND + } + public static class Request { - private String value; + protected String value; public Request() { @@ -85,14 +162,21 @@ public int hashCode() { } } + public static class Request2 extends Request { + } + @Provider @Unremovable public static class CustomObjectMapperContextResolver implements ContextResolver { + static final AtomicLong COUNT = new AtomicLong(); + @Override public ObjectMapper getContext(final Class type) { + COUNT.incrementAndGet(); final ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.enable(DeserializationFeature.UNWRAP_ROOT_VALUE); + objectMapper.enable(DeserializationFeature.UNWRAP_ROOT_VALUE) + .enable(SerializationFeature.WRITE_ENUMS_USING_INDEX); return objectMapper; } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyReader.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyReader.java index 89dbe47c6d36a..863d2b396f877 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyReader.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyReader.java @@ -43,7 +43,8 @@ public class FullyFeaturedServerJacksonMessageBodyReader extends JacksonBasicMes private final Providers providers; private final ConcurrentMap perMethodReader = new ConcurrentHashMap<>(); private final ConcurrentMap perTypeReader = new ConcurrentHashMap<>(); - private final ConcurrentMap contextResolverMap = new ConcurrentHashMap<>(); + private final ConcurrentMap, ObjectMapper> contextResolverMap = new ConcurrentHashMap<>(); + private final ConcurrentMap objectReaderMap = new ConcurrentHashMap<>(); @Inject public FullyFeaturedServerJacksonMessageBodyReader(ObjectMapper mapper, Providers providers) { @@ -154,7 +155,7 @@ private ObjectReader getEffectiveReader(Class type, Type genericType, Me ObjectReader effectiveReader = defaultReader; if (effectiveMapper != originalMapper) { // Effective reader based on the context - effectiveReader = contextResolverMap.computeIfAbsent(effectiveMapper, new Function<>() { + effectiveReader = objectReaderMap.computeIfAbsent(effectiveMapper, new Function<>() { @Override public ObjectReader apply(ObjectMapper objectMapper) { return objectMapper.reader(); @@ -201,7 +202,16 @@ private ObjectMapper getEffectiveMapper(Class type, MediaType responseMe contextResolver = providers.getContextResolver(ObjectMapper.class, null); } if (contextResolver != null) { - return contextResolver.getContext(type); + var cr = contextResolver; + ObjectMapper result = contextResolverMap.computeIfAbsent(type, new Function<>() { + @Override + public ObjectMapper apply(Class aClass) { + return cr.getContext(type); + } + }); + if (result != null) { + return result; + } } return originalMapper; diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java index 171b6843fa62d..df6601849601f 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java @@ -38,7 +38,8 @@ public class FullyFeaturedServerJacksonMessageBodyWriter extends ServerMessageBo private final ObjectWriter defaultWriter; private final ConcurrentMap perMethodWriter = new ConcurrentHashMap<>(); private final ConcurrentMap perTypeWriter = new ConcurrentHashMap<>(); - private final ConcurrentMap contextResolverMap = new ConcurrentHashMap<>(); + private final ConcurrentMap, ObjectMapper> contextResolverMap = new ConcurrentHashMap<>(); + private final ConcurrentMap objectWriterMap = new ConcurrentHashMap<>(); @Inject public FullyFeaturedServerJacksonMessageBodyWriter(ObjectMapper mapper, Providers providers) { @@ -112,7 +113,7 @@ private ObjectWriter getEffectiveWriter(ObjectMapper effectiveMapper) { if (effectiveMapper == originalMapper) { return defaultWriter; } - return contextResolverMap.computeIfAbsent(effectiveMapper, new Function<>() { + return objectWriterMap.computeIfAbsent(effectiveMapper, new Function<>() { @Override public ObjectWriter apply(ObjectMapper objectMapper) { return createDefaultWriter(effectiveMapper); @@ -133,7 +134,13 @@ private ObjectMapper getEffectiveMapper(Object o, ServerRequestContext context) contextResolver = providers.getContextResolver(ObjectMapper.class, null); } if (contextResolver != null) { - ObjectMapper mapperFromContextResolver = contextResolver.getContext(o.getClass()); + var cr = contextResolver; + ObjectMapper mapperFromContextResolver = contextResolverMap.computeIfAbsent(o.getClass(), new Function<>() { + @Override + public ObjectMapper apply(Class aClass) { + return cr.getContext(o.getClass()); + } + }); if (mapperFromContextResolver != null) { effectiveMapper = mapperFromContextResolver; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java index 9c4ddafd41688..2a991448e49df 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java @@ -1,5 +1,7 @@ package io.quarkus.resteasy.reactive.links.deployment; +import static org.jboss.resteasy.reactive.common.processor.JandexUtil.isAssignableFrom; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COLLECTION; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COMPLETABLE_FUTURE; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COMPLETION_STAGE; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI; @@ -17,8 +19,6 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; -import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.ParameterizedType; @@ -138,14 +138,7 @@ private String getAnnotationValue(AnnotationInstance annotationInstance, String } private boolean isCollection(Type type, IndexView index) { - if (type.kind() == Type.Kind.PRIMITIVE) { - return false; - } - ClassInfo classInfo = index.getClassByName(type.name()); - if (classInfo == null) { - return false; - } - return classInfo.interfaceNames().stream().anyMatch(DotName.createSimple(Collection.class.getName())::equals); + return isAssignableFrom(COLLECTION, type.name(), index); } private Type getNonAsyncReturnType(Type returnType) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 63a1959712a48..c52f28315e392 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -523,17 +523,19 @@ public void accept(EndpointIndexer.ResourceMethodCallbackEntry entry) { .constructors(false).methods().build()); } - reflectiveHierarchy.produce(new ReflectiveHierarchyBuildItem.Builder() - .type(method.returnType()) - .index(index) - .ignoreTypePredicate( - QuarkusResteasyReactiveDotNames.IGNORE_TYPE_FOR_REFLECTION_PREDICATE) - .ignoreFieldPredicate( - QuarkusResteasyReactiveDotNames.IGNORE_FIELD_FOR_REFLECTION_PREDICATE) - .ignoreMethodPredicate( - QuarkusResteasyReactiveDotNames.IGNORE_METHOD_FOR_REFLECTION_PREDICATE) - .source(source) - .build()); + if (!result.getPossibleSubResources().containsKey(method.returnType().name())) { + reflectiveHierarchy.produce(new ReflectiveHierarchyBuildItem.Builder() + .type(method.returnType()) + .index(index) + .ignoreTypePredicate( + QuarkusResteasyReactiveDotNames.IGNORE_TYPE_FOR_REFLECTION_PREDICATE) + .ignoreFieldPredicate( + QuarkusResteasyReactiveDotNames.IGNORE_FIELD_FOR_REFLECTION_PREDICATE) + .ignoreMethodPredicate( + QuarkusResteasyReactiveDotNames.IGNORE_METHOD_FOR_REFLECTION_PREDICATE) + .source(source) + .build()); + } boolean paramsRequireReflection = false; for (short i = 0; i < method.parametersCount(); i++) { diff --git a/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/DifferentObjectMapperForClientAndServerTest.java b/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/DifferentObjectMapperForClientAndServerTest.java index dc444fdd5d9ff..90f3de332246c 100644 --- a/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/DifferentObjectMapperForClientAndServerTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/DifferentObjectMapperForClientAndServerTest.java @@ -3,13 +3,11 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.URI; import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import jakarta.inject.Singleton; import jakarta.ws.rs.GET; @@ -70,10 +68,17 @@ void serverShouldWrapRootElement() { */ @Test void shouldClientUseCustomObjectMapperUnwrappingRootElement() { - assertFalse(ClientObjectMapperUnwrappingRootElement.USED.get()); + AtomicLong count = ClientObjectMapperUnwrappingRootElement.COUNT; + assertEquals(0, count.get()); Request request = clientUnwrappingRootElement.get(); assertEquals("good", request.value); - assertTrue(ClientObjectMapperUnwrappingRootElement.USED.get()); + assertEquals(1, count.get()); + + assertEquals("good", clientUnwrappingRootElement.get().value); + assertEquals("good", clientUnwrappingRootElement.get().value); + assertEquals("good", clientUnwrappingRootElement.get().value); + // count should not change as the resolution of the ObjectMapper should be cached + assertEquals(1, count.get()); } /** @@ -82,10 +87,17 @@ void shouldClientUseCustomObjectMapperUnwrappingRootElement() { */ @Test void shouldClientUseCustomObjectMapperNotUnwrappingRootElement() { - assertFalse(MyClientNotUnwrappingRootElement.CUSTOM_OBJECT_MAPPER_USED.get()); + AtomicLong count = MyClientNotUnwrappingRootElement.CUSTOM_OBJECT_MAPPER_COUNT; + assertEquals(0, count.get()); Request request = clientNotUnwrappingRootElement.get(); assertNull(request.value); - assertTrue(MyClientNotUnwrappingRootElement.CUSTOM_OBJECT_MAPPER_USED.get()); + assertEquals(1, count.get()); + + assertNull(clientNotUnwrappingRootElement.get().value); + assertNull(clientNotUnwrappingRootElement.get().value); + assertNull(clientNotUnwrappingRootElement.get().value); + // count should not change as the resolution of the ObjectMapper should be cached + assertEquals(1, count.get()); } @Path("/server") @@ -108,14 +120,14 @@ public interface MyClientUnwrappingRootElement { @Path("/server") @Produces(MediaType.APPLICATION_JSON) public interface MyClientNotUnwrappingRootElement { - AtomicBoolean CUSTOM_OBJECT_MAPPER_USED = new AtomicBoolean(false); + AtomicLong CUSTOM_OBJECT_MAPPER_COUNT = new AtomicLong(); @GET Request get(); @ClientObjectMapper static ObjectMapper objectMapper(ObjectMapper defaultObjectMapper) { - CUSTOM_OBJECT_MAPPER_USED.set(true); + CUSTOM_OBJECT_MAPPER_COUNT.incrementAndGet(); return defaultObjectMapper.copy() .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .disable(DeserializationFeature.UNWRAP_ROOT_VALUE); @@ -158,11 +170,11 @@ public int hashCode() { } public static class ClientObjectMapperUnwrappingRootElement implements ContextResolver { - static final AtomicBoolean USED = new AtomicBoolean(false); + static final AtomicLong COUNT = new AtomicLong(); @Override public ObjectMapper getContext(Class type) { - USED.set(true); + COUNT.incrementAndGet(); return new ObjectMapper().enable(DeserializationFeature.UNWRAP_ROOT_VALUE); } } diff --git a/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java b/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java index ad7e481b8405b..63c4fb8cec20c 100644 --- a/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java +++ b/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java @@ -1,5 +1,7 @@ package io.quarkus.rest.client.reactive.jackson.runtime.serialisers; +import static io.quarkus.rest.client.reactive.jackson.runtime.serialisers.JacksonUtil.getObjectMapperFromContext; + import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Annotation; @@ -13,8 +15,6 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.ext.ContextResolver; -import jakarta.ws.rs.ext.Providers; import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.ClientWebApplicationException; @@ -33,7 +33,8 @@ public class ClientJacksonMessageBodyReader extends JacksonBasicMessageBodyReade private static final Logger log = Logger.getLogger(ClientJacksonMessageBodyReader.class); - private final ConcurrentMap contextResolverMap = new ConcurrentHashMap<>(); + private final ConcurrentMap contextResolverMap = new ConcurrentHashMap<>(); + private final ConcurrentMap objectReaderMap = new ConcurrentHashMap<>(); private RestClientRequestContext context; @Inject @@ -66,43 +67,16 @@ public void handle(RestClientRequestContext requestContext) { } private ObjectReader getEffectiveReader(Class type, MediaType responseMediaType) { - ObjectMapper effectiveMapper = getObjectMapperFromContext(type, responseMediaType); + ObjectMapper effectiveMapper = getObjectMapperFromContext(type, responseMediaType, context, contextResolverMap); if (effectiveMapper == null) { return getEffectiveReader(); } - return contextResolverMap.computeIfAbsent(effectiveMapper, new Function<>() { + return objectReaderMap.computeIfAbsent(effectiveMapper, new Function<>() { @Override public ObjectReader apply(ObjectMapper objectMapper) { return objectMapper.reader(); } }); } - - private ObjectMapper getObjectMapperFromContext(Class type, MediaType responseMediaType) { - Providers providers = getProviders(); - if (providers == null) { - return null; - } - - ContextResolver contextResolver = providers.getContextResolver(ObjectMapper.class, - responseMediaType); - if (contextResolver == null) { - // TODO: not sure if this is correct, but Jackson does this as well... - contextResolver = providers.getContextResolver(ObjectMapper.class, null); - } - if (contextResolver != null) { - return contextResolver.getContext(type); - } - - return null; - } - - private Providers getProviders() { - if (context != null && context.getClientRequestContext() != null) { - return context.getClientRequestContext().getProviders(); - } - - return null; - } } diff --git a/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyWriter.java b/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyWriter.java index 98a52d39391fe..9c71047af4a28 100644 --- a/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyWriter.java +++ b/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyWriter.java @@ -1,5 +1,6 @@ package io.quarkus.rest.client.reactive.jackson.runtime.serialisers; +import static io.quarkus.rest.client.reactive.jackson.runtime.serialisers.JacksonUtil.getObjectMapperFromContext; import static org.jboss.resteasy.reactive.server.jackson.JacksonMessageBodyWriterUtil.createDefaultWriter; import static org.jboss.resteasy.reactive.server.jackson.JacksonMessageBodyWriterUtil.doLegacyWrite; @@ -27,7 +28,8 @@ public class ClientJacksonMessageBodyWriter implements MessageBodyWriter protected final ObjectMapper originalMapper; protected final ObjectWriter defaultWriter; - private final ConcurrentMap contextResolverMap = new ConcurrentHashMap<>(); + private final ConcurrentMap contextResolverMap = new ConcurrentHashMap<>(); + private final ConcurrentMap objectWriterMap = new ConcurrentHashMap<>(); private RestClientRequestContext context; @Inject @@ -44,7 +46,7 @@ public boolean isWriteable(Class type, Type genericType, Annotation[] annotation @Override public void writeTo(Object o, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { - doLegacyWrite(o, annotations, httpHeaders, entityStream, getEffectiveWriter()); + doLegacyWrite(o, annotations, httpHeaders, entityStream, getEffectiveWriter(type, mediaType)); } @Override @@ -52,22 +54,18 @@ public void handle(RestClientRequestContext requestContext) throws Exception { this.context = requestContext; } - protected ObjectWriter getEffectiveWriter() { - if (context == null) { - // no context injected when writer is not running within a rest client context - return defaultWriter; - } - - ObjectMapper objectMapper = context.getConfiguration().getFromContext(ObjectMapper.class); + protected ObjectWriter getEffectiveWriter(Class type, MediaType responseMediaType) { + ObjectMapper objectMapper = getObjectMapperFromContext(type, responseMediaType, context, contextResolverMap); if (objectMapper == null) { return defaultWriter; } - return contextResolverMap.computeIfAbsent(objectMapper, new Function<>() { + return objectWriterMap.computeIfAbsent(objectMapper, new Function<>() { @Override public ObjectWriter apply(ObjectMapper objectMapper) { return createDefaultWriter(objectMapper); } }); } + } diff --git a/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/JacksonUtil.java b/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/JacksonUtil.java new file mode 100644 index 0000000000000..bb8c217f4ac35 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/JacksonUtil.java @@ -0,0 +1,53 @@ +package io.quarkus.rest.client.reactive.jackson.runtime.serialisers; + +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.ws.rs.ext.Providers; + +import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; + +import com.fasterxml.jackson.databind.ObjectMapper; + +final class JacksonUtil { + + private JacksonUtil() { + } + + static ObjectMapper getObjectMapperFromContext(Class type, MediaType responseMediaType, RestClientRequestContext context, + ConcurrentMap contextResolverMap) { + Providers providers = getProviders(context); + if (providers == null) { + return null; + } + + ContextResolver contextResolver = providers.getContextResolver(ObjectMapper.class, + responseMediaType); + if (contextResolver == null) { + // TODO: not sure if this is correct, but Jackson does this as well... + contextResolver = providers.getContextResolver(ObjectMapper.class, null); + } + if (contextResolver != null) { + var cr = contextResolver; + var key = new ResolverMapKey(type, context.getConfiguration(), context.getInvokedMethod().getDeclaringClass()); + return contextResolverMap.computeIfAbsent(key, new Function<>() { + @Override + public ObjectMapper apply(ResolverMapKey resolverMapKey) { + return cr.getContext(resolverMapKey.getType()); + } + }); + } + + return null; + } + + private static Providers getProviders(RestClientRequestContext context) { + if (context != null && context.getClientRequestContext() != null) { + return context.getClientRequestContext().getProviders(); + } + + return null; + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ResolverMapKey.java b/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ResolverMapKey.java new file mode 100644 index 0000000000000..02b8148f06b40 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ResolverMapKey.java @@ -0,0 +1,53 @@ +package io.quarkus.rest.client.reactive.jackson.runtime.serialisers; + +import java.util.Objects; + +import jakarta.ws.rs.core.Configuration; + +/** + * Each REST Client can potentially have different providers, so we need to make sure that + * caching for one client does not affect caching of another + */ +public final class ResolverMapKey { + + private final Class type; + private final Configuration configuration; + + private final Class restClientClass; + + public ResolverMapKey(Class type, Configuration configuration, Class restClientClass) { + this.type = type; + this.configuration = configuration; + this.restClientClass = restClientClass; + } + + public Class getType() { + return type; + } + + public Configuration getConfiguration() { + return configuration; + } + + public Class getRestClientClass() { + return restClientClass; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ResolverMapKey)) { + return false; + } + ResolverMapKey that = (ResolverMapKey) o; + return Objects.equals(type, that.type) && Objects.equals(configuration, that.configuration) + && Objects.equals(restClientClass, that.restClientClass); + } + + @Override + public int hashCode() { + return Objects.hash(type, configuration, restClientClass); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceScanner.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceScanner.java index 943d1725f9028..bca9ef3b1e576 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceScanner.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceScanner.java @@ -80,7 +80,7 @@ boolean hasFTAnnotations(ClassInfo clazz) { void forEachMethod(ClassInfo clazz, Consumer action) { for (MethodInfo method : clazz.methods()) { if (method.name().startsWith("<")) { - // constructors (or static init blocks) can't be intercepted + // constructors and static inititalizers can't be intercepted continue; } if (method.isSynthetic()) { diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java index 805878809ebce..345c8808dc85f 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java @@ -24,7 +24,6 @@ import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; -import org.jboss.jandex.MethodInfo; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; @@ -279,6 +278,7 @@ void processFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder, List ftMethods = new ArrayList<>(); List exceptions = new ArrayList<>(); + Map> existingCircuitBreakerNames = new HashMap<>(); for (BeanInfo info : validationPhase.getContext().beans()) { ClassInfo beanClass = info.getImplClazz(); @@ -319,6 +319,12 @@ void processFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder, reflectiveClass.produce(ReflectiveClassBuildItem.builder(exceptionNames.get()).build()); } } + + if (annotationStore.hasAnnotation(method, DotNames.CIRCUIT_BREAKER_NAME)) { + AnnotationInstance ann = annotationStore.getAnnotation(method, DotNames.CIRCUIT_BREAKER_NAME); + existingCircuitBreakerNames.computeIfAbsent(ann.value().asString(), ignored -> new HashSet<>()) + .add(method + " @ " + method.declaringClass()); + } } }); @@ -337,16 +343,6 @@ void processFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder, recorder.createFaultToleranceOperation(ftMethods); - // since annotation transformations are applied lazily, we can't know - // all transformed `@CircuitBreakerName`s and have to rely on Jandex here - Map> existingCircuitBreakerNames = new HashMap<>(); - for (AnnotationInstance it : index.getAnnotations(DotNames.CIRCUIT_BREAKER_NAME)) { - if (it.target().kind() == Kind.METHOD) { - MethodInfo method = it.target().asMethod(); - existingCircuitBreakerNames.computeIfAbsent(it.value().asString(), ignored -> new HashSet<>()) - .add(method + " @ " + method.declaringClass()); - } - } for (Map.Entry> entry : existingCircuitBreakerNames.entrySet()) { if (entry.getValue().size() > 1) { exceptions.add(new DefinitionException("Multiple circuit breakers have the same name '" diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/CircuitBreakerService1.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/CircuitBreakerService1.java similarity index 94% rename from extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/CircuitBreakerService1.java rename to extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/CircuitBreakerService1.java index 687f50da09b0b..884c337c7ef26 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/CircuitBreakerService1.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/CircuitBreakerService1.java @@ -1,4 +1,4 @@ -package io.quarkus.smallrye.faulttolerance.test.circuitbreaker.maintenance.inheritance; +package io.quarkus.smallrye.faulttolerance.test.circuitbreaker.maintenance.duplicate; import jakarta.inject.Singleton; diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/CircuitBreakerService2.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/CircuitBreakerService2.java similarity index 94% rename from extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/CircuitBreakerService2.java rename to extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/CircuitBreakerService2.java index 94d7be6f34af0..6aa742b9b324d 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/CircuitBreakerService2.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/CircuitBreakerService2.java @@ -1,4 +1,4 @@ -package io.quarkus.smallrye.faulttolerance.test.circuitbreaker.maintenance.inheritance; +package io.quarkus.smallrye.faulttolerance.test.circuitbreaker.maintenance.duplicate; import jakarta.inject.Singleton; diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/DuplicateCircuitBreakerNameTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/DuplicateCircuitBreakerNameTest.java similarity index 97% rename from extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/DuplicateCircuitBreakerNameTest.java rename to extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/DuplicateCircuitBreakerNameTest.java index c5bb50252ed96..bbc57be5a8726 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/DuplicateCircuitBreakerNameTest.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/DuplicateCircuitBreakerNameTest.java @@ -1,4 +1,4 @@ -package io.quarkus.smallrye.faulttolerance.test.circuitbreaker.maintenance.inheritance; +package io.quarkus.smallrye.faulttolerance.test.circuitbreaker.maintenance.duplicate; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/CircuitBreakerNameInheritanceTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/CircuitBreakerNameInheritanceTest.java similarity index 96% rename from extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/CircuitBreakerNameInheritanceTest.java rename to extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/CircuitBreakerNameInheritanceTest.java index 160e0d396ba18..d5fdf1a4a724e 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/CircuitBreakerNameInheritanceTest.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/CircuitBreakerNameInheritanceTest.java @@ -1,4 +1,4 @@ -package io.quarkus.smallrye.faulttolerance.test.circuitbreaker.maintenance.duplicate; +package io.quarkus.smallrye.faulttolerance.test.circuitbreaker.maintenance.inheritance; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/SubCircuitBreakerService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/SubCircuitBreakerService.java similarity index 93% rename from extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/SubCircuitBreakerService.java rename to extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/SubCircuitBreakerService.java index b2f26bbc454a1..49e1c9c9bc742 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/SubCircuitBreakerService.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/SubCircuitBreakerService.java @@ -1,4 +1,4 @@ -package io.quarkus.smallrye.faulttolerance.test.circuitbreaker.maintenance.duplicate; +package io.quarkus.smallrye.faulttolerance.test.circuitbreaker.maintenance.inheritance; import jakarta.inject.Singleton; diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/SuperCircuitBreakerService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/SuperCircuitBreakerService.java similarity index 94% rename from extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/SuperCircuitBreakerService.java rename to extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/SuperCircuitBreakerService.java index 0a87d76f87b20..685b09931e0aa 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/duplicate/SuperCircuitBreakerService.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/inheritance/SuperCircuitBreakerService.java @@ -1,4 +1,4 @@ -package io.quarkus.smallrye.faulttolerance.test.circuitbreaker.maintenance.duplicate; +package io.quarkus.smallrye.faulttolerance.test.circuitbreaker.maintenance.inheritance; import jakarta.inject.Singleton; diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/noduplicate/CircuitBreakerService1.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/noduplicate/CircuitBreakerService1.java new file mode 100644 index 0000000000000..78431399ba5ca --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/noduplicate/CircuitBreakerService1.java @@ -0,0 +1,16 @@ +package io.quarkus.smallrye.faulttolerance.test.circuitbreaker.maintenance.noduplicate; + +import jakarta.inject.Singleton; + +import org.eclipse.microprofile.faulttolerance.CircuitBreaker; + +import io.smallrye.faulttolerance.api.CircuitBreakerName; + +@Singleton +public class CircuitBreakerService1 { + @CircuitBreaker + @CircuitBreakerName("hello") + public String hello() { + return "1"; + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/noduplicate/CircuitBreakerService2.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/noduplicate/CircuitBreakerService2.java new file mode 100644 index 0000000000000..5a0bdd28ffdc6 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/noduplicate/CircuitBreakerService2.java @@ -0,0 +1,14 @@ +package io.quarkus.smallrye.faulttolerance.test.circuitbreaker.maintenance.noduplicate; + +import org.eclipse.microprofile.faulttolerance.CircuitBreaker; + +import io.smallrye.faulttolerance.api.CircuitBreakerName; + +public class CircuitBreakerService2 { + // this class is not a bean, so there's no circuit breaker and hence no duplicate circuit breaker name + @CircuitBreaker + @CircuitBreakerName("hello") + public String hello() { + return "2"; + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/noduplicate/NoDuplicateCircuitBreakerNameTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/noduplicate/NoDuplicateCircuitBreakerNameTest.java new file mode 100644 index 0000000000000..f513b7cbdbd0c --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/circuitbreaker/maintenance/noduplicate/NoDuplicateCircuitBreakerNameTest.java @@ -0,0 +1,29 @@ +package io.quarkus.smallrye.faulttolerance.test.circuitbreaker.maintenance.noduplicate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.faulttolerance.api.CircuitBreakerMaintenance; +import io.smallrye.faulttolerance.api.CircuitBreakerState; + +public class NoDuplicateCircuitBreakerNameTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(CircuitBreakerService1.class, CircuitBreakerService2.class)); + + @Inject + CircuitBreakerMaintenance cb; + + @Test + public void deploysWithoutError() { + assertNotNull(cb); + assertEquals(CircuitBreakerState.CLOSED, cb.currentState("hello")); + } +} diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js index 86dbf99328516..82c85eb85863b 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js @@ -105,7 +105,7 @@ export class QwcContinuousTesting extends QwcHotReloadElement { this._state = JSON.parse(jsonRpcResponse.result); }); this._streamResultsObserver = this.jsonRpc.streamTestResults().onNext(jsonRpcResponse => { - this._results = JSON.parse(jsonRpcResponse.result); + this._results = jsonRpcResponse.result; }); } @@ -121,7 +121,7 @@ export class QwcContinuousTesting extends QwcHotReloadElement { }); this.jsonRpc.lastKnownResults().then(jsonRpcResponse => { if(jsonRpcResponse.result){ - this._results = JSON.parse(jsonRpcResponse.result); + this._results = jsonRpcResponse.result; } }); } diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/JandexUtil.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/JandexUtil.java index 8eb2a5d725bf9..2c8f8c37833b3 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/JandexUtil.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/JandexUtil.java @@ -5,8 +5,10 @@ import java.lang.reflect.Modifier; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -415,4 +417,42 @@ public static boolean isImplementorOf(IndexView index, ClassInfo info, DotName n return isImplementorOf(index, superClass, name, additionalIgnoredSuperClasses); } + public static boolean isAssignableFrom(DotName superType, DotName subType, IndexView index) { + // java.lang.Object is assignable from any type + if (superType.equals(DOTNAME_OBJECT)) { + return true; + } + // type1 is the same as type2 + if (superType.equals(subType)) { + return true; + } + // type1 is a superclass + return findSupertypes(subType, index).contains(superType); + } + + private static Set findSupertypes(DotName name, IndexView index) { + Set result = new HashSet<>(); + + Deque workQueue = new ArrayDeque<>(); + workQueue.add(name); + while (!workQueue.isEmpty()) { + DotName type = workQueue.poll(); + if (result.contains(type)) { + continue; + } + result.add(type); + + ClassInfo clazz = index.getClassByName(type); + if (clazz == null) { + continue; + } + if (clazz.superName() != null) { + workQueue.add(clazz.superName()); + } + workQueue.addAll(clazz.interfaceNames()); + } + + return result; + } + } diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/headers/MediaTypeHeaderDelegate.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/headers/MediaTypeHeaderDelegate.java index 88977f12023af..ed26840ce0c67 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/headers/MediaTypeHeaderDelegate.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/headers/MediaTypeHeaderDelegate.java @@ -80,6 +80,9 @@ private static MediaType internalParse(String type) { } else { major = type.substring(0, typeIndex); if (paramIndex > -1) { + if (typeIndex + 1 > paramIndex) { + throw new IllegalArgumentException("Failed to parse media type " + type); + } subtype = type.substring(typeIndex + 1, paramIndex); } else { subtype = type.substring(typeIndex + 1); diff --git a/independent-projects/resteasy-reactive/common/runtime/src/test/java/org/jboss/resteasy/reactive/common/headers/MediaTypeHeaderDelegateTest.java b/independent-projects/resteasy-reactive/common/runtime/src/test/java/org/jboss/resteasy/reactive/common/headers/MediaTypeHeaderDelegateTest.java new file mode 100644 index 0000000000000..4141af5ff6c26 --- /dev/null +++ b/independent-projects/resteasy-reactive/common/runtime/src/test/java/org/jboss/resteasy/reactive/common/headers/MediaTypeHeaderDelegateTest.java @@ -0,0 +1,21 @@ +package org.jboss.resteasy.reactive.common.headers; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class MediaTypeHeaderDelegateTest { + + public void parsingBrokenMediaTypeShouldThrowIllegalArgumentException_minimized() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + MediaTypeHeaderDelegate.parse("x; /x"); + }); + } + + @Test + public void parsingBrokenMediaTypeShouldThrowIllegalArgumentException_actual() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + MediaTypeHeaderDelegate.parse("() { ::}; echo \"NS:\" $(/bin/sh -c \"expr 123456 - 123456\")"); + }); + } + +} \ No newline at end of file diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/PublisherResponseHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/PublisherResponseHandler.java index a6aa28fd1e632..4d097feaad874 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/PublisherResponseHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/PublisherResponseHandler.java @@ -308,14 +308,14 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti produces = REST_MULTI_DEFAULT_SERVER_MEDIA_TYPE; } else { throw new IllegalStateException( - "Negotiation or dynamic media type not supported yet for Multi: please use the @Produces annotation when returning a Multi"); + "Negotiation or dynamic media type resolution for Multi is only supported when using 'org.jboss.resteasy.reactive.RestMulti'"); } } MediaType[] mediaTypes = produces.getSortedOriginalMediaTypes(); if (mediaTypes.length != 1) { throw new IllegalStateException( - "Negotiation or dynamic media type not supported yet for Multi: please use a single @Produces annotation"); + "Negotiation or dynamic media type resolution for Multi is only supported when using 'org.jboss.resteasy.reactive.RestMulti'"); } MediaType mediaType = mediaTypes[0]; diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResponseHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResponseHandler.java index 62b7fc3ae5a49..25116ff3d8f69 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResponseHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResponseHandler.java @@ -146,13 +146,13 @@ public Response get() { if (result instanceof GenericEntity) { GenericEntity genericEntity = (GenericEntity) result; requestContext.setGenericReturnType(genericEntity.getType()); - responseBuilder = (ResponseBuilderImpl) ResponseImpl.ok(genericEntity.getEntity()); + responseBuilder = ResponseBuilderImpl.ok(genericEntity.getEntity()); } else if (result == null) { // FIXME: custom status codes depending on method? - responseBuilder = (ResponseBuilderImpl) ResponseImpl.noContent(); + responseBuilder = ResponseBuilderImpl.noContent(); } else { // FIXME: custom status codes depending on method? - responseBuilder = (ResponseBuilderImpl) ResponseImpl.ok(result); + responseBuilder = ResponseBuilderImpl.ok(result); } if (responseBuilder.getEntity() != null) { EncodedMediaType produces = requestContext.getResponseContentType(); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/ResponseBuilderImpl.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/ResponseBuilderImpl.java index 208c1093d4fd8..f1a546ff5c846 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/ResponseBuilderImpl.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/ResponseBuilderImpl.java @@ -96,19 +96,19 @@ protected AbstractResponseBuilder doClone() { //TODO: add the rest of static methods of Response if we need them - public static Response.ResponseBuilder withStatus(Response.Status status) { - return new ResponseBuilderImpl().status(status); + public static ResponseBuilderImpl withStatus(Response.Status status) { + return (ResponseBuilderImpl) new ResponseBuilderImpl().status(status); } - public static Response.ResponseBuilder ok() { + public static ResponseBuilderImpl ok() { return withStatus(Response.Status.OK); } - public static Response.ResponseBuilder ok(Object entity) { - return ok().entity(entity); + public static ResponseBuilderImpl ok(Object entity) { + return (ResponseBuilderImpl) ok().entity(entity); } - public static Response.ResponseBuilder noContent() { + public static ResponseBuilderImpl noContent() { return withStatus(Response.Status.NO_CONTENT); } } diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/JBangCatalogService.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/JBangCatalogService.java index c28ce62985983..08cc81e1ce4df 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/JBangCatalogService.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/JBangCatalogService.java @@ -1,6 +1,7 @@ package io.quarkus.cli.plugin; import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -22,6 +23,7 @@ public class JBangCatalogService extends CatalogService { private final String fallbackCatalog; private final String[] remoteCatalogs; private final JBangSupport jbang; + private final MessageWriter output; public JBangCatalogService(MessageWriter output) { this(output, "quarkus-", "quarkusio"); @@ -38,6 +40,7 @@ public JBangCatalogService(boolean interactiveMode, MessageWriter output, String this.fallbackCatalog = fallbackCatalog; this.remoteCatalogs = remoteCatalogs; this.jbang = new JBangSupport(interactiveMode, output); + this.output = output; } @Override @@ -88,7 +91,12 @@ public JBangCatalog readCombinedCatalog(Optional projectDir, Optional catalogFile = projectDir .map(d -> RELATIVE_PLUGIN_CATALOG.apply(d).toAbsolutePath().toString()); catalogFile.ifPresent(f -> { - List lines = jbang.execute("alias", "list", "-f", f, "--verbose"); + List lines = new ArrayList<>(); + try { + lines.addAll(jbang.execute("alias", "list", "-f", f, "--verbose")); + } catch (Exception e) { + output.debug("Failed to read catalog file: " + f + ". Ignoring."); + } aliases.putAll(readAliases(lines)); }); }); @@ -111,14 +119,25 @@ public JBangCatalog readCombinedCatalog(Optional projectDir, Optional listAliases(JBangSupport jbang, String remoteCatalog) { - List lines = jbang.execute("alias", "list", "--verbose", remoteCatalog); + List lines = new ArrayList<>(); + try { + lines.addAll(jbang.execute("alias", "list", "--verbose", remoteCatalog)); + } catch (Exception e) { + this.output.debug("Failed to list aliases from remote catalog: " + remoteCatalog + ". Ignorning."); + } + return readAliases(lines); } private Map listAliasesOrFallback(JBangSupport jbang, String fallbackCatalog) { - List localCatalogs = jbang.execute("catalog", "list").stream() - .map(l -> l.substring(0, l.indexOf(" "))) - .collect(Collectors.toList()); + List localCatalogs = new ArrayList<>(); + try { + for (String catalog : jbang.execute("catalog", "list")) { + localCatalogs.add(catalog.substring(0, catalog.indexOf(" "))); + } + } catch (Exception e) { + this.output.debug("Failed to list jbang catalogs. Ignoring."); + } //If there are locally installed catalogs, then go through every single one of them //and collect the aliases. diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginManager.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginManager.java index e3c4d1dd54e93..41032580ce5e4 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginManager.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginManager.java @@ -97,10 +97,11 @@ public Optional addPlugin(String nameOrLocation, boolean userCatalog, Op Map installablePlugins = state.installablePlugins(); Optional plugin = Optional.ofNullable(installablePlugins.get(name)).map(Plugin::inUserCatalog); return plugin.map(p -> { - PluginCatalog updatedCatalog = state.pluginCatalog(userCatalog).addPlugin(p); + Plugin withDescription = p.withDescription(description); + PluginCatalog updatedCatalog = state.pluginCatalog(userCatalog).addPlugin(withDescription); pluginCatalogService.writeCatalog(updatedCatalog); state.invalidateInstalledPlugins(); - return p; + return withDescription; }); } diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java index edff9ab037a05..7652b34063ba2 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java @@ -54,7 +54,7 @@ public String toString() { } // ordering is important here, so let's keep them ordered - public static final SortedSet JAVA_VERSIONS_LTS = new TreeSet<>(List.of(11, 17)); + public static final SortedSet JAVA_VERSIONS_LTS = new TreeSet<>(List.of(11, 17, 21)); public static final int DEFAULT_JAVA_VERSION = 11; public static final int MAX_LTS_SUPPORTED_BY_KOTLIN = 17; public static final String DETECT_JAVA_RUNTIME_VERSION = "<>"; diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitBase.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitBase.java new file mode 100644 index 0000000000000..cdf9968b0341b --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitBase.java @@ -0,0 +1,90 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.batch.v1.Job; +import io.fabric8.kubernetes.api.model.rbac.RoleBinding; + +public class KubernetesWithFlywayInitBase { + + public void assertGeneratedResources(Path kubernetesDir, String name, String imagePullSecret, String serviceAccount) + throws IOException { + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml")); + List kubernetesList = DeserializationUtil.deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); + + Optional deployment = kubernetesList.stream() + .filter(d -> "Deployment".equals(d.getKind()) + && name.equals(d.getMetadata().getName())) + .map(d -> (Deployment) d).findAny(); + + assertTrue(deployment.isPresent()); + assertThat(deployment).satisfies(j -> j.isPresent()); + assertThat(deployment.get()).satisfies(d -> { + assertThat(d.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo(name); + }); + + assertThat(d.getSpec()).satisfies(deploymentSpec -> { + assertThat(deploymentSpec.getTemplate()).satisfies(t -> { + assertThat(t.getSpec()).satisfies(podSpec -> { + assertThat(podSpec.getImagePullSecrets()).singleElement() + .satisfies(s -> assertThat(s.getName()).isEqualTo(imagePullSecret)); + assertThat(podSpec.getServiceAccountName()).isEqualTo(serviceAccount); + assertThat(podSpec.getInitContainers()).singleElement().satisfies(container -> { + assertThat(container.getName()).isEqualTo("init"); + assertThat(container.getImage()).isEqualTo("groundnuty/k8s-wait-for:no-root-v1.7"); + }); + + }); + }); + }); + }); + + Optional job = kubernetesList.stream() + .filter(j -> "Job".equals(j.getKind()) && (name + "-flyway-init").equals(j.getMetadata().getName())) + .map(j -> (Job) j) + .findAny(); + assertTrue(job.isPresent()); + + assertThat(job.get()).satisfies(j -> { + assertThat(j.getSpec()).satisfies(jobSpec -> { + assertThat(jobSpec.getCompletionMode()).isEqualTo("NonIndexed"); + assertThat(jobSpec.getTemplate()).satisfies(t -> { + assertThat(t.getSpec()).satisfies(podSpec -> { + assertThat(podSpec.getImagePullSecrets()).singleElement() + .satisfies(s -> assertThat(s.getName()).isEqualTo(imagePullSecret)); + assertThat(podSpec.getServiceAccountName()).isEqualTo(serviceAccount); + assertThat(podSpec.getRestartPolicy()).isEqualTo("OnFailure"); + assertThat(podSpec.getContainers()).singleElement().satisfies(container -> { + assertThat(container.getName()).isEqualTo(name + "-flyway-init"); + assertThat(container.getEnv()).filteredOn(env -> "QUARKUS_FLYWAY_ENABLED".equals(env.getName())) + .singleElement().satisfies(env -> { + assertThat(env.getValue()).isEqualTo("true"); + }); + assertThat(container.getEnv()) + .filteredOn(env -> "QUARKUS_INIT_AND_EXIT".equals(env.getName())).singleElement() + .satisfies(env -> { + assertThat(env.getValue()).isEqualTo("true"); + }); + }); + }); + }); + }); + }); + + Optional roleBinding = kubernetesList.stream().filter( + r -> r instanceof RoleBinding && (name + "-view-jobs").equals(r.getMetadata().getName())) + .map(r -> (RoleBinding) r).findFirst(); + assertTrue(roleBinding.isPresent()); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitTest.java index fb7fe0aba6d61..06607fd7469be 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitTest.java @@ -1,30 +1,21 @@ package io.quarkus.it.kubernetes; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.IOException; import java.nio.file.Path; import java.util.Arrays; -import java.util.List; -import java.util.Optional; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.batch.v1.Job; -import io.fabric8.kubernetes.api.model.rbac.RoleBinding; import io.quarkus.bootstrap.model.AppArtifact; import io.quarkus.builder.Version; import io.quarkus.test.ProdBuildResults; import io.quarkus.test.ProdModeTestResults; import io.quarkus.test.QuarkusProdModeTest; -public class KubernetesWithFlywayInitTest { +public class KubernetesWithFlywayInitTest extends KubernetesWithFlywayInitBase { private static final String NAME = "kubernetes-with-flyway"; private static final String IMAGE_PULL_SECRET = "my-pull-secret"; @@ -46,74 +37,6 @@ public class KubernetesWithFlywayInitTest { @Test public void assertGeneratedResources() throws IOException { final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); - assertThat(kubernetesDir) - .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.json")) - .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml")); - List kubernetesList = DeserializationUtil - .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); - - Optional deployment = kubernetesList.stream() - .filter(d -> "Deployment".equals(d.getKind()) - && NAME.equals(d.getMetadata().getName())) - .map(d -> (Deployment) d).findAny(); - - assertTrue(deployment.isPresent()); - assertThat(deployment).satisfies(j -> j.isPresent()); - assertThat(deployment.get()).satisfies(d -> { - assertThat(d.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo(NAME); - }); - - assertThat(d.getSpec()).satisfies(deploymentSpec -> { - assertThat(deploymentSpec.getTemplate()).satisfies(t -> { - assertThat(t.getSpec()).satisfies(podSpec -> { - assertThat(podSpec.getImagePullSecrets()).singleElement() - .satisfies(s -> assertThat(s.getName()).isEqualTo(IMAGE_PULL_SECRET)); - assertThat(podSpec.getServiceAccountName()).isEqualTo(NAME); - assertThat(podSpec.getInitContainers()).singleElement().satisfies(container -> { - assertThat(container.getName()).isEqualTo("init"); - assertThat(container.getImage()).isEqualTo("groundnuty/k8s-wait-for:no-root-v1.7"); - }); - - }); - }); - }); - }); - - Optional job = kubernetesList.stream() - .filter(j -> "Job".equals(j.getKind()) && (NAME + "-flyway-init").equals(j.getMetadata().getName())) - .map(j -> (Job) j) - .findAny(); - assertTrue(job.isPresent()); - - assertThat(job.get()).satisfies(j -> { - assertThat(j.getSpec()).satisfies(jobSpec -> { - assertThat(jobSpec.getCompletionMode()).isEqualTo("NonIndexed"); - assertThat(jobSpec.getTemplate()).satisfies(t -> { - assertThat(t.getSpec()).satisfies(podSpec -> { - assertThat(podSpec.getImagePullSecrets()).singleElement() - .satisfies(s -> assertThat(s.getName()).isEqualTo(IMAGE_PULL_SECRET)); - assertThat(podSpec.getRestartPolicy()).isEqualTo("OnFailure"); - assertThat(podSpec.getContainers()).singleElement().satisfies(container -> { - assertThat(container.getName()).isEqualTo(NAME + "-flyway-init"); - assertThat(container.getEnv()).filteredOn(env -> "QUARKUS_FLYWAY_ENABLED".equals(env.getName())) - .singleElement().satisfies(env -> { - assertThat(env.getValue()).isEqualTo("true"); - }); - assertThat(container.getEnv()) - .filteredOn(env -> "QUARKUS_INIT_AND_EXIT".equals(env.getName())).singleElement() - .satisfies(env -> { - assertThat(env.getValue()).isEqualTo("true"); - }); - }); - }); - }); - }); - }); - - Optional roleBinding = kubernetesList.stream().filter( - r -> r instanceof RoleBinding && (NAME + "-view-jobs").equals(r.getMetadata().getName())) - .map(r -> (RoleBinding) r).findFirst(); - assertTrue(roleBinding.isPresent()); + assertGeneratedResources(kubernetesDir, NAME, IMAGE_PULL_SECRET, NAME); } } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayIntAndCustomServiceAccountTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayIntAndCustomServiceAccountTest.java new file mode 100644 index 0000000000000..34197f6b040c1 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayIntAndCustomServiceAccountTest.java @@ -0,0 +1,44 @@ +package io.quarkus.it.kubernetes; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.builder.Version; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithFlywayIntAndCustomServiceAccountTest extends KubernetesWithFlywayInitBase { + + private static final String NAME = "kubernetes-with-flyway"; + private static final String IMAGE_PULL_SECRET = "my-pull-secret"; + private static final String SERVICE_ACCOUNT = "my-service-account"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) + .setApplicationName(NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .setLogFileName("k8s.log") + .overrideConfigKey("quarkus.kubernetes.image-pull-secrets", IMAGE_PULL_SECRET) + .overrideConfigKey("quarkus.kubernetes.service-account", SERVICE_ACCOUNT) + .setForcedDependencies(Arrays.asList( + new AppArtifact("io.quarkus", "quarkus-kubernetes", Version.getVersion()), + new AppArtifact("io.quarkus", "quarkus-flyway", Version.getVersion()))); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertGeneratedResources(kubernetesDir, NAME, IMAGE_PULL_SECRET, SERVICE_ACCOUNT); + } +} diff --git a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/PingPongResource.java b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/PingPongResource.java index 4d02a03af17e8..8593eb7981e17 100644 --- a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/PingPongResource.java +++ b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/PingPongResource.java @@ -10,6 +10,8 @@ import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import org.eclipse.microprofile.rest.client.inject.RestClient; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import io.smallrye.common.annotation.Blocking; import io.smallrye.mutiny.Uni; import io.vertx.core.MultiMap; @@ -34,6 +36,11 @@ public interface PingPongRestClient { @GET @Path("/client/pong/{message}") Uni asyncPingpong(@PathParam("message") String message); + + @GET + @Path("/client/pong/{message}") + @WithSpan + String pingpongIntercept(@SpanAttribute(value = "message") @PathParam("message") String message); } @Inject @@ -81,4 +88,9 @@ public Uni asyncPingNamed(@PathParam("message") String message) { .onItemOrFailure().call(httpClient::close); } + @GET + @Path("pong-intercept/{message}") + public String pongIntercept(@PathParam("message") String message) { + return pingRestClient.pingpongIntercept(message); + } } diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTest.java index 1e970fc6a583f..b0a1f43fa33ca 100644 --- a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTest.java +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTest.java @@ -15,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -557,6 +558,79 @@ void testAsyncClientTracing() { assertNotNull(clientServer.get("attr_user_agent.original")); } + @Test + void testClientTracingWithInterceptor() { + given() + .when().get("/client/pong-intercept/one") + .then() + .statusCode(200) + .body(containsString("one")); + + await().atMost(5, SECONDS).until(() -> getSpans().size() == 4); + List> spans = getSpans(); + assertEquals(4, spans.size()); + assertEquals(1, spans.stream().map(map -> map.get("traceId")).collect(toSet()).size()); + + Map server = getSpanByKindAndParentId(spans, SERVER, "0000000000000000"); + assertEquals(SERVER.toString(), server.get("kind")); + verifyResource(server); + assertEquals("GET /client/pong-intercept/{message}", server.get("name")); + assertEquals(SERVER.toString(), server.get("kind")); + assertTrue((Boolean) server.get("ended")); + assertEquals(SpanId.getInvalid(), server.get("parent_spanId")); + assertEquals(TraceId.getInvalid(), server.get("parent_traceId")); + assertFalse((Boolean) server.get("parent_valid")); + assertFalse((Boolean) server.get("parent_remote")); + assertEquals("GET", server.get("attr_http.method")); + assertEquals("/client/pong-intercept/one", server.get("attr_http.target")); + assertEquals(pathParamUrl.getHost(), server.get("attr_net.host.name")); + assertEquals(pathParamUrl.getPort(), Integer.valueOf((String) server.get("attr_net.host.port"))); + assertEquals("http", server.get("attr_http.scheme")); + assertEquals("/client/pong-intercept/{message}", server.get("attr_http.route")); + assertEquals("200", server.get("attr_http.status_code")); + assertNotNull(server.get("attr_http.client_ip")); + assertNotNull(server.get("attr_user_agent.original")); + + Map fromInterceptor = getSpanByKindAndParentId(spans, INTERNAL, server.get("spanId")); + assertEquals("PingPongRestClient.pingpongIntercept", fromInterceptor.get("name")); + assertEquals(INTERNAL.toString(), fromInterceptor.get("kind")); + assertTrue((Boolean) fromInterceptor.get("ended")); + assertTrue((Boolean) fromInterceptor.get("parent_valid")); + assertFalse((Boolean) fromInterceptor.get("parent_remote")); + assertNull(fromInterceptor.get("attr_http.method")); + assertNull(fromInterceptor.get("attr_http.status_code")); + assertEquals("one", fromInterceptor.get("attr_message")); + + Map client = getSpanByKindAndParentId(spans, CLIENT, fromInterceptor.get("spanId")); + assertEquals("GET", client.get("name")); + assertEquals(SpanKind.CLIENT.toString(), client.get("kind")); + assertTrue((Boolean) client.get("ended")); + assertTrue((Boolean) client.get("parent_valid")); + assertFalse((Boolean) client.get("parent_remote")); + assertEquals("GET", client.get("attr_http.method")); + assertEquals("http://localhost:8081/client/pong/one", client.get("attr_http.url")); + assertEquals("200", client.get("attr_http.status_code")); + + Map clientServer = getSpanByKindAndParentId(spans, SERVER, client.get("spanId")); + assertEquals(SERVER.toString(), clientServer.get("kind")); + verifyResource(clientServer); + assertEquals("GET /client/pong/{message}", clientServer.get("name")); + assertEquals(SERVER.toString(), clientServer.get("kind")); + assertTrue((Boolean) clientServer.get("ended")); + assertTrue((Boolean) clientServer.get("parent_valid")); + assertTrue((Boolean) clientServer.get("parent_remote")); + assertEquals("GET", clientServer.get("attr_http.method")); + assertEquals("/client/pong/one", clientServer.get("attr_http.target")); + assertEquals(pathParamUrl.getHost(), server.get("attr_net.host.name")); + assertEquals(pathParamUrl.getPort(), Integer.valueOf((String) server.get("attr_net.host.port"))); + assertEquals("http", clientServer.get("attr_http.scheme")); + assertEquals("/client/pong/{message}", clientServer.get("attr_http.route")); + assertEquals("200", clientServer.get("attr_http.status_code")); + assertNotNull(clientServer.get("attr_http.client_ip")); + assertNotNull(clientServer.get("attr_user_agent.original")); + assertEquals(clientServer.get("parentSpanId"), client.get("spanId")); + } + @Test void testTemplatedPathOnClass() { given()