diff --git a/.github/quarkus-github-bot.yml b/.github/quarkus-github-bot.yml index e259862b13356..6156a023b828e 100644 --- a/.github/quarkus-github-bot.yml +++ b/.github/quarkus-github-bot.yml @@ -30,24 +30,28 @@ triage: discussions: monitoredCategories: [33575230] rules: - - labels: [area/amazon-lambda] + - id: amazon-lambda + labels: [area/amazon-lambda] title: "lambda" notify: [patriot1burke, matejvasek] directories: - extensions/amazon-lambda - integration-tests/amazon-lambda - - labels: [area/persistence] + - id: db2 + labels: [area/persistence] title: "db2" notify: [mswatosh] directories: - extensions/reactive-db2-client/ - extensions/jdbc/jdbc-db2/ - - labels: [area/funqy] + - id: funqy + labels: [area/funqy] titleBody: "funqy" notify: [patriot1burke, matejvasek] directories: - extensions/funqy/ - - labels: [area/devmode] + - id: devmode + labels: [area/devmode] title: "dev mode" - labels: [area/gradle] title: "gradle" @@ -55,7 +59,8 @@ triage: directories: - devtools/gradle/ - integration-tests/gradle/ - - labels: [area/maven] + - id: maven + labels: [area/maven] title: "maven" notify: [quarkusio/devtools] directories: @@ -64,13 +69,15 @@ triage: - independent-projects/bootstrap/maven-plugin/ - integration-tests/maven/ - test-framework/maven/ - - labels: [area/jbang] + - id: jbang + labels: [area/jbang] titleBody: "jbang" notify: [quarkusio/devtools, maxandersen] directories: - "**/*JBang*" - "**/*jbang*" - - labels: [area/codestarts] + - id: codestart + labels: [area/codestarts] title: "codestart" notify: [quarkusio/devtools, ia3andy] directories: @@ -78,7 +85,8 @@ triage: - devtools/platform-descriptor-json/src/main/resources/bundled-codestarts/ - devtools/platform-descriptor-json/src/main/resources/codestarts/ - devtools/platform-descriptor-json/src/main/resources/templates/ - - labels: [area/hibernate-reactive, area/persistence] + - id: hibernate-reactive + labels: [area/hibernate-reactive, area/persistence] title: "hibernate.reactive" expression: | matches("hibernate", title) && matches("reactive", title) @@ -87,7 +95,8 @@ triage: notify: [DavideD, gavinking, Sanne] directories: - extensions/hibernate-reactive - - labels: [area/hibernate-orm, area/persistence] + - id: hibernate-orm + labels: [area/hibernate-orm, area/persistence] expression: | matches("hibernate", title) && !matches("reactive", title) && !matches("hibernate.validator", title) @@ -104,7 +113,8 @@ triage: - integration-tests/hibernate-envers - integration-tests/hibernate-orm-tenancy - integration-tests/infinispan-cache-jpa - - labels: [area/hibernate-search] + - id: hibernate-search + labels: [area/hibernate-search] title: "hibernate.search" notify: [gsmet, yrodiere] notifyInPullRequest: true @@ -112,7 +122,8 @@ triage: # No trailing slashes: we also match sibling directories starting with these names - extensions/hibernate-search - integration-tests/hibernate-search - - labels: [area/elasticsearch] + - id: elasticsearch + labels: [area/elasticsearch] title: "elasticsearch" notify: [gsmet, yrodiere, loicmathieu] notifyInPullRequest: true @@ -120,31 +131,36 @@ triage: # No trailing slashes: we also match sibling directories starting with these names - extensions/elasticsearch - integration-tests/elasticsearch - - labels: [area/hibernate-validator] + - id: hibernate-validator + labels: [area/hibernate-validator] title: "hibernate.validator" notify: [gsmet, yrodiere] directories: # No trailing slashes: we also match sibling directories starting with these names - extensions/hibernate-validator - integration-tests/hibernate-validator - - labels: [area/jaeger] + - id: jaeger + labels: [area/jaeger] title: "jaeger" notify: [radcortez, brunobat] directories: - extensions/jaeger/ - - labels: [area/jackson] + - id: jackson + labels: [area/jackson] title: "jackson" notify: [geoand, gsmet] directories: - extensions/jackson/ - integration-tests/jackson/ - - labels: [area/kotlin] + - id: kotlin + labels: [area/kotlin] titleBody: "kotlin" notify: [evanchooly, geoand] directories: - extensions/kotlin/ - integration-tests/kotlin/ - - labels: [area/mongodb] + - id: mongodb + labels: [area/mongodb] title: "mongo" notify: [loicmathieu, evanchooly] directories: @@ -152,12 +168,14 @@ triage: - integration-tests/mongodb-client/ - integration-tests/mongodb-panache/ - extensions/panache/mongodb-panache/ - - labels: [area/openapi, area/smallrye] + - id: openapi + labels: [area/openapi, area/smallrye] title: "openapi" notify: [EricWittmann, MikeEdgar, phillip-kruger] directories: - extensions/smallrye-openapi - - labels: [area/graphql, area/smallrye] + - id: graphql + labels: [area/graphql, area/smallrye] title: "graphql" notify: [phillip-kruger, jmartisk] directories: @@ -167,47 +185,60 @@ triage: - integration-tests/vertx-graphql/ - integration-tests/smallrye-graphql/ - integration-tests/smallrye-graphql-client/ - - labels: [area/tracing, area/smallrye] + - id: tracing + labels: [area/tracing, area/smallrye] title: "(trace|tracing)" notify: [radcortez, Ladicek, brunobat] directories: - extensions/smallrye-opentracing/ - - labels: [area/tracing] + - id: opentelemetry + labels: [area/tracing] title: "(trace|opentelemetry)" notify: [radcortez, brunobat] notifyInPullRequest: true directories: - extensions/opentelemetry/ - extensions/opentelemetry-exporter-jaeger/ - - labels: [area/security, area/smallrye] + - id: jwt + labels: [area/security, area/smallrye] title: "jwt" notify: [sberyozkin] - - labels: [area/security] + - id: security + labels: [area/security] title: "security" notify: [sberyozkin] - - labels: [area/metrics, area/smallrye] + directories: + - extensions/security/ + - extensions/elytron + - integration-tests/elytron + - id: metrics + labels: [area/metrics, area/smallrye] title: "metrics" notify: [jmartisk, ebullient] notifyInPullRequest: true directories: - extensions/smallrye-metrics - - labels: [area/metrics] + - id: micrometer + labels: [area/metrics] title: "micrometer" notify: [ebullient] notifyInPullRequest: true directories: - extensions/micrometer - - labels: [area/health, area/smallrye] + - id: health + labels: [area/health, area/smallrye] title: "health" notify: [jmartisk, xstefank] directories: - extensions/smallrye-health/ - - labels: [area/fault-tolerance, area/smallrye] + - id: fault-tolerance + labels: [area/fault-tolerance, area/smallrye] title: "fault.tolerance" notify: [Ladicek] directories: - extensions/smallrye-fault-tolerance/ - - labels: [area/mutiny, area/smallrye] + - id: mutiny + labels: [area/mutiny, area/smallrye] title: "mutiny" notify: [cescoffier, jponge] directories: @@ -215,48 +246,58 @@ triage: - extensions/rest-client-mutiny/ - extensions/resteasy-mutiny-common/ - extensions/resteasy-mutiny/ - - labels: [area/panache] + - id: panache + labels: [area/panache] title: "panache" notify: [FroMage, loicmathieu] directories: - extensions/panache/ - integration-tests/hibernate-orm-panache/ - - labels: [area/panache] + - id: rest-data-panache + labels: [area/panache] title: "(panache.*resource|rest.data.panache)" notify: [Sgitario] - - labels: [area/qute] + - id: qute + labels: [area/qute] title: "qute" notify: [mkouba] directories: - extensions/qute/ - extensions/resteasy-qute/ - - labels: [area/reactive-messaging, area/smallrye] + - id: reactive-messaging + labels: [area/reactive-messaging, area/smallrye] title: "reactive.messaging" notify: [cescoffier, ozangunalp] directories: - extensions/smallrye-reactive-messaging - - labels: [area/rest-client] + - id: rest-client + labels: [area/rest-client] title: "rest.client" notify: [cescoffier, geoand, Sgitario] directories: - extensions/rest-client/ - - labels: [area/smallrye] + - id: smallrye + labels: [area/smallrye] title: "smallrye" notify: [phillip-kruger, jmartisk, radcortez, Ladicek] directories: - extensions/smallrye- - - labels: [area/spring] + - id: spring + labels: [area/spring] title: "spring" notify: [geoand] directories: - extensions/spring- - integration-tests/spring- - - labels: [env/windows] + - id: windows + labels: [env/windows] titleBody: "windows" - - labels: [env/m1] + - id: m1 + labels: [env/m1] titleBody: "\\bm1\\b" notify: [gastaldi] - - labels: [area/kubernetes] + - id: kubernetes + labels: [area/kubernetes] titleBody: "kubernetes" notify: [geoand, iocanel, Sgitario] directories: @@ -264,76 +305,91 @@ triage: - extensions/kubernetes-client/ - integration-tests/kubernetes/ - integration-tests/kubernetes-client/ - - labels: [area/kubernetes] + - id: minikube + labels: [area/kubernetes] titleBody: "minikube" notify: [geoand] - - labels: [area/kubernetes] + - id: openshift + labels: [area/kubernetes] titleBody: "openshift" notify: [geoand, iocanel] - - labels: [area/kubernetes] + - id: knative + labels: [area/kubernetes] titleBody: "knative" notify: [geoand, iocanel] - - labels: [area/container-image] + - id: jib + labels: [area/container-image] titleBody: "jib" notify: [geoand] - - labels: [area/kafka] + - id: kafka + labels: [area/kafka] notify: [cescoffier, ozangunalp, alesj] title: "kafka" directories: - extensions/kafka-client/ - integration-tests/kafka/ - - labels: [area/kafka-streams] + - id: kafka-streams + labels: [area/kafka-streams] title: "k(afka)?(\\s|-)?stream" notify: [gunnarmorling, rquinio, alesj, ozangunalp] directories: - extensions/kafka-streams - integration-tests/kafka-streams/ - - labels: [area/infinispan] + - id: infinispan + labels: [area/infinispan] title: "infinispan" notify: [karesti, wburns] directories: - extensions/infinispan-client/ - integration-tests/infinispan-cache-jpa/ - integration-tests/infinispan-client/ - - labels: [area/grpc] + - id: grpc + labels: [area/grpc] title: "grpc" notify: [alesj, cescoffier] directories: - extensions/grpc-common - extensions/grpc - - labels: [area/scheduler] + - id: scheduler + labels: [area/scheduler] title: "schedule(r)?" notify: [mkouba] directories: - extensions/scheduler/ - - labels: [area/scheduler] + - id: quartz + labels: [area/scheduler] title: "quartz" notify: [mkouba, machi1990] directories: - extensions/quartz/ - integration-tests/quartz/ - - labels: [area/redis] + - id: redis + labels: [area/redis] title: "redis" notify: [machi1990, gsmet, cescoffier] directories: - extensions/redis-client/ - integration-tests/redis-client/ - - labels: [area/arc] + - id: arc + labels: [area/arc] title: "\\b(arc|cdi|injection)\\b" notify: [mkouba, manovotn] directories: - extensions/arc/ - independent-projects/arc/ - - labels: [area/google-cloud-functions] + - id: google-cloud + labels: [area/google-cloud-functions] title: "google.cloud" notify: [loicmathieu] directories: - extensions/google-cloud-functions - integration-tests/google-cloud-functions - - labels: [area/mandrel] + - id: mandrel + labels: [area/mandrel] titleBody: "mandrel" notify: [galderz, zakkak, Karm] - - labels: [area/graphics] + - id: awt + labels: [area/graphics] expression: | matches("sun.font", titleBody) || matches("sun.java2d", titleBody) @@ -345,7 +401,8 @@ triage: - extensions/awt/ - integration-tests/awt/ - integration-tests/no-awt/ - - labels: [area/securepipeline] + - id: securepipeline + labels: [area/securepipeline] expression: | matches("FIPS", titleBody) || matches("SunPKCS11-NSS-FIPS", titleBody) @@ -353,63 +410,75 @@ triage: || matches("libnss", titleBody) notify: [jerboaa, Karm] notifyInPullRequest: true - - labels: [area/artemis] + - id: artemis + labels: [area/artemis] directories: - extensions/artemis-core/ - extensions/artemis-jms/ - integration-tests/artemis-core/ - integration-tests/artemis-jms/ - - labels: [area/cache] + - id: cache + labels: [area/cache] title: "cache" notify: [gwenneg] directories: - extensions/cache/ - integration-tests/cache/ - - labels: [area/cli] + - id: cli + labels: [area/cli] title: "\\b(cli)\\b" directories: - devtools/cli/ notify: [maxandersen,ebullient] - - labels: [area/picocli] + - id: picocli + labels: [area/picocli] title: "picocli" directories: - extensions/picocli/ - integration-tests/picocli/ - integration-tests/picocli-native/ notify: [ebullient] - - labels: [area/config] + - id: config + labels: [area/config] directories: - extensions/config-yaml/ - core/deployment/src/main/java/io/quarkus/deployment/configuration/ - core/runtime/src/main/java/io/quarkus/runtime/configuration/ - - labels: [area/core] + - id: core + labels: [area/core] notify: [aloubyansky, gsmet, geoand, radcortez, Sanne, stuartwdouglas] directories: - core/ - - labels: [area/dependencies] + - id: dependencies + labels: [area/dependencies] directories: - .github/dependabot.yml - bom/ - build-parent/ allowSecondPass: true - - labels: [area/devtools] + - id: devtools + labels: [area/devtools] directories: - devtools/ - independent-projects/bootstrap/ - independent-projects/tools/ - - labels: [area/documentation] + - id: documentation + labels: [area/documentation] notify: [ebullient, inoxx03, sunayna15, michelle-purcell, hmanwani-rh, sheilamjones, MichalMaler] directories: - docs/ - - labels: [area/infra-automation] + - id: infra-automation + labels: [area/infra-automation] directories: - .github/ - - labels: [area/jaxb] + - id: jaxb + labels: [area/jaxb] title: "jaxb" notify: [gsmet, Sgitario] directories: - extensions/jaxb/ - - labels: [area/logging] + - id: logging + labels: [area/logging] directories: - extensions/logging-gelf/ - extensions/logging-json/ @@ -417,45 +486,53 @@ triage: - integration-tests/logging-gelf/ - core/runtime/src/main/java/io/quarkus/runtime/logging/ - core/deployment/src/main/java/io/quarkus/logging/ - - labels: [area/narayana] + - id: narayana + labels: [area/narayana] directories: - extensions/narayana-jta/ - extensions/narayana-stm/ - integration-tests/narayana-jta/ - integration-tests/narayana-stm/ notify: [mmusgrov] - - labels: [area/lra] + - id: lra + labels: [area/lra] title: "lra" directories: - extensions/narayana-lra/ - integration-tests/narayana-lra/ notify: [xstefank, mmusgrov] - - labels: [area/neo4j] + - id: neo4j + labels: [area/neo4j] title: "neo4j" notify: [michael-simons] directories: - extensions/neo4j/ - integration-tests/neo4j/ - - labels: [area/oidc] + - id: oidc + labels: [area/oidc] title: "oidc" notify: [sberyozkin, pedroigor] directories: - extensions/oidc/ - integration-tests/oidc/ - integration-tests/oidc-code-flow/ - - labels: [area/keycloak] + - id: keycloak + labels: [area/keycloak] title: "keycloak" notify: [sberyozkin, pedroigor] directories: - extensions/keycloak - integration-tests/keycloak - - labels: [area/platform] + - id: platform + labels: [area/platform] directories: - independent-projects/tools/ - - labels: [area/reactive-streams-operators] + - id: reactive-streams-operators + labels: [area/reactive-streams-operators] directories: - extensions/reactive-streams-operators/ - - labels: [area/resteasy] + - id: resteasy + labels: [area/resteasy] directories: - extensions/resteasy/ - extensions/resteasy-common/ @@ -467,94 +544,107 @@ triage: - integration-tests/resteasy-jackson/ - integration-tests/elytron-resteasy/ - integration-tests/virtual-http-resteasy/ - - labels: [area/resteasy-reactive] + - id: resteasy-reactive + labels: [area/resteasy-reactive] title: resteasy.reactive notify: [geoand, FroMage, stuartwdouglas] directories: - extensions/resteasy-reactive/ - - labels: [area/scala] + - id: scala + labels: [area/scala] directories: - extensions/scala/ - integration-tests/scala/ - - labels: [area/vault] + - id: vault + labels: [area/vault] title: vault notify: [vsevel] directories: - extensions/vault/ - integration-tests/vault - - labels: [area/vertx] + - id: vertx + labels: [area/vertx] directories: - extensions/vertx - integration-tests/vertx - - labels: [area/tika] + - id: tika + labels: [area/tika] title: tika notify: [sberyozkin] directories: - extensions/tika/ - integration-tests/tika/ - - labels: [area/testing] + - id: testing + title: "quarkusintegrationtest" + labels: [area/testing] + notify: [geoand] directories: - test-framework/ - - labels: [area/undertow] + - id: undertow + labels: [area/undertow] directories: - extensions/undertow/ - integration-tests/elytron-undertow/ - - labels: [area/websockets] + - id: websockets + labels: [area/websockets] directories: - extensions/websockets/ - integration-tests/websockets/ - - labels: [area/swagger-ui] + - id: swagger-ui + labels: [area/swagger-ui] title: "swagger" notify: [phillip-kruger, MikeEdgar] directories: - extensions/swagger-ui/ - - labels: [area/security] - directories: - - extensions/security/ - - extensions/elytron - - integration-tests/elytron - - labels: [area/flyway] + - id: flyway + labels: [area/flyway] title: "flyway" notify: [cristhiank, geoand, gastaldi, gsmet] directories: - extensions/flyway/ - integration-tests/flyway/ - - labels: [area/liquibase] + - id: liquibase + labels: [area/liquibase] title: "liquibase" notify: [andrejpetras, geoand, gsmet] directories: - extensions/liquibase/ - integration-tests/liquibase/ - - labels: [area/kogito] + - id: kogito + labels: [area/kogito] title: "kogito" notify: [evacchi, mariofusco] - - labels: [area/optaplanner] + - id: optaplanner + labels: [area/optaplanner] title: "optaplanner" notify: [ge0ffrey, rsynek, Christopher-Chianelli] - - labels: [kind/extension-proposal,area/quarkiverse] + - id: extension-proposal + labels: [kind/extension-proposal,area/quarkiverse] title: "Extension Proposal" notify: [gastaldi, gsmet, aloubyansky, maxandersen] - - labels: [area/mailer] + - id: mailer + labels: [area/mailer] title: "\\bmail\\b" notify: [cescoffier] directories: - extensions/mailer/ - integration-tests/mailer/ - - labels: [area/agroal] + - id: agroal + labels: [area/agroal] title: "agroal" notify: [barreiro, Sanne, yrodiere] directories: - extensions/agroal/ - - labels: [area/continuous-testing] + - id: continuous-testing + labels: [area/continuous-testing] title: "continuous.testing" notify: [stuartwdouglas] - - labels: [area/testing] - title: "quarkusintegrationtest" - notify: [geoand] - - labels: [area/devservices] + - id: devservices + labels: [area/devservices] title: "dev.?services" notify: [stuartwdouglas, geoand] - - labels: [area/reactive-sql-clients] + - id: reactive-sql-clients + labels: [area/reactive-sql-clients] title: "(reactive sql|reactive pool|pgpool|mysqlpool|db2pool)" notify: [tsegismont,vietj] directories: @@ -566,22 +656,26 @@ triage: - integration-tests/reactive-mysql-client/ - integration-tests/reactive-mssql-client/ - integration-tests/reactive-pg-client/ - - labels: [area/adr] + - id: adr + labels: [area/adr] title: "adr" notify: [cescoffier] directories: - adr/ - - labels: [area/context-propagation] + - id: context-propagation + labels: [area/context-propagation] title: "(context propagation|context-propagation)" notify: [FroMage, manovotn] directories: - extensions/smallrye-context-propagation - - labels: [area/stork] + - id: stork + labels: [area/stork] title: "stork" notify: [aureamunoz, cescoffier, Sgitario] directories: - extensions/smallrye-stork/ - - labels: [area/jakarta] + - id: jakarta + labels: [area/jakarta] notify: [sanne,maxandersen,gsmet,manovotn,radcortez] notifyInPullRequest: true directories: diff --git a/.github/quarkus-github-lottery.yml b/.github/quarkus-github-lottery.yml index 82ab4366c6eb3..414f2ddef2101 100644 --- a/.github/quarkus-github-lottery.yml +++ b/.github/quarkus-github-lottery.yml @@ -88,11 +88,11 @@ participants: - username: "maxandersen" timezone: "Europe/Paris" triage: - days: ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"] + days: ["WEDNESDAY", "FRIDAY"] maxIssues: 3 maintenance: labels: ["area/build", "area/cli", "area/devmode", "area/dev-ui", "area/gradle", "area/jbang", "area/devtools"] - days: ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"] + days: ["WEDNESDAY", "FRIDAY"] reproducer: needed: maxIssues: 4 diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index 694d20eb87b72..a3e23b2c5d441 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -737,7 +737,7 @@ jobs: env: TEST_MODULES: ${{matrix.test-modules}} CONTAINER_BUILD: ${{startsWith(matrix.os-name, 'windows') && 'false' || 'true'}} - run: ./mvnw $COMMON_MAVEN_ARGS -f integration-tests -pl "$TEST_MODULES" $NATIVE_TEST_MAVEN_ARGS -Dquarkus.native.container-build=$CONTAINER_BUILD + run: ./mvnw $COMMON_MAVEN_ARGS -f integration-tests -pl "$TEST_MODULES" -amd $NATIVE_TEST_MAVEN_ARGS -Dquarkus.native.container-build=$CONTAINER_BUILD - name: Prepare failure archive (if maven failed) if: failure() shell: bash diff --git a/.github/workflows/ci-kubernetes.yml b/.github/workflows/ci-kubernetes.yml index 513f60be28e3b..e2f6c8df93558 100644 --- a/.github/workflows/ci-kubernetes.yml +++ b/.github/workflows/ci-kubernetes.yml @@ -57,7 +57,7 @@ jobs: shell: bash run: tar -xzf maven-repo.tgz -C ~ - name: Set up Minikube-Kubernetes - uses: manusa/actions-setup-minikube@v2.7.1 + uses: manusa/actions-setup-minikube@v2.7.2 with: minikube version: v1.16.0 kubernetes version: ${{ matrix.kubernetes }} @@ -91,7 +91,7 @@ jobs: openshift: name: OpenShift Integration Tests needs: cache - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 if: "github.repository == 'quarkusio/quarkus' || github.event_name == 'workflow_dispatch'" strategy: fail-fast: false diff --git a/.github/workflows/doc-build.yml b/.github/workflows/doc-build.yml index 1c2ba99450ec4..1d64facb5025a 100644 --- a/.github/workflows/doc-build.yml +++ b/.github/workflows/doc-build.yml @@ -53,8 +53,8 @@ jobs: key: q2maven-doc-${{ steps.get-date.outputs.date }} - name: Build run: | - ./mvnw -Dquickly-ci -B -DskipDocs=false --settings .github/mvn-settings.xml install + ./mvnw -DquicklyDocs -B --settings .github/mvn-settings.xml - name: Build Docs run: | - ./mvnw -e -B --settings .github/mvn-settings.xml clean org.asciidoctor:asciidoctor-maven-plugin:process-asciidoc -pl docs -Ddocumentation-pdf + ./mvnw -e -B --settings .github/mvn-settings.xml clean package -pl docs diff --git a/.github/workflows/vale.yml b/.github/workflows/vale.yml new file mode 100644 index 0000000000000..aef7dbcfdd5e4 --- /dev/null +++ b/.github/workflows/vale.yml @@ -0,0 +1,33 @@ +--- +name: Linting with Vale +on: + pull_request: + paths: + - 'docs/src/main/asciidoc/**' + - '.github/workflows/vale.yml' + +concurrency: + group: "workflow = ${{ github.workflow }}, ref = ${{ github.event.ref }}, pr = ${{ github.event.pull_request.id }}" + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.repository != 'quarkusio/quarkus' }} + +jobs: + vale: + name: Linting with Vale + runs-on: ubuntu-latest + permissions: + actions: read + checks: read + contents: read + pull-requests: read + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Vale Linter + uses: errata-ai/vale-action@reviewdog + with: + fail_on_error: false + vale_flags: "--no-exit --config=docs/.vale/vale.ini" + filter_mode: diff_context + files: docs/src/main/asciidoc/ + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/ADOPTERS.md b/ADOPTERS.md index 8d36043924ba4..ae105f622d984 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -24,6 +24,7 @@ If any organization would like get added or removed please make a pull request b |Logicdrop | https://quarkus.io/blog/logicdrop-customer-story/ | |Payair | https://quarkus.io/blog/why-did-payair-technologies-switch-to-quarkus/ | |Sedona | https://quarkus.io/blog/sedona-rewrites-insurance-premium/ | +|Stargate | https://quarkus.io/blog/stargate-selects-quarkus-for-its-v2-implementation/ | |Suomen Asiakastieto Oy | https://quarkus.io/blog/asiakastieto-chooses-quarkus-for-microservices/ | |Talkdesk | https://quarkus.io/blog/talkdesk-chooses-quarkus-for-fast-innovation/ | |UTN Faculty Córdoba | https://www.frc.utn.edu.ar/computos/tech/ | diff --git a/README.md b/README.md index 6014a7f066825..d72e4d120a9e0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Quarkus](https://design.jboss.org/quarkus/logo/final/PNG/quarkus_logo_horizontal_rgb_1280px_reverse.png#gh-dark-mode-only)](https://quarkus.io/#gh-dark-mode-only) [![Version](https://img.shields.io/maven-central/v/io.quarkus/quarkus-bom?logo=apache-maven&style=for-the-badge)](https://search.maven.org/artifact/io.quarkus/quarkus-bom) -[![GitHub Actions Status]()](https://github.com/quarkusio/quarkus/actions?query=workflow%3A%22Quarkus+CI%22) +[![GitHub Actions Status]()](https://github.com/quarkusio/quarkus/actions?query=workflow%3A%22Quarkus+CI%22) [![Commits](https://img.shields.io/github/commit-activity/m/quarkusio/quarkus.svg?label=commits&style=for-the-badge&logo=git&logoColor=white)](https://github.com/quarkusio/quarkus/pulse) [![License](https://img.shields.io/github/license/quarkusio/quarkus?style=for-the-badge&logo=apache)](https://www.apache.org/licenses/LICENSE-2.0) [![Project Chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg?style=for-the-badge&logo=zulip)](https://quarkusio.zulipchat.com/) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index b9d5b51652411..307194dd465f4 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -18,14 +18,16 @@ 1.0.2.3 1.0.14 3.0.2 - 3.0.4 + 3.0.5 4.7.7.Final 0.33.0 0.2.4 0.1.15 0.1.5 - 1.19.0 - 1.19.0-alpha + 1.21.0 + 1.21.0-alpha + 1.20.1-alpha + 1.20.1 1.8.1 4.2.1 1.10.2 @@ -39,20 +41,21 @@ 2.0 1.2 1.0 + 2.0.1 1.13.2 - 2.13.0 + 2.13.1 3.3.1 3.0.5 - 2.3.1 + 3.1.1 1.9.0 2.1.1 5.6.0 - 3.5.4 + 3.6.0 1.2.2 1.0.13 2.7.0 - 2.28.0 - 3.22.0 + 2.29.0 + 3.22.1 1.3.3 1.2.1 1.3.5 @@ -97,7 +100,7 @@ 1.1.1.Final 1.16 7.6.0.Final - 8.5.0 + 8.5.3 7.10.2 2.2.21 @@ -110,10 +113,10 @@ 1.0.1.Final 1.20.1.Final 3.4.3.Final - 4.3.5 + 4.3.6 - 4.5.13 - 4.4.15 + 4.5.14 + 4.4.16 4.1.5 9.2.0 2.3.2 @@ -131,10 +134,10 @@ 5.9.1 1.5.0 6.14.2 - 14.0.2.Final - 4.5.0.Final + 14.0.4.Final + 4.5.1.Final 3.1.1 - 4.1.85.Final + 4.1.86.Final 1.8.0 1.0.3 3.5.0.Final @@ -149,20 +152,20 @@ 3.11.0 2.13.0 1.4.2 - 1.7.21 + 1.7.22 1.6.4 1.4.1 - 6.2.0 - 3.1.2 + 6.3.1 + 3.2.0 3.2.0 4.2.0 1.0.11 - 9.8.3 + 9.10.2 1.0.11 - 4.17.2 + 4.18.0 1.33 6.0.0 - 4.8.0 + 4.8.1 1.6.1 0.34.0 3.24.2 @@ -174,11 +177,11 @@ 2.1.SP2 5.3.Final 2.1.SP1 - 4.9.0 + 4.10.0 5.8.0 4.10.1 1.1.4.Final - 19.0.3 + 20.0.1 1.15.0 3.28.0 2.16 @@ -204,7 +207,7 @@ 2.6 0.11.0 - 9.25.6 + 9.27 0.0.6 0.1.3 2.9.2 @@ -3350,6 +3353,76 @@ rxjava ${rxjava.version} + + + io.opentelemetry.contrib + opentelemetry-aws-xray-propagator + ${opentelemetry-aws.contrib.version} + + + io.opentelemetry + opentelemetry-api + + + + + io.opentelemetry.contrib + opentelemetry-aws-resources + ${opentelemetry-aws.contrib.version} + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-sdk + + + io.opentelemetry + opentelemetry-semconv + + + com.fasterxml.jackson.core + jackson-core + + + com.squareup.okhttp3 + okhttp + + + + + io.opentelemetry.contrib + opentelemetry-aws-xray + ${opentelemetry-aws-xray.contrib.version} + + + io.opentelemetry + opentelemetry-sdk + + + io.opentelemetry + opentelemetry-sdk-trace + + + io.opentelemetry + opentelemetry-semconv + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.squareup.okhttp3 + okhttp + + + io.opentracing opentracing-api @@ -4215,6 +4288,11 @@ + + org.eclipse.microprofile.openapi + microprofile-openapi-api + ${microprofile-openapi.version} + org.glassfish jakarta.el @@ -5591,6 +5669,12 @@ org.keycloak keycloak-adapter-core ${keycloak.version} + + + commons-logging + commons-logging + + org.keycloak diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 592803b005192..39e69f4b18080 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -19,11 +19,11 @@ - 3.8.1 - 1.7.21 + 3.10.1 + 1.7.22 1.7.20 2.13.8 - 4.7.2 + 4.8.0 ${scala-maven-plugin.version} @@ -34,7 +34,7 @@ ${version.surefire.plugin} - 3.0.4 + 3.0.5 1.0.0 2.5.7 @@ -97,19 +97,20 @@ - 19.0.3 + 20.0.1 + 19.0.3 quay.io/keycloak/keycloak:${keycloak.version} - quay.io/keycloak/keycloak:${keycloak.version}-legacy + quay.io/keycloak/keycloak:${keycloak.wildfly.version}-legacy - 6.0.6 + 6.0.7 3.23.1 2.35.0 - 7.2.0 + 7.3.0 - 2.27.0 + 2.27.1 @@ -117,7 +118,7 @@ 0.8.8 2.0.0 - 0.40.2 + 0.40.3 2.21.0 1.8.0 3.2.2 @@ -709,6 +710,7 @@ false true + **.SuppressForbidden compile diff --git a/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java b/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java index c504de2b24410..2b26c34844365 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java @@ -88,7 +88,7 @@ public Result get() { .loadClass("org.testcontainers.utility.TestcontainersConfiguration"); Object configurationInstance = configurationClass.getMethod("getInstance").invoke(null); String oldReusePropertyValue = (String) configurationClass - .getMethod("getUserProperty", String.class, String.class) + .getMethod("getEnvVarOrUserProperty", String.class, String.class) .invoke(configurationInstance, "testcontainers.reuse.enable", "false"); // use the default provided in TestcontainersConfiguration#environmentSupportsReuse Method updateUserConfigMethod = configurationClass.getMethod("updateUserConfig", String.class, String.class); // this will ensure that testcontainers does not start ryuk - see https://github.com/quarkusio/quarkus/issues/25852 for why this is important diff --git a/core/deployment/src/main/java/io/quarkus/deployment/SuppressForbidden.java b/core/deployment/src/main/java/io/quarkus/deployment/SuppressForbidden.java new file mode 100644 index 0000000000000..8d4e6db80fe75 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/SuppressForbidden.java @@ -0,0 +1,16 @@ +package io.quarkus.deployment; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to suppress forbidden-apis errors inside for the element it's added to. + */ +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE }) +public @interface SuppressForbidden { + + String reason(); +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java index 982c19bed475c..aa27268145693 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConfig.java @@ -149,6 +149,12 @@ public class TestConfig { @ConfigItem(defaultValue = "prod") String nativeImageProfile; + /** + * The profile to use when testing using {@code @QuarkusIntegrationTest} + */ + @ConfigItem(defaultValue = "prod") + String integrationTestProfile; + /** * Profile related test settings */ diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java index 58c97c1eb237c..60d9243131ad5 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java @@ -116,8 +116,14 @@ protected List getContainerRuntimeBuildArgs() { volumeOutputPath = FileUtil.translateToVolumePath(volumeOutputPath); } + String selinuxBindOption = ":z"; + if (SystemUtils.IS_OS_MAC + && ContainerRuntimeUtil.detectContainerRuntime() == ContainerRuntimeUtil.ContainerRuntime.PODMAN) { + selinuxBindOption = ""; + } + Collections.addAll(containerRuntimeArgs, "-v", - volumeOutputPath + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH + ":z"); + volumeOutputPath + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH + selinuxBindOption); return containerRuntimeArgs; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/AnnotationProxyProvider.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/AnnotationProxyProvider.java index f6960c54221a4..280939bd38842 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/recording/AnnotationProxyProvider.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/AnnotationProxyProvider.java @@ -142,7 +142,7 @@ public A build(ClassOutput classOutput) { String name = annotationInstance.name().toString(); - // Ljavax/enterprise/util/AnnotationLiteral;Lcom/foo/MyAnnotation; + // Ljakarta/enterprise/util/AnnotationLiteral;Lcom/foo/MyAnnotation; String signature = String.format("L%1$s;L%2$s;", AnnotationLiteral.class.getName().replace('.', '/'), name.replace('.', '/')); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java index 8bfeb4646b8f2..e6a776439e17b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java @@ -132,7 +132,7 @@ public class BytecodeRecorderImpl implements RecorderContext { private final List loaders = new ArrayList<>(); private final Map, ConstantHolder> constants = new HashMap<>(); - private final Set classesToUseRecorableConstructor = new HashSet<>(); + private final Set classesToUseRecordableConstructor = new HashSet<>(); private final boolean useIdentityComparison; /** @@ -394,7 +394,7 @@ public void run() { } public void markClassAsConstructorRecordable(Class clazz) { - classesToUseRecorableConstructor.add(clazz); + classesToUseRecordableConstructor.add(clazz); } private ProxyInstance getProxyInstance(Class returnType) throws InstantiationException, IllegalAccessException { @@ -1173,7 +1173,7 @@ public void prepare(MethodContext context) { nonDefaultConstructorHandles[i] = loadObjectInstance(obj, existing, parameterTypes[count++], relaxedValidation); } - } else if (classesToUseRecorableConstructor.contains(param.getClass())) { + } else if (classesToUseRecordableConstructor.contains(param.getClass())) { Constructor current = null; int count = 0; for (var c : param.getClass().getConstructors()) { @@ -1198,7 +1198,7 @@ public void prepare(MethodContext context) { } } else { for (Constructor ctor : param.getClass().getConstructors()) { - if (ctor.isAnnotationPresent(RecordableConstructor.class)) { + if (RecordingAnnotationsUtil.isRecordableConstructor(ctor)) { nonDefaultConstructorHolder = new NonDefaultConstructorHolder(ctor, null); nonDefaultConstructorHandles = new DeferredParameter[ctor.getParameterCount()]; @@ -1219,7 +1219,7 @@ public void prepare(MethodContext context) { for (Property i : desc) { if (!i.getDeclaringClass().getPackageName().startsWith("java.")) { // check if the getter is ignored - if ((i.getReadMethod() != null) && (i.getReadMethod().getAnnotation(IgnoreProperty.class) != null)) { + if ((i.getReadMethod() != null) && RecordingAnnotationsUtil.isIgnored(i.getReadMethod())) { continue; } // check if the matching field is ignored @@ -1556,7 +1556,10 @@ ResultHandle createValue(MethodContext context, MethodCreator method, ResultHand * Returns {@code true} iff the field is annotated {@link IgnoreProperty} or the field is marked as {@code transient} */ private static boolean ignoreField(Field field) { - return (field.getAnnotation(IgnoreProperty.class) != null) || Modifier.isTransient(field.getModifiers()); + if (Modifier.isTransient(field.getModifiers())) { + return true; + } + return RecordingAnnotationsUtil.isIgnored(field); } private DeferredParameter findLoaded(final Object param) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsProvider.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsProvider.java new file mode 100644 index 0000000000000..f6748b33ad4ad --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsProvider.java @@ -0,0 +1,14 @@ +package io.quarkus.deployment.recording; + +import java.lang.annotation.Annotation; + +public interface RecordingAnnotationsProvider { + + default Class ignoredProperty() { + return null; + } + + default Class recordableConstructor() { + return null; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsUtil.java new file mode 100644 index 0000000000000..fe118e7c30a5c --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsUtil.java @@ -0,0 +1,62 @@ +package io.quarkus.deployment.recording; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.util.HashSet; +import java.util.List; +import java.util.ServiceLoader; +import java.util.Set; + +import io.quarkus.runtime.annotations.IgnoreProperty; +import io.quarkus.runtime.annotations.RecordableConstructor; + +final class RecordingAnnotationsUtil { + + static final List> IGNORED_PROPERTY_ANNOTATIONS; + static final List> RECORDABLE_CONSTRUCTOR_ANNOTATIONS; + + static { + Set> ignoredPropertyAnnotations = new HashSet<>(); + ignoredPropertyAnnotations.add(IgnoreProperty.class); + Set> recordableConstructorAnnotations = new HashSet<>(); + recordableConstructorAnnotations.add(RecordableConstructor.class); + + for (RecordingAnnotationsProvider provider : ServiceLoader.load(RecordingAnnotationsProvider.class)) { + Class ignoredProperty = provider.ignoredProperty(); + if (ignoredProperty != null) { + ignoredPropertyAnnotations.add(ignoredProperty); + } + Class recordableConstructor = provider.recordableConstructor(); + if (recordableConstructor != null) { + recordableConstructorAnnotations.add(recordableConstructor); + } + } + + IGNORED_PROPERTY_ANNOTATIONS = List.copyOf(ignoredPropertyAnnotations); + RECORDABLE_CONSTRUCTOR_ANNOTATIONS = List.copyOf(recordableConstructorAnnotations); + } + + private RecordingAnnotationsUtil() { + } + + static boolean isIgnored(AccessibleObject object) { + for (int i = 0; i < IGNORED_PROPERTY_ANNOTATIONS.size(); i++) { + Class annotation = IGNORED_PROPERTY_ANNOTATIONS.get(i); + if (object.isAnnotationPresent(annotation)) { + return true; + } + } + return false; + } + + static boolean isRecordableConstructor(Constructor ctor) { + for (int i = 0; i < RECORDABLE_CONSTRUCTOR_ANNOTATIONS.size(); i++) { + Class annotation = RECORDABLE_CONSTRUCTOR_ANNOTATIONS.get(i); + if (ctor.isAnnotationPresent(annotation)) { + return true; + } + } + return false; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/KotlinUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/KotlinUtil.java new file mode 100644 index 0000000000000..af69aa5390ac7 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/KotlinUtil.java @@ -0,0 +1,16 @@ +package io.quarkus.deployment.steps; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; + +final class KotlinUtil { + + private static final DotName KOTLIN_METADATA_ANNOTATION = DotName.createSimple("kotlin.Metadata"); + + private KotlinUtil() { + } + + static boolean isKotlinClass(ClassInfo classInfo) { + return classInfo.hasDeclaredAnnotation(KOTLIN_METADATA_ANNOTATION); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java index 6371b57ef3c7c..d1412f8c43c0b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java @@ -1,5 +1,6 @@ package io.quarkus.deployment.steps; +import static io.quarkus.deployment.steps.KotlinUtil.isKotlinClass; import static io.quarkus.gizmo.MethodDescriptor.ofConstructor; import static io.quarkus.gizmo.MethodDescriptor.ofMethod; @@ -17,8 +18,10 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ArrayType; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; import org.jboss.logging.Logger; @@ -78,6 +81,7 @@ import io.quarkus.runtime.StartupTask; import io.quarkus.runtime.annotations.QuarkusMain; import io.quarkus.runtime.appcds.AppCDSUtil; +import io.quarkus.runtime.configuration.ConfigUtils; import io.quarkus.runtime.configuration.ProfileManager; import io.quarkus.runtime.naming.DisabledInitialContextManager; import io.quarkus.runtime.util.StepTiming; @@ -100,6 +104,8 @@ public class MainClassBuildStep { void.class); public static final MethodDescriptor CONFIGURE_STEP_TIME_START = ofMethod(StepTiming.class.getName(), "configureStart", void.class); + private static final DotName QUARKUS_APPLICATION = DotName.createSimple(QuarkusApplication.class.getName()); + private static final DotName OBJECT = DotName.createSimple(Object.class.getName()); @BuildStep void build(List staticInitTasks, @@ -246,8 +252,8 @@ void build(List staticInitTasks, startupContext, mv.getMethodParam(0)); mv.invokeStaticMethod(CONFIGURE_STEP_TIME_ENABLED); - ResultHandle activeProfile = mv - .invokeStaticMethod(ofMethod(ProfileManager.class, "getActiveProfile", String.class)); + ResultHandle profiles = mv + .invokeStaticMethod(ofMethod(ConfigUtils.class, "getProfiles", List.class)); tryBlock = mv.tryBlock(); tryBlock.invokeStaticMethod(CONFIGURE_STEP_TIME_START); @@ -269,12 +275,12 @@ void build(List staticInitTasks, ResultHandle featuresHandle = tryBlock.load(featureNames.stream().sorted().collect(Collectors.joining(", "))); tryBlock.invokeStaticMethod( ofMethod(Timing.class, "printStartupTime", void.class, String.class, String.class, String.class, String.class, - String.class, boolean.class, boolean.class), + List.class, boolean.class, boolean.class), tryBlock.load(applicationInfo.getName()), tryBlock.load(applicationInfo.getVersion()), tryBlock.load(Version.getVersion()), featuresHandle, - activeProfile, + profiles, tryBlock.load(LaunchMode.DEVELOPMENT.equals(launchMode.getLaunchMode())), tryBlock.load(launchMode.isAuxiliaryApplication())); @@ -333,7 +339,8 @@ public MainClassBuildItem mainClassBuildStep(BuildProducer quarkusMainAnnotations = new HashMap<>(); - Collection quarkusMains = combinedIndexBuildItem.getIndex() + IndexView index = combinedIndexBuildItem.getIndex(); + Collection quarkusMains = index .getAnnotations(DotName.createSimple(QuarkusMain.class.getName())); for (AnnotationInstance i : quarkusMains) { AnnotationValue nameValue = i.value("name"); @@ -341,12 +348,13 @@ public MainClassBuildItem mainClassBuildStep(BuildProducer impls = combinedIndexBuildItem.getIndex() - .getAllKnownImplementors(DotName.createSimple(QuarkusApplication.class.getName())); - ClassInfo classByName = combinedIndexBuildItem.getIndex().getClassByName(DotName.createSimple(mainClassName)); + Collection impls = index + .getAllKnownImplementors(QUARKUS_APPLICATION); + ClassInfo classByName = index.getClassByName(DotName.createSimple(mainClassName)); MethodInfo mainClassMethod = null; if (classByName != null) { mainClassMethod = classByName @@ -398,7 +406,7 @@ public MainClassBuildItem mainClassBuildStep(BuildProducer generatedClass) { ClassCreator file = new ClassCreator(new GeneratedClassGizmoAdaptor(generatedClass, true), MAIN_CLASS, null, 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 873cfce2d5abc..3e4e24105d90c 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 @@ -282,6 +282,7 @@ public void write(String s, byte[] bytes) { CatchBlockCreator cc = tc.addCatch(Throwable.class); cc.invokeVirtualMethod(ofMethod(Throwable.class, "printStackTrace", void.class), cc.getCaughtException()); } + runtimeInitializedPackages.returnValue(packagesArray); ResultHandle packages = overallCatch.invokeStaticMethod(runtimeInitializedPackages.getMethodDescriptor()); overallCatch.invokeStaticMethod(INITIALIZE_PACKAGES_AT_RUN_TIME, packages); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/RegisterForReflectionBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/RegisterForReflectionBuildStep.java index 0018217fa3ae7..b4a085c479f28 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/RegisterForReflectionBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/RegisterForReflectionBuildStep.java @@ -1,5 +1,7 @@ package io.quarkus.deployment.steps; +import static io.quarkus.deployment.steps.KotlinUtil.isKotlinClass; + import java.io.IOException; import java.lang.reflect.Modifier; import java.util.HashSet; @@ -34,8 +36,6 @@ public class RegisterForReflectionBuildStep { private static final Logger log = Logger.getLogger(RegisterForReflectionBuildStep.class); - private static final DotName KOTLIN_METADATA_ANNOTATION = DotName.createSimple("kotlin.Metadata"); - @BuildStep public void build(CombinedIndexBuildItem combinedIndexBuildItem, Capabilities capabilities, BuildProducer reflectiveClass, @@ -98,10 +98,6 @@ public void build(CombinedIndexBuildItem combinedIndexBuildItem, Capabilities ca } } - private static boolean isKotlinClass(ClassInfo classInfo) { - return classInfo.hasDeclaredAnnotation(KOTLIN_METADATA_ANNOTATION); - } - /** * BFS Recursive Method to register a class and it's inner classes for Reflection. * diff --git a/core/deployment/src/test/java/io/quarkus/deployment/recording/BytecodeRecorderTestCase.java b/core/deployment/src/test/java/io/quarkus/deployment/recording/BytecodeRecorderTestCase.java index ea887b2d7463e..eff0271228d08 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/recording/BytecodeRecorderTestCase.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/recording/BytecodeRecorderTestCase.java @@ -167,8 +167,9 @@ public void testIgnoredProperties() throws Exception { ignoredProperties.setNotIgnored("Shows up"); ignoredProperties.setIgnoredField("Does not show up"); ignoredProperties.setAnotherIgnoredField("Does not show up either"); + ignoredProperties.setCustomIgnoredField("Does not show up either"); recorder.ignoredProperties(ignoredProperties); - }, new IgnoredProperties("Shows up", null, null)); + }, new IgnoredProperties("Shows up", null, null, null)); } @Test @@ -292,6 +293,13 @@ public void testRecordableConstructor() throws Exception { TestRecorder recorder = generator.getRecordingProxy(TestRecorder.class); recorder.bean(bean); }, new TestConstructorBean("John", "Citizen").setAge(30)); + + runTest(generator -> { + OtherTestConstructorBean bean = new OtherTestConstructorBean("Jane", "Citizen"); + bean.setAge(30); + TestRecorder recorder = generator.getRecordingProxy(TestRecorder.class); + recorder.bean(bean); + }, new OtherTestConstructorBean("Jane", "Citizen").setAge(30)); } @Test diff --git a/core/deployment/src/test/java/io/quarkus/deployment/recording/IgnoredProperties.java b/core/deployment/src/test/java/io/quarkus/deployment/recording/IgnoredProperties.java index 24a80a71c83c3..6f6aaafee4240 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/recording/IgnoredProperties.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/recording/IgnoredProperties.java @@ -12,13 +12,17 @@ public class IgnoredProperties { private transient String anotherIgnoredField; + @TestRecordingAnnotationsProvider.TestIgnoreProperty + private String customIgnoredField; + public IgnoredProperties() { } - public IgnoredProperties(String notIgnored, String ignoredField, String anotherIgnoredField) { + public IgnoredProperties(String notIgnored, String ignoredField, String anotherIgnoredField, String customIgnoredField) { this.notIgnored = notIgnored; this.ignoredField = ignoredField; this.anotherIgnoredField = anotherIgnoredField; + this.customIgnoredField = customIgnoredField; } public String getNotIgnored() { @@ -45,6 +49,14 @@ public void setAnotherIgnoredField(String anotherIgnoredField) { this.anotherIgnoredField = anotherIgnoredField; } + public String getCustomIgnoredField() { + return customIgnoredField; + } + + public void setCustomIgnoredField(String customIgnoredField) { + this.customIgnoredField = customIgnoredField; + } + @IgnoreProperty public String getSomethingElse() { throw new IllegalStateException("This should not have been called"); @@ -59,12 +71,13 @@ public boolean equals(Object o) { IgnoredProperties that = (IgnoredProperties) o; return Objects.equals(notIgnored, that.notIgnored) && Objects.equals(ignoredField, that.ignoredField) && - Objects.equals(anotherIgnoredField, that.anotherIgnoredField); + Objects.equals(anotherIgnoredField, that.anotherIgnoredField) && + Objects.equals(customIgnoredField, that.customIgnoredField); } @Override public int hashCode() { - return Objects.hash(notIgnored, ignoredField, anotherIgnoredField); + return Objects.hash(notIgnored, ignoredField, anotherIgnoredField, customIgnoredField); } @Override @@ -72,6 +85,8 @@ public String toString() { return "IgnoredProperties{" + "notIgnored='" + notIgnored + '\'' + ", ignoredField='" + ignoredField + '\'' + + ", anotherIgnoredField='" + anotherIgnoredField + '\'' + + ", customIgnoredField='" + customIgnoredField + '\'' + '}'; } } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/recording/OtherTestConstructorBean.java b/core/deployment/src/test/java/io/quarkus/deployment/recording/OtherTestConstructorBean.java new file mode 100644 index 0000000000000..59f3fccdd520d --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/recording/OtherTestConstructorBean.java @@ -0,0 +1,55 @@ +package io.quarkus.deployment.recording; + +import java.util.Objects; + +public class OtherTestConstructorBean { + + String first; + final String last; + int age; + + @TestRecordingAnnotationsProvider.TestRecordableConstructor + public OtherTestConstructorBean(String first, String last) { + this.first = first; + this.last = last; + } + + public void setFirst(String first) { + //should not be called, as it was initialized in the constructor + this.first = "Mrs " + first; + } + + public String getFirst() { + return first; + } + + public String getLast() { + return last; + } + + public int getAge() { + return age; + } + + public OtherTestConstructorBean setAge(int age) { + this.age = age; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + OtherTestConstructorBean that = (OtherTestConstructorBean) o; + return age == that.age && + Objects.equals(first, that.first) && + Objects.equals(last, that.last); + } + + @Override + public int hashCode() { + return Objects.hash(first, last, age); + } +} diff --git a/core/deployment/src/test/java/io/quarkus/deployment/recording/TestRecorder.java b/core/deployment/src/test/java/io/quarkus/deployment/recording/TestRecorder.java index 41eb65ac76c45..50345ca827c3f 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/recording/TestRecorder.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/recording/TestRecorder.java @@ -76,6 +76,10 @@ public void bean(TestConstructorBean bean) { RESULT.add(bean); } + public void bean(OtherTestConstructorBean bean) { + RESULT.add(bean); + } + public void result(RuntimeValue bean) { RESULT.add(bean.getValue()); } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/recording/TestRecordingAnnotationsProvider.java b/core/deployment/src/test/java/io/quarkus/deployment/recording/TestRecordingAnnotationsProvider.java new file mode 100644 index 0000000000000..c39a973afbd54 --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/recording/TestRecordingAnnotationsProvider.java @@ -0,0 +1,30 @@ +package io.quarkus.deployment.recording; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public class TestRecordingAnnotationsProvider implements RecordingAnnotationsProvider { + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD, ElementType.FIELD }) + public @interface TestIgnoreProperty { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.CONSTRUCTOR) + public @interface TestRecordableConstructor { + } + + @Override + public Class ignoredProperty() { + return TestIgnoreProperty.class; + } + + @Override + public Class recordableConstructor() { + return TestRecordableConstructor.class; + } +} diff --git a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ParentFirstFlagPropagationTest.java b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ParentFirstFlagPropagationTest.java new file mode 100644 index 0000000000000..9f992898e9e73 --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ParentFirstFlagPropagationTest.java @@ -0,0 +1,77 @@ +package io.quarkus.deployment.runnerjar; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.resolver.TsArtifact; +import io.quarkus.bootstrap.resolver.TsQuarkusExt; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ArtifactDependency; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.maven.dependency.DependencyFlags; + +public class ParentFirstFlagPropagationTest extends BootstrapFromOriginalJarTestBase { + + @Override + protected TsArtifact composeApplication() { + + final TsArtifact extADep1 = TsArtifact.jar("ext-a-dep"); + addToExpectedLib(extADep1); + final TsArtifact extBDep1 = TsArtifact.jar("ext-b-dep"); + addToExpectedLib(extBDep1); + final TsArtifact extBDepTrans1 = TsArtifact.jar("ext-b-dep-trans"); + addToExpectedLib(extBDepTrans1); + extBDep1.addDependency(extBDepTrans1); + + final TsQuarkusExt extA = new TsQuarkusExt("ext-a"); + extA.getRuntime() + .addDependency(extADep1) + .addDependency(extBDep1, extBDepTrans1); + addToExpectedLib(extA.getRuntime()); + final TsQuarkusExt extB = new TsQuarkusExt("ext-b"); + addToExpectedLib(extB.getRuntime()); + extB.getRuntime().addDependency(extBDep1); + extB.addDependency(extA); + extB.setDependencyFlag(extBDep1.getKey(), DependencyFlags.CLASSLOADER_PARENT_FIRST); + + return TsArtifact.jar("app") + .addManagedDependency(platformDescriptor()) + .addManagedDependency(platformProperties()) + .addDependency(extB); + } + + @Override + protected void assertAppModel(ApplicationModel appModel) throws Exception { + final Set expectedDeployDeps = Set.of( + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-b-deployment", "1"), "compile", + DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-a-deployment", "1"), "compile", + DependencyFlags.DEPLOYMENT_CP)); + assertEquals(expectedDeployDeps, appModel.getDependencies().stream().filter(d -> d.isDeploymentCp() && !d.isRuntimeCp()) + .map(d -> new ArtifactDependency(d)).collect(Collectors.toSet())); + + final Set expectedRuntimeDeps = Set.of( + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-a", "1"), "compile", + DependencyFlags.RUNTIME_EXTENSION_ARTIFACT, DependencyFlags.RUNTIME_CP, DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-a-dep", "1"), "compile", + DependencyFlags.RUNTIME_CP, DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-b", "1"), "compile", + DependencyFlags.DIRECT, DependencyFlags.RUNTIME_EXTENSION_ARTIFACT, DependencyFlags.RUNTIME_CP, + DependencyFlags.DEPLOYMENT_CP, DependencyFlags.TOP_LEVEL_RUNTIME_EXTENSION_ARTIFACT), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-b-dep", "1"), "compile", + DependencyFlags.RUNTIME_CP, DependencyFlags.DEPLOYMENT_CP, DependencyFlags.CLASSLOADER_PARENT_FIRST), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-b-dep-trans", "1"), "compile", + DependencyFlags.RUNTIME_CP, DependencyFlags.DEPLOYMENT_CP, DependencyFlags.CLASSLOADER_PARENT_FIRST)); + assertEquals(expectedRuntimeDeps, + appModel.getRuntimeDependencies().stream().map(d -> new ArtifactDependency(d)).collect(Collectors.toSet())); + final Set expectedFullDeps = new HashSet<>(); + expectedFullDeps.addAll(expectedDeployDeps); + expectedFullDeps.addAll(expectedRuntimeDeps); + assertEquals(expectedFullDeps, + appModel.getDependencies().stream().map(d -> new ArtifactDependency(d)).collect(Collectors.toSet())); + } +} diff --git a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/RunnerParentFirstFlagPropagationTest.java b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/RunnerParentFirstFlagPropagationTest.java new file mode 100644 index 0000000000000..8b20d2c88aee7 --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/RunnerParentFirstFlagPropagationTest.java @@ -0,0 +1,76 @@ +package io.quarkus.deployment.runnerjar; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.resolver.TsArtifact; +import io.quarkus.bootstrap.resolver.TsQuarkusExt; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ArtifactDependency; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.maven.dependency.DependencyFlags; + +public class RunnerParentFirstFlagPropagationTest extends BootstrapFromOriginalJarTestBase { + @Override + protected TsArtifact composeApplication() { + + final TsArtifact extADep1 = TsArtifact.jar("ext-a-dep"); + addToExpectedLib(extADep1); + final TsArtifact extBDep1 = TsArtifact.jar("ext-b-dep"); + addToExpectedLib(extBDep1); + final TsArtifact extBDepTrans1 = TsArtifact.jar("ext-b-dep-trans"); + addToExpectedLib(extBDepTrans1); + extBDep1.addDependency(extBDepTrans1); + + final TsQuarkusExt extA = new TsQuarkusExt("ext-a"); + extA.getRuntime() + .addDependency(extADep1) + .addDependency(extBDep1, extBDepTrans1); + addToExpectedLib(extA.getRuntime()); + final TsQuarkusExt extB = new TsQuarkusExt("ext-b"); + addToExpectedLib(extB.getRuntime()); + extB.getRuntime().addDependency(extBDep1); + extB.addDependency(extA); + extB.setDependencyFlag(extBDep1.getKey(), DependencyFlags.CLASSLOADER_PARENT_FIRST); + + return TsArtifact.jar("app") + .addManagedDependency(platformDescriptor()) + .addManagedDependency(platformProperties()) + .addDependency(extB); + } + + @Override + protected void assertAppModel(ApplicationModel appModel) throws Exception { + final Set expectedDeployDeps = Set.of( + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-b-deployment", "1"), "compile", + DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-a-deployment", "1"), "compile", + DependencyFlags.DEPLOYMENT_CP)); + assertEquals(expectedDeployDeps, appModel.getDependencies().stream().filter(d -> d.isDeploymentCp() && !d.isRuntimeCp()) + .map(d -> new ArtifactDependency(d)).collect(Collectors.toSet())); + + final Set expectedRuntimeDeps = Set.of( + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-a", "1"), "compile", + DependencyFlags.RUNTIME_EXTENSION_ARTIFACT, DependencyFlags.RUNTIME_CP, DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-a-dep", "1"), "compile", + DependencyFlags.RUNTIME_CP, DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-b", "1"), "compile", + DependencyFlags.DIRECT, DependencyFlags.RUNTIME_EXTENSION_ARTIFACT, DependencyFlags.RUNTIME_CP, + DependencyFlags.DEPLOYMENT_CP, DependencyFlags.TOP_LEVEL_RUNTIME_EXTENSION_ARTIFACT), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-b-dep", "1"), "compile", + DependencyFlags.RUNTIME_CP, DependencyFlags.DEPLOYMENT_CP, DependencyFlags.CLASSLOADER_PARENT_FIRST), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-b-dep-trans", "1"), "compile", + DependencyFlags.RUNTIME_CP, DependencyFlags.DEPLOYMENT_CP, DependencyFlags.CLASSLOADER_PARENT_FIRST)); + assertEquals(expectedRuntimeDeps, + appModel.getRuntimeDependencies().stream().map(d -> new ArtifactDependency(d)).collect(Collectors.toSet())); + final Set expectedFullDeps = new HashSet<>(); + expectedFullDeps.addAll(expectedDeployDeps); + expectedFullDeps.addAll(expectedRuntimeDeps); + assertEquals(expectedFullDeps, + appModel.getDependencies().stream().map(d -> new ArtifactDependency(d)).collect(Collectors.toSet())); + } +} diff --git a/core/deployment/src/test/resources/META-INF/services/io.quarkus.deployment.recording.RecordingAnnotationsProvider b/core/deployment/src/test/resources/META-INF/services/io.quarkus.deployment.recording.RecordingAnnotationsProvider new file mode 100644 index 0000000000000..3a18a34a6f991 --- /dev/null +++ b/core/deployment/src/test/resources/META-INF/services/io.quarkus.deployment.recording.RecordingAnnotationsProvider @@ -0,0 +1 @@ +io.quarkus.deployment.recording.TestRecordingAnnotationsProvider diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java index b629a4d4fa99c..aa824fd8ad3f6 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java @@ -18,7 +18,6 @@ import javax.enterprise.inject.spi.BeanManager; import javax.enterprise.inject.spi.CDI; -import org.graalvm.nativeimage.ImageInfo; import org.jboss.logging.Logger; import org.jboss.logmanager.handlers.AsyncHandler; import org.wildfly.common.lock.Locks; @@ -269,7 +268,7 @@ private static void longLivedPostBootCleanup() { } private static void registerHooks(final BiConsumer exitCodeHandler) { - if (ImageInfo.inImageRuntimeCode() && System.getenv(DISABLE_SIGNAL_HANDLERS) == null) { + if (ImageMode.current() == ImageMode.NATIVE_RUN && System.getenv(DISABLE_SIGNAL_HANDLERS) == null) { registerSignalHandlers(exitCodeHandler); } shutdownHookThread = new ShutdownHookThread(); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/DebugRuntimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/DebugRuntimeConfig.java new file mode 100644 index 0000000000000..129742b14af23 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/DebugRuntimeConfig.java @@ -0,0 +1,16 @@ +package io.quarkus.runtime; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "debug", phase = ConfigPhase.RUN_TIME) +public class DebugRuntimeConfig { + + /** + * If set to {@code true}, Quarkus prints the wall-clock time each build step took to complete. + * This is useful as a first step in debugging slow startup times. + */ + @ConfigItem(defaultValue = "false") + boolean printStartupTimes; +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ImageMode.java b/core/runtime/src/main/java/io/quarkus/runtime/ImageMode.java new file mode 100644 index 0000000000000..e66e6e3cbb966 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/ImageMode.java @@ -0,0 +1,48 @@ +package io.quarkus.runtime; + +import org.graalvm.nativeimage.ImageInfo; + +/** + * The image execution mode of the application. + */ +public enum ImageMode { + /** + * The image mode which indicates that the application is running in a standard JVM. + */ + JVM, + /** + * The image mode which indicates that the application is currently executing the build phase of a native static image. + */ + NATIVE_BUILD, + /** + * The image mode which indicates that the application is a native static image which is currently running on a target + * system. + */ + NATIVE_RUN, + ; + + /** + * Determine whether the application image is a native static image. + * + * @return {@code true} if the application image is a native static image, or {@code false} otherwise + */ + public boolean isNativeImage() { + return current() != JVM; + } + + /** + * Get the current image mode. Note that it is possible for the image mode to change during the lifetime of + * an application. + * + * @return the image mode (not {@code null}) + */ + public static ImageMode current() { + if (ImageInfo.inImageBuildtimeCode()) { + return NATIVE_BUILD; + } else if (ImageInfo.inImageRuntimeCode()) { + return NATIVE_RUN; + } else { + return JVM; + } + } +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/NativeImageRuntimePropertiesRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/NativeImageRuntimePropertiesRecorder.java index 1a261d7585294..23b5c36c4bff4 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/NativeImageRuntimePropertiesRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/NativeImageRuntimePropertiesRecorder.java @@ -3,8 +3,6 @@ import java.util.HashMap; import java.util.Map; -import org.graalvm.nativeimage.ImageInfo; - import io.quarkus.runtime.annotations.Recorder; /** @@ -16,7 +14,7 @@ public class NativeImageRuntimePropertiesRecorder { private static final Map MAP = new HashMap<>(); public void setInStaticInit(String name, String value) { - if (ImageInfo.inImageBuildtimeCode()) { + if (ImageMode.current() == ImageMode.NATIVE_BUILD) { MAP.put(name, value); } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java index ac4fc458b9573..cc113466940e6 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigDiagnostic.java @@ -10,9 +10,9 @@ import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; -import org.graalvm.nativeimage.ImageInfo; import org.jboss.logging.Logger; +import io.quarkus.runtime.ImageMode; import io.smallrye.config.common.utils.StringUtil; /** @@ -107,7 +107,7 @@ public static void unknownProperties(List properties) { } public static void unknownRunTime(String name) { - if (ImageInfo.inImageRuntimeCode()) { + if (ImageMode.current() == ImageMode.NATIVE_RUN) { // only warn at run time for native images, otherwise the user will get warned twice for every property unknown(name); } @@ -118,7 +118,7 @@ public static void unknownRunTime(NameIterator name) { } public static void unknownPropertiesRuntime(List properties) { - if (ImageInfo.inImageRuntimeCode()) { + if (ImageMode.current() == ImageMode.NATIVE_RUN) { unknownProperties(properties); } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java index 8c872044359bd..4a956e57bba69 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java @@ -26,7 +26,6 @@ import java.util.logging.LogManager; import java.util.logging.LogRecord; -import org.graalvm.nativeimage.ImageInfo; import org.jboss.logmanager.EmbeddedConfigurator; import org.jboss.logmanager.LogContext; import org.jboss.logmanager.Logger; @@ -44,6 +43,7 @@ import io.quarkus.bootstrap.logging.InitialConfigurator; import io.quarkus.dev.console.CurrentAppExceptionHighlighter; import io.quarkus.dev.testing.ExceptionReporting; +import io.quarkus.runtime.ImageMode; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; @@ -492,7 +492,7 @@ private static void addNamedHandlersToRootHandlers(Optional> handle } public void initializeLoggingForImageBuild() { - if (ImageInfo.inImageBuildtimeCode()) { + if (ImageMode.current() == ImageMode.NATIVE_BUILD) { final ConsoleHandler handler = new ConsoleHandler(new PatternFormatter( "%d{HH:mm:ss,SSS} %-5p [%c{1.}] %s%e%n")); handler.setLevel(Level.INFO); diff --git a/devtools/cli/src/main/java/io/quarkus/cli/CreateApp.java b/devtools/cli/src/main/java/io/quarkus/cli/CreateApp.java index 74f30f1996bc7..9cbaaf94e0e40 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/CreateApp.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/CreateApp.java @@ -3,6 +3,7 @@ import java.util.HashSet; import java.util.Set; +import io.quarkus.cli.common.DataOptions; import io.quarkus.cli.common.PropertiesOptions; import io.quarkus.cli.common.TargetQuarkusVersionGroup; import io.quarkus.cli.create.BaseCreateCommand; @@ -31,26 +32,29 @@ public class CreateApp extends BaseCreateCommand { "--extension", "--extensions" }, description = "Extension(s) to add to the project.", split = ",") Set extensions = new HashSet<>(); - @CommandLine.Option(paramLabel = "NAME", names = { "--name" }, description = "Name of the project.") + @CommandLine.Option(order = 2, paramLabel = "NAME", names = { "--name" }, description = "Name of the project.") String name; - @CommandLine.Option(paramLabel = "DESCRIPTION", names = { + @CommandLine.Option(order = 3, paramLabel = "DESCRIPTION", names = { "--description" }, description = "Description of the project.") String description; - @CommandLine.ArgGroup(order = 2, heading = "%nQuarkus version:%n") + @CommandLine.ArgGroup(order = 4, heading = "%nQuarkus version:%n") TargetQuarkusVersionGroup targetQuarkusVersion = new TargetQuarkusVersionGroup(); - @CommandLine.ArgGroup(order = 3, heading = "%nBuild tool (Maven):%n") + @CommandLine.ArgGroup(order = 5, heading = "%nBuild tool (Maven):%n") TargetBuildToolGroup targetBuildTool = new TargetBuildToolGroup(); - @CommandLine.ArgGroup(order = 4, exclusive = false, heading = "%nTarget language:%n") + @CommandLine.ArgGroup(order = 6, exclusive = false, heading = "%nTarget language:%n") TargetLanguageGroup targetLanguage = new TargetLanguageGroup(); - @CommandLine.ArgGroup(order = 5, exclusive = false, heading = "%nCode Generation:%n") + @CommandLine.ArgGroup(order = 7, exclusive = false, heading = "%nCode Generation:%n") CodeGenerationGroup codeGeneration = new CodeGenerationGroup(); - @CommandLine.ArgGroup(order = 6, exclusive = false, validate = false) + @CommandLine.ArgGroup(order = 8, exclusive = false, validate = false) + DataOptions dataOptions = new DataOptions(); + + @CommandLine.ArgGroup(order = 9, exclusive = false, validate = false) PropertiesOptions propertiesOptions = new PropertiesOptions(); @Override @@ -72,6 +76,7 @@ public Integer call() throws Exception { setCodegenOptions(codeGeneration); setValue(CreateProjectKey.PROJECT_NAME, name); setValue(CreateProjectKey.PROJECT_DESCRIPTION, description); + setValue(CreateProjectKey.DATA, dataOptions.data); QuarkusCommandInvocation invocation = build(buildTool, targetQuarkusVersion, propertiesOptions.properties, extensions); @@ -112,6 +117,7 @@ public String toString() { + ", name=" + name + ", description=" + description + ", project=" + super.toString() + + ", data=" + dataOptions.data + ", properties=" + propertiesOptions.properties + '}'; } diff --git a/devtools/cli/src/main/java/io/quarkus/cli/CreateCli.java b/devtools/cli/src/main/java/io/quarkus/cli/CreateCli.java index 4c5788b6d3968..9369dcbec9e2a 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/CreateCli.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/CreateCli.java @@ -3,6 +3,7 @@ import java.util.HashSet; import java.util.Set; +import io.quarkus.cli.common.DataOptions; import io.quarkus.cli.common.PropertiesOptions; import io.quarkus.cli.common.TargetQuarkusVersionGroup; import io.quarkus.cli.create.BaseCreateCommand; @@ -10,6 +11,7 @@ import io.quarkus.cli.create.TargetBuildToolGroup; import io.quarkus.cli.create.TargetGAVGroup; import io.quarkus.cli.create.TargetLanguageGroup; +import io.quarkus.devtools.commands.CreateProject.CreateProjectKey; import io.quarkus.devtools.commands.SourceType; import io.quarkus.devtools.commands.data.QuarkusCommandInvocation; import io.quarkus.devtools.commands.handlers.CreateJBangProjectCommandHandler; @@ -30,19 +32,29 @@ public class CreateCli extends BaseCreateCommand { "--extension", "--extensions" }, description = "Extension(s) to add to the project.", split = ",") Set extensions = new HashSet<>(); - @CommandLine.ArgGroup(order = 2, heading = "%nQuarkus version:%n") + @CommandLine.Option(order = 2, paramLabel = "NAME", names = { "--name" }, description = "Name of the project.") + String name; + + @CommandLine.Option(order = 3, paramLabel = "DESCRIPTION", names = { + "--description" }, description = "Description of the project.") + String description; + + @CommandLine.ArgGroup(order = 4, heading = "%nQuarkus version:%n") TargetQuarkusVersionGroup targetQuarkusVersion = new TargetQuarkusVersionGroup(); - @CommandLine.ArgGroup(order = 3, heading = "%nBuild tool (Maven):%n") + @CommandLine.ArgGroup(order = 5, heading = "%nBuild tool (Maven):%n") TargetBuildToolGroup targetBuildTool = new TargetBuildToolGroup(); - @CommandLine.ArgGroup(order = 4, exclusive = false, heading = "%nTarget language:%n") + @CommandLine.ArgGroup(order = 6, exclusive = false, heading = "%nTarget language:%n") TargetLanguageGroup targetLanguage = new TargetLanguageGroup(); - @CommandLine.ArgGroup(order = 5, exclusive = false, heading = "%nCode Generation:%n") + @CommandLine.ArgGroup(order = 7, exclusive = false, heading = "%nCode Generation:%n") CodeGenerationGroup codeGeneration = new CodeGenerationGroup(); - @CommandLine.ArgGroup(order = 6, exclusive = false, validate = false) + @CommandLine.ArgGroup(order = 8, exclusive = false, validate = false) + DataOptions dataOptions = new DataOptions(); + + @CommandLine.ArgGroup(order = 9, exclusive = false, validate = false) PropertiesOptions propertiesOptions = new PropertiesOptions(); @Override @@ -64,6 +76,9 @@ public Integer call() throws Exception { setJavaVersion(sourceType, targetLanguage.getJavaVersion()); setSourceTypeExtensions(extensions, sourceType); setCodegenOptions(codeGeneration); + setValue(CreateProjectKey.PROJECT_NAME, name); + setValue(CreateProjectKey.PROJECT_DESCRIPTION, description); + setValue(CreateProjectKey.DATA, dataOptions.data); QuarkusCommandInvocation invocation = build(buildTool, targetQuarkusVersion, propertiesOptions.properties, extensions); @@ -101,6 +116,7 @@ public String toString() { + ", codeGeneration=" + codeGeneration + ", extensions=" + extensions + ", project=" + super.toString() + + ", data=" + dataOptions.data + ", properties=" + propertiesOptions.properties + '}'; } diff --git a/devtools/cli/src/main/java/io/quarkus/cli/common/DataOptions.java b/devtools/cli/src/main/java/io/quarkus/cli/common/DataOptions.java new file mode 100644 index 0000000000000..9cc70fee084ae --- /dev/null +++ b/devtools/cli/src/main/java/io/quarkus/cli/common/DataOptions.java @@ -0,0 +1,21 @@ +package io.quarkus.cli.common; + +import java.util.HashMap; +import java.util.Map; + +import picocli.CommandLine; + +public class DataOptions { + + public Map data = new HashMap<>(); + + @CommandLine.Option(names = "--data", mapFallbackValue = "", description = "Additional data") + void setData(Map data) { + this.data = data; + } + + @Override + public String toString() { + return data.toString(); + } +} diff --git a/devtools/cli/src/test/java/io/quarkus/cli/CliCreateExtensionTest.java b/devtools/cli/src/test/java/io/quarkus/cli/CliCreateExtensionTest.java index 160a2edbafe92..7ec866320be8f 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliCreateExtensionTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliCreateExtensionTest.java @@ -172,7 +172,7 @@ String validateBasicIdentifiers(Path pom, String group, String artifact, String Assertions.assertTrue(pom.toFile().exists(), "pom.xml should exist: " + pom.toAbsolutePath().toString()); - String pomContent = CliDriver.readFileAsString(project, pom); + String pomContent = CliDriver.readFileAsString(pom); Assertions.assertTrue(pomContent.contains("" + group + ""), pom + " should contain group id " + group + ":\n" + pomContent); Assertions.assertTrue(pomContent.contains("" + artifact + ""), diff --git a/devtools/cli/src/test/java/io/quarkus/cli/CliDriver.java b/devtools/cli/src/test/java/io/quarkus/cli/CliDriver.java index 8ed984f892df2..6bbbc63a26463 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliDriver.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliDriver.java @@ -250,7 +250,7 @@ public static void deleteDir(Path path) throws Exception { Assertions.assertFalse(path.toFile().exists()); } - public static String readFileAsString(Path projectRoot, Path path) throws Exception { + public static String readFileAsString(Path path) throws Exception { return new String(Files.readAllBytes(path)); } @@ -293,7 +293,7 @@ public static Result invokeExtensionAddQute(Path projectRoot, Path file) throws Assertions.assertTrue(result.stdout.contains("quarkus-qute"), "Expected quarkus-qute to be in the list of extensions. Result:\n" + result); - String content = readFileAsString(projectRoot, file); + String content = readFileAsString(file); Assertions.assertTrue(content.contains("quarkus-qute"), "quarkus-qute should be listed as a dependency. Result:\n" + content); @@ -311,7 +311,7 @@ public static Result invokeExtensionRemoveQute(Path projectRoot, Path file) thro Assertions.assertFalse(result.stdout.contains("quarkus-qute"), "Expected quarkus-qute to be missing from the list of extensions. Result:\n" + result); - String content = readFileAsString(projectRoot, file); + String content = readFileAsString(file); Assertions.assertFalse(content.contains("quarkus-qute"), "quarkus-qute should not be listed as a dependency. Result:\n" + content); @@ -333,7 +333,7 @@ public static Result invokeExtensionAddMultiple(Path projectRoot, Path file) thr Assertions.assertTrue(result.stdout.contains("quarkus-jackson"), "Expected quarkus-jackson to be in the list of extensions. Result:\n" + result); - String content = CliDriver.readFileAsString(projectRoot, file); + String content = CliDriver.readFileAsString(file); Assertions.assertTrue(content.contains("quarkus-qute"), "quarkus-qute should still be listed as a dependency. Result:\n" + content); Assertions.assertTrue(content.contains("quarkus-amazon-lambda-http"), @@ -359,7 +359,7 @@ public static Result invokeExtensionRemoveMultiple(Path projectRoot, Path file) Assertions.assertFalse(result.stdout.contains("quarkus-jackson"), "quarkus-jackson should not be in the list of extensions. Result:\n" + result); - String content = CliDriver.readFileAsString(projectRoot, file); + String content = CliDriver.readFileAsString(file); Assertions.assertFalse(content.contains("quarkus-qute"), "quarkus-qute should not be listed as a dependency. Result:\n" + content); Assertions.assertFalse(content.contains("quarkus-amazon-lambda-http"), @@ -455,7 +455,7 @@ public static void validateApplicationProperties(Path projectRoot, List Path properties = projectRoot.resolve("src/main/resources/application.properties"); Assertions.assertTrue(properties.toFile().exists(), "application.properties should exist: " + properties.toAbsolutePath().toString()); - String propertiesFile = CliDriver.readFileAsString(projectRoot, properties); + String propertiesFile = CliDriver.readFileAsString(properties); configs.forEach(conf -> Assertions.assertTrue(propertiesFile.contains(conf), "Properties file should contain " + conf + ". Found:\n" + propertiesFile)); } diff --git a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java index 0a99027fe806b..51ac69761cb79 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java @@ -207,7 +207,7 @@ public void testExtensionList() throws Exception { Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); Path buildGradle = project.resolve("build.gradle"); - String buildGradleContent = CliDriver.readFileAsString(project, buildGradle); + String buildGradleContent = CliDriver.readFileAsString(buildGradle); Assertions.assertFalse(buildGradleContent.contains("quarkus-qute"), "Dependencies should not contain qute extension by default. Found:\n" + buildGradleContent); @@ -379,7 +379,7 @@ public void testCreateArgJava11() throws Exception { Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); Path buildGradle = project.resolve("build.gradle"); - String buildGradleContent = CliDriver.readFileAsString(project, buildGradle); + String buildGradleContent = CliDriver.readFileAsString(buildGradle); Assertions.assertTrue(buildGradleContent.contains("sourceCompatibility = JavaVersion.VERSION_11"), "Java 11 should be used when specified. Found:\n" + buildGradle); } @@ -394,7 +394,7 @@ public void testCreateArgJava17() throws Exception { Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); Path buildGradle = project.resolve("build.gradle"); - String buildGradleContent = CliDriver.readFileAsString(project, buildGradle); + String buildGradleContent = CliDriver.readFileAsString(buildGradle); Assertions.assertTrue(buildGradleContent.contains("sourceCompatibility = JavaVersion.VERSION_17"), "Java 17 should be used when specified. Found:\n" + buildGradleContent); @@ -405,7 +405,7 @@ String validateBasicGradleGroovyIdentifiers(Path project, String group, String a Assertions.assertTrue(buildGradle.toFile().exists(), "build.gradle should exist: " + buildGradle.toAbsolutePath().toString()); - String buildContent = CliDriver.readFileAsString(project, buildGradle); + String buildContent = CliDriver.readFileAsString(buildGradle); Assertions.assertTrue(buildContent.contains("group '" + group + "'"), "build.gradle should include the group id:\n" + buildContent); Assertions.assertTrue(buildContent.contains("version '" + version + "'"), @@ -414,7 +414,7 @@ String validateBasicGradleGroovyIdentifiers(Path project, String group, String a Path settings = project.resolve("settings.gradle"); Assertions.assertTrue(settings.toFile().exists(), "settings.gradle should exist: " + settings.toAbsolutePath().toString()); - String settingsContent = CliDriver.readFileAsString(project, settings); + String settingsContent = CliDriver.readFileAsString(settings); Assertions.assertTrue(settingsContent.contains(artifact), "settings.gradle should include the artifact id:\n" + settingsContent); @@ -426,7 +426,7 @@ String validateBasicGradleKotlinIdentifiers(Path project, String group, String a Assertions.assertTrue(buildGradle.toFile().exists(), "build.gradle.kts should exist: " + buildGradle.toAbsolutePath().toString()); - String buildContent = CliDriver.readFileAsString(project, buildGradle); + String buildContent = CliDriver.readFileAsString(buildGradle); Assertions.assertTrue(buildContent.contains("group = \"" + group + "\""), "build.gradle.kts should include the group id:\n" + buildContent); Assertions.assertTrue(buildContent.contains("version = \"" + version + "\""), @@ -435,7 +435,7 @@ String validateBasicGradleKotlinIdentifiers(Path project, String group, String a Path settings = project.resolve("settings.gradle.kts"); Assertions.assertTrue(settings.toFile().exists(), "settings.gradle.kts should exist: " + settings.toAbsolutePath().toString()); - String settingsContent = CliDriver.readFileAsString(project, settings); + String settingsContent = CliDriver.readFileAsString(settings); Assertions.assertTrue(settingsContent.contains(artifact), "settings.gradle.kts should include the artifact id:\n" + settingsContent); diff --git a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectJBangTest.java b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectJBangTest.java index e41807efd0574..d085158c37492 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectJBangTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectJBangTest.java @@ -53,7 +53,7 @@ public void testCreateAppDefaults() throws Exception { Path javaMain = valdiateJBangSourcePackage(project, ""); // no package name - String source = CliDriver.readFileAsString(project, javaMain); + String source = CliDriver.readFileAsString(javaMain); Assertions.assertTrue(source.contains("quarkus-resteasy"), "Generated source should reference resteasy. Found:\n" + source); @@ -89,7 +89,7 @@ public void testCreateAppOverrides() throws Exception { validateBasicIdentifiers(project, "silly", "my-project", "0.1.0"); Path javaMain = valdiateJBangSourcePackage(project, ""); - String source = CliDriver.readFileAsString(project, javaMain); + String source = CliDriver.readFileAsString(javaMain); Assertions.assertTrue(source.contains("quarkus-reactive-routes"), "Generated source should reference quarkus-reactive-routes. Found:\n" + source); @@ -114,7 +114,7 @@ public void testCreateCliDefaults() throws Exception { Path javaMain = valdiateJBangSourcePackage(project, ""); // no package name - String source = CliDriver.readFileAsString(project, javaMain); + String source = CliDriver.readFileAsString(javaMain); Assertions.assertFalse(source.contains("quarkus-resteasy"), "Generated source should reference resteasy. Found:\n" + source); Assertions.assertTrue(source.contains("quarkus-picocli"), @@ -178,7 +178,7 @@ public void testCreateArgJava11() throws Exception { Path javaMain = valdiateJBangSourcePackage(project, ""); // no package name - String source = CliDriver.readFileAsString(project, javaMain); + String source = CliDriver.readFileAsString(javaMain); Assertions.assertTrue(source.contains("//JAVA 11"), "Generated source should contain //JAVA 11. Found:\n" + source); } @@ -193,7 +193,7 @@ public void testCreateArgJava17() throws Exception { Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); Path javaMain = valdiateJBangSourcePackage(project, ""); // no package name - String source = CliDriver.readFileAsString(project, javaMain); + String source = CliDriver.readFileAsString(javaMain); Assertions.assertTrue(source.contains("//JAVA 17"), "Generated source should contain //JAVA 17. Found:\n" + source); } diff --git a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java index 796ecd0acb71c..4d74100b21eaa 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java @@ -77,11 +77,13 @@ public void testCreateAppOverrides() throws Exception { List configs = Arrays.asList("custom.app.config1=val1", "custom.app.config2=val2", "lib.config=val3"); + List data = Arrays.asList("resteasy-reactive-codestart.resource.response=An awesome response"); CliDriver.Result result = CliDriver.execute(workspaceRoot, "create", "app", "--verbose", "-e", "-B", "--no-wrapper", "--package-name=custom.pkg", "--output-directory=" + nested, "--app-config=" + String.join(",", configs), + "--data=" + String.join(",", data), "-x resteasy-reactive,micrometer-registry-prometheus", "silly:my-project:0.1.0"); @@ -110,6 +112,9 @@ public void testCreateAppOverrides() throws Exception { Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code. " + result); Assertions.assertTrue(result.stdout.contains("WARN"), "Expected a warning that the directory already exists. " + result); + + String greetingResource = CliDriver.readFileAsString(project.resolve("src/main/java/custom/pkg/GreetingResource.java")); + Assertions.assertTrue(greetingResource.contains("return \"An awesome response\";")); } @Test @@ -118,7 +123,7 @@ public void testExtensionList() throws Exception { Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); Path pom = project.resolve("pom.xml"); - String pomContent = CliDriver.readFileAsString(project, pom); + String pomContent = CliDriver.readFileAsString(pom); Assertions.assertFalse(pomContent.contains("quarkus-qute"), "Dependencies should not contain qute extension by default. Found:\n" + pomContent); @@ -329,7 +334,7 @@ public void testCreateArgJava11() throws Exception { Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); Path pom = project.resolve("pom.xml"); - String pomContent = CliDriver.readFileAsString(project, pom); + String pomContent = CliDriver.readFileAsString(pom); Assertions.assertTrue(pomContent.contains("maven.compiler.release>11<"), "Java 11 should be used when specified. Found:\n" + pomContent); @@ -345,7 +350,7 @@ public void testCreateArgJava17() throws Exception { Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); Path pom = project.resolve("pom.xml"); - String pomContent = CliDriver.readFileAsString(project, pom); + String pomContent = CliDriver.readFileAsString(pom); Assertions.assertTrue(pomContent.contains("maven.compiler.release>17<"), "Java 17 should be used when specified. Found:\n" + pomContent); @@ -377,7 +382,7 @@ String validateBasicIdentifiers(String group, String artifact, String version) t Assertions.assertTrue(pom.toFile().exists(), "pom.xml should exist: " + pom.toAbsolutePath().toString()); - String pomContent = CliDriver.readFileAsString(project, pom); + String pomContent = CliDriver.readFileAsString(pom); Assertions.assertTrue(pomContent.contains("" + group + ""), "pom.xml should contain group id:\n" + pomContent); Assertions.assertTrue(pomContent.contains("" + artifact + ""), diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java index b07bf55b1c73f..3f64ea80e5d0b 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java @@ -572,7 +572,7 @@ private void addLocalProject(ResolvedDependency project, GradleDevModeLauncher.B } } Path classesDir = classesDirs.isEmpty() ? null - : QuarkusGradleUtils.mergeClassesDirs(classesDirs, project.getWorkspaceModule().getBuildDir(), root, root); + : QuarkusGradleUtils.mergeClassesDirs(classesDirs, project.getWorkspaceModule().getBuildDir(), root, false); final Set resourcesSrcDirs = new LinkedHashSet<>(); // resourcesSrcDir may exist but if it's empty the resources output dir won't be created diff --git a/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java b/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java index caae3567578a1..0edaafdaf50d0 100644 --- a/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java +++ b/devtools/gradle/gradle-application-plugin/src/test/java/io/quarkus/gradle/QuarkusPluginTest.java @@ -99,7 +99,7 @@ public void shouldReturnMultipleOutputSourceDirectories() { @Test public void shouldNotFailOnProjectDependenciesWithoutMain() throws IOException { - var kotlinVersion = System.getProperty("kotlin_version", "1.7.21"); + var kotlinVersion = System.getProperty("kotlin_version", "1.7.22"); var settingFile = testProjectDir.resolve("settings.gradle.kts"); var mppProjectDir = testProjectDir.resolve("mpp"); var quarkusProjectDir = testProjectDir.resolve("quarkus"); diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java index d9788a7117920..fea4982a42529 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java @@ -9,9 +9,11 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Properties; import java.util.Set; @@ -27,7 +29,9 @@ import org.gradle.api.initialization.IncludedBuild; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskCollection; import org.gradle.api.tasks.compile.AbstractCompile; +import org.gradle.api.tasks.testing.Test; import org.gradle.internal.composite.IncludedBuildInternal; import org.gradle.language.jvm.tasks.ProcessResources; import org.gradle.tooling.provider.model.ParameterizedToolingModelBuilder; @@ -94,7 +98,8 @@ public Object buildAll(String modelName, Project project) { public Object buildAll(String modelName, ModelParameter parameter, Project project) { final LaunchMode mode = LaunchMode.valueOf(parameter.getMode()); - final ApplicationDeploymentClasspathBuilder classpathBuilder = new ApplicationDeploymentClasspathBuilder(project, mode); + final ApplicationDeploymentClasspathBuilder classpathBuilder = new ApplicationDeploymentClasspathBuilder(project, + mode); final Configuration classpathConfig = classpathBuilder.getRuntimeConfiguration(); final Configuration deploymentConfig = classpathBuilder.getDeploymentConfiguration(); final PlatformImports platformImports = classpathBuilder.getPlatformImports(); @@ -127,18 +132,41 @@ public static ResolvedDependency getProjectArtifact(Project project, boolean wor .setArtifactId(project.getName()) .setVersion(project.getVersion().toString()); - SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + final SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); final WorkspaceModule.Mutable mainModule = WorkspaceModule.builder() .setModuleId(new GAV(appArtifact.getGroupId(), appArtifact.getArtifactId(), appArtifact.getVersion())) .setModuleDir(project.getProjectDir().toPath()) .setBuildDir(project.getBuildDir().toPath()) .setBuildFile(project.getBuildFile().toPath()); - initProjectModule(project, mainModule, sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME), - SourceSet.MAIN_SOURCE_SET_NAME, ArtifactSources.MAIN); + initProjectModule(project, mainModule, sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME), ArtifactSources.MAIN); if (workspaceDiscovery) { - initProjectModule(project, mainModule, sourceSets.getByName(SourceSet.TEST_SOURCE_SET_NAME), - SourceSet.TEST_SOURCE_SET_NAME, ArtifactSources.TEST); + final TaskCollection testTasks = project.getTasks().withType(Test.class); + if (!testTasks.isEmpty()) { + final Map sourceSetsByClassesDir = new HashMap<>(); + sourceSets.forEach(s -> { + s.getOutput().getClassesDirs().forEach(d -> { + if (d.exists()) { + sourceSetsByClassesDir.put(d, s); + } + }); + }); + testTasks.forEach(t -> { + if (t.getEnabled()) { + t.getTestClassesDirs().forEach(d -> { + if (d.exists()) { + final SourceSet sourceSet = sourceSetsByClassesDir.remove(d); + if (sourceSet != null) { + initProjectModule(project, mainModule, sourceSet, + sourceSet.getName().equals(SourceSet.TEST_SOURCE_SET_NAME) + ? ArtifactSources.TEST + : sourceSet.getName()); + } + } + }); + } + }); + } } final PathList.Builder paths = PathList.builder(); @@ -150,12 +178,8 @@ public static ResolvedDependency getProjectArtifact(Project project, boolean wor private static void collectDestinationDirs(Collection sources, final PathList.Builder paths) { for (SourceDir src : sources) { - if (!Files.exists(src.getOutputDir())) { - continue; - } - final Path path = src.getOutputDir(); - if (paths.contains(path)) { + if (paths.contains(path) || !Files.exists(path)) { continue; } paths.add(path); @@ -314,7 +338,7 @@ private void collectDependencies(org.gradle.api.artifacts.ResolvedDependency res paths = pathBuilder.build(); } } else if (sourceSets != null) { - if ("test".equals(classifier)) { + if (SourceSet.TEST_SOURCE_SET_NAME.equals(classifier)) { final PathList.Builder pathBuilder = PathList.builder(); projectModule = initProjectModuleAndBuildPaths(projectDep, a, modelBuilder, depBuilder, pathBuilder, SourceSet.TEST_SOURCE_SET_NAME, true); @@ -347,7 +371,7 @@ private void collectDependencies(org.gradle.api.artifacts.ResolvedDependency res } } - processedModules.add(new GACT(resolvedDep.getModuleGroup(), resolvedDep.getModuleName())); + processedModules.add(ArtifactKey.ga(resolvedDep.getModuleGroup(), resolvedDep.getModuleName())); for (org.gradle.api.artifacts.ResolvedDependency child : resolvedDep.getChildren()) { if (!processedModules.contains(new GACT(child.getModuleGroup(), child.getModuleName()))) { collectDependencies(child, workspaceDiscovery, project, artifactFiles, processedModules, @@ -357,7 +381,7 @@ private void collectDependencies(org.gradle.api.artifacts.ResolvedDependency res } private static String toNonNullClassifier(String resolvedClassifier) { - return resolvedClassifier == null ? "" : resolvedClassifier; + return resolvedClassifier == null ? ArtifactCoords.DEFAULT_CLASSIFIER : resolvedClassifier; } private WorkspaceModule.Mutable initProjectModuleAndBuildPaths(final Project project, @@ -375,13 +399,13 @@ private WorkspaceModule.Mutable initProjectModuleAndBuildPaths(final Project pro final String classifier = toNonNullClassifier(resolvedArtifact.getClassifier()); SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); - initProjectModule(project, projectModule, sourceSets.findByName(sourceName), sourceName, classifier); + initProjectModule(project, projectModule, sourceSets.findByName(sourceName), classifier); collectDestinationDirs(projectModule.getSources(classifier).getSourceDirs(), buildPaths); collectDestinationDirs(projectModule.getSources(classifier).getResourceDirs(), buildPaths); appModel.addReloadableWorkspaceModule( - new GACT(resolvedArtifact.getModuleVersion().getId().getGroup(), resolvedArtifact.getName(), classifier, + ArtifactKey.of(resolvedArtifact.getModuleVersion().getId().getGroup(), resolvedArtifact.getName(), classifier, ArtifactCoords.TYPE_JAR)); return projectModule; } @@ -445,7 +469,7 @@ private static Properties readDescriptor(final Path path) { } private static void initProjectModule(Project project, WorkspaceModule.Mutable module, SourceSet sourceSet, - String sourceName, String classifier) { + String classifier) { if (sourceSet == null) { return; @@ -456,8 +480,6 @@ private static void initProjectModule(Project project, WorkspaceModule.Mutable m // see https://github.com/quarkusio/quarkus/issues/20755 final List sourceDirs = new ArrayList<>(1); - final List resourceDirs = new ArrayList<>(1); - project.getTasks().withType(AbstractCompile.class, t -> { if (!t.getEnabled()) { return; @@ -474,9 +496,7 @@ private static void initProjectModule(Project project, WorkspaceModule.Mutable m // we are looking for the root dirs containing sources if (a.getRelativePath().getSegments().length == 1) { final File srcDir = a.getFile().getParentFile(); - DefaultSourceDir sources = new DefaultSourceDir(srcDir.toPath(), destDir.toPath(), - Collections.singletonMap("compiler", t.getName())); - sourceDirs.add(sources); + sourceDirs.add(new DefaultSourceDir(srcDir.toPath(), destDir.toPath(), Map.of("compiler", t.getName()))); } }); }); @@ -500,9 +520,8 @@ private static void initProjectModule(Project project, WorkspaceModule.Mutable m // we are looking for the root dirs containing sources if (a.getRelativePath().getSegments().length == 1) { final File srcDir = a.getFile().getParentFile(); - DefaultSourceDir sources = new DefaultSourceDir(srcDir.toPath(), destDir.toPath(), - Collections.singletonMap("compiler", t.getName())); - sourceDirs.add(sources); + sourceDirs + .add(new DefaultSourceDir(srcDir.toPath(), destDir.toPath(), Map.of("compiler", t.getName()))); } }); }); @@ -510,8 +529,8 @@ private static void initProjectModule(Project project, WorkspaceModule.Mutable m // ignore } + final LinkedHashMap resourceDirs = new LinkedHashMap<>(1); final File resourcesOutputDir = sourceSet.getOutput().getResourcesDir(); - project.getTasks().withType(ProcessResources.class, t -> { if (!t.getEnabled()) { return; @@ -528,16 +547,20 @@ private static void initProjectModule(Project project, WorkspaceModule.Mutable m // we are looking for the root dirs containing sources if (a.getRelativePath().getSegments().length == 1) { final File srcDir = a.getFile().getParentFile(); - resourceDirs.add(new DefaultSourceDir(srcDir.toPath(), destDir)); + resourceDirs.put(srcDir, destDir); } }); }); // there could be a task generating resources if (resourcesOutputDir.exists() && resourceDirs.isEmpty()) { sourceSet.getResources().getSrcDirs() - .forEach(srcDir -> resourceDirs.add(new DefaultSourceDir(srcDir.toPath(), resourcesOutputDir.toPath()))); + .forEach(srcDir -> resourceDirs.put(srcDir, resourcesOutputDir.toPath())); + } + final List resources = new ArrayList<>(resourceDirs.size()); + for (Map.Entry e : resourceDirs.entrySet()) { + resources.add(new DefaultSourceDir(e.getKey().toPath(), e.getValue())); } - module.addArtifactSources(new DefaultArtifactSources(classifier, sourceDirs, resourceDirs)); + module.addArtifactSources(new DefaultArtifactSources(classifier, sourceDirs, resources)); } private void addSubstitutedProject(PathList.Builder paths, File projectFile) { diff --git a/devtools/gradle/gradle.properties b/devtools/gradle/gradle.properties index cc722b7c20b3e..133e709814ac5 100644 --- a/devtools/gradle/gradle.properties +++ b/devtools/gradle/gradle.properties @@ -1,2 +1,2 @@ version = 999-SNAPSHOT -kotlin_version = 1.7.21 +kotlin_version = 1.7.22 diff --git a/devtools/gradle/settings.gradle b/devtools/gradle/settings.gradle index 973791f98f4be..f56156959a984 100644 --- a/devtools/gradle/settings.gradle +++ b/devtools/gradle/settings.gradle @@ -1,5 +1,5 @@ plugins { - id "com.gradle.enterprise" version "3.11.4" + id "com.gradle.enterprise" version "3.12.1" } gradleEnterprise { diff --git a/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java index 21932062a482c..2c3ed1d1769f1 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java @@ -187,6 +187,9 @@ public class CreateProjectMojo extends AbstractMojo { @Parameter(property = "appConfig") private String appConfig; + @Parameter(property = "data") + private String data; + @Override public void execute() throws MojoExecutionException { @@ -304,7 +307,8 @@ public void execute() throws MojoExecutionException { .resourcePath(path) .example(example) .noCode(noCode) - .appConfig(appConfig); + .appConfig(appConfig) + .data(data); success = createProject.execute().isSuccess(); if (success && parentPomModel != null && BuildTool.MAVEN.equals(buildToolEnum)) { diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/base/README.tpl.qute.md b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/base/README.tpl.qute.md index 405280158877d..312518fd91788 100644 --- a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/base/README.tpl.qute.md +++ b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/base/README.tpl.qute.md @@ -1 +1,9 @@ -{#include readme-header /} \ No newline at end of file +{#include readme-header /} + +{#if input.selected-extensions-ga.contains('io.quarkus:quarkus-hibernate-orm-panache')} +[Related Hibernate with Panache section...](https://quarkus.io/guides/hibernate-orm-panache) +{/if} + +{#if input.selected-extensions-ga.contains('io.quarkus:quarkus-hibernate-orm-panache-kotlin')} +[Related Hibernate with Panache in Kotlin section...](https://quarkus.io/guides/hibernate-orm-panache-kotlin) +{/if} \ No newline at end of file diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/java/src/main/java/org/acme/MyEntity.java b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/java/src/main/java/org/acme/MyEntity.java deleted file mode 100644 index c28a608e6020e..0000000000000 --- a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/java/src/main/java/org/acme/MyEntity.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.acme; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; - -/** - * Example JPA entity. - * - * To use it, get access to a JPA EntityManager via injection. - * - * {@code - * @Inject - * EntityManager em; - * - * public void doSomething() { - * MyEntity entity1 = new MyEntity(); - * entity1.setField("field-1"); - * em.persist(entity1); - * - * List entities = em.createQuery("from MyEntity", MyEntity.class).getResultList(); - * } - * } - */ -@Entity -public class MyEntity { - private Long id; - private String field; - - @Id - @GeneratedValue - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getField() { - return field; - } - - public void setField(String field) { - this.field = field; - } -} diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/java/src/main/java/org/acme/MyEntity.tpl.qute.java b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/java/src/main/java/org/acme/MyEntity.tpl.qute.java new file mode 100644 index 0000000000000..ae11050e3e1c8 --- /dev/null +++ b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/java/src/main/java/org/acme/MyEntity.tpl.qute.java @@ -0,0 +1,63 @@ +package org.acme; + +{#if !input.selected-extensions-ga.contains('io.quarkus:quarkus-hibernate-orm-panache')} +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +/** + * Example JPA entity. + * + * To use it, get access to a JPA EntityManager via injection. + * + * \{@code + * @Inject + * EntityManager em; + * + * public void doSomething() { + * MyEntity entity1 = new MyEntity(); + * entity1.field = "field-1"; + * em.persist(entity1); + * + * List entities = em.createQuery("from MyEntity", MyEntity.class).getResultList(); + * } + * } + */ +@Entity +public class MyEntity { + @Id + @GeneratedValue + public Long id; + + public String field; +} +{/if} +{#if input.selected-extensions-ga.contains('io.quarkus:quarkus-hibernate-orm-panache')} +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import javax.persistence.Entity; + + +/** + * Example JPA entity defined as a Panache Entity. + * An ID field of Long type is provided, if you want to define your own ID field extends PanacheEntityBase instead. + * + * This uses the active record pattern, you can also use the repository pattern instead: + * {@see https://quarkus.io/guides/hibernate-orm-panache#solution-2-using-the-repository-pattern}. + * + * Usage (more example on the documentation) + * + * \{@code + * public void doSomething() { + * MyEntity entity1 = new MyEntity(); + * entity1.field = "field-1"; + * entity1.persist(); + * + * List entities = MyEntity.listAll(); + * } + * } + */ +@Entity +public class MyEntity extends PanacheEntity { + public String field; +} +{/if} \ No newline at end of file diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/kotlin/src/main/kotlin/org/acme/MyKotlinEntity.tpl.qute.kt b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/kotlin/src/main/kotlin/org/acme/MyKotlinEntity.tpl.qute.kt new file mode 100644 index 0000000000000..71793b36ebafb --- /dev/null +++ b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/kotlin/src/main/kotlin/org/acme/MyKotlinEntity.tpl.qute.kt @@ -0,0 +1,67 @@ +package org.acme + +{#if !input.selected-extensions-ga.contains('io.quarkus:quarkus-hibernate-orm-panache-kotlin')} +import javax.persistence.Entity +import javax.persistence.GeneratedValue +import javax.persistence.Id + +/** + * Example JPA entity. + * + * To use it, get access to a JPA EntityManager via injection. + * + * \{@code + * @Inject + * lateinit var em:EntityManager; + * + * fun doSomething() { + * val entity1 = MyKotlinEntity(); + * entity1.field = "field-1" + * em.persist(entity1); + * + * val entities:List = em.createQuery("from MyEntity", MyKotlinEntity::class.java).getResultList() + * } + * } + */ +@Entity +class MyKotlinEntity { + @get:GeneratedValue + @get:Id + var id: Long? = null + var field: String? = null +} +{/if} +{#if input.selected-extensions-ga.contains('io.quarkus:quarkus-hibernate-orm-panache-kotlin')} +import io.quarkus.hibernate.orm.panache.kotlin.PanacheEntity +import io.quarkus.hibernate.orm.panache.kotlin.PanacheCompanion +import javax.persistence.Entity + +/** + * Example JPA entity defined as a Kotlin Panache Entity. + * An ID field of Long type is provided, if you want to define your own ID field extends PanacheEntityBase instead. + * + * This uses the active record pattern, you can also use the repository pattern instead: + * {@see https://quarkus.io/guides/hibernate-orm-panache-kotlin#defining-your-repository}. + * + * Usage (more example on the documentation) + * + * \{@code + * + * fun doSomething() { + * val entity1 = MyKotlinEntity(); + * entity1.field = "field-1" + * entity1.persist() + * + * val entities:List = MyKotlinEntity.listAll() + * } + * } + */ +@Entity +class MyKotlinEntity: PanacheEntity() { + companion object: PanacheCompanion { + fun byName(name: String) = list("name", name) + } + + lateinit var field: String +} +{/if} \ No newline at end of file diff --git a/docs/.vale/fixtures/Quarkus/CaseSensitiveTerms/testinvalid.adoc b/docs/.vale/fixtures/Quarkus/CaseSensitiveTerms/testinvalid.adoc index 45a40f3f95e55..b162583a58e5a 100644 --- a/docs/.vale/fixtures/Quarkus/CaseSensitiveTerms/testinvalid.adoc +++ b/docs/.vale/fixtures/Quarkus/CaseSensitiveTerms/testinvalid.adoc @@ -1,3 +1,9 @@ +assertj +Assertj +Asciidoc +asciidoc +AsciiDoctor +asciidoctor Bios btrfs burner @@ -10,6 +16,8 @@ Cds CDS CGroup classic mode +configmap +configuration map control key csv ctrl @@ -56,7 +64,6 @@ gpl Gpl Graalvm graalVM -gradle Grub gtk Gtk @@ -68,6 +75,7 @@ HyperVisor ia64 IA64 IBM z Systems +ignition config Infiniband Intel Tolapai Iops @@ -79,11 +87,14 @@ iso iso image Itanium2 JBoss.org +jetbrains +Jetbrains Junit junit jvm Jvm kernel-based virtual machine +knowledgebase kickstart kvm lan @@ -94,6 +105,7 @@ Microprofile micro-profile MicroSoft mongoDB +Mongo DB Mongodb Mongo-db MS @@ -135,13 +147,15 @@ popup posix Posix Postscript +Powershell +powershell PPC PPC64 ppp Ppp prom Prom -prometheus +proof key for code exchange pSeries q & a Q & A @@ -159,7 +173,10 @@ RAMdisk RAW Red Boot Red Hat Proxy -Red Hat Satellite +Red Hat satellite +Red Hat Satellite server +Red Hat Satellite Capsule server +Red Hat Network Satellite server Redboot resteasy Resteasy @@ -206,7 +223,7 @@ uid UltraSparc ULTRASPARC unix -Unix +unix Socket UNIX-like url var @@ -235,6 +252,8 @@ wca Webauthn webAuthn WebAuthN +Websocket +websocket web-UI webUI Window-Maker diff --git a/docs/.vale/fixtures/Quarkus/CaseSensitiveTerms/testvalid.adoc b/docs/.vale/fixtures/Quarkus/CaseSensitiveTerms/testvalid.adoc index e6c57861f39a6..6a52748268866 100644 --- a/docs/.vale/fixtures/Quarkus/CaseSensitiveTerms/testvalid.adoc +++ b/docs/.vale/fixtures/Quarkus/CaseSensitiveTerms/testvalid.adoc @@ -1,3 +1,6 @@ +AssertJ +AsciiDoc +Asciidoctor /var BIND bind @@ -7,6 +10,7 @@ CapEx CD #1 CDs cgroup +ConfigMap CSV Ctrl Cygmon @@ -43,6 +47,7 @@ IBM eServer System p IBM S 390 IBM S 390 IBM Z +Ignition config InfiniBand Insight Intel Virtualization Technology @@ -56,9 +61,11 @@ ISO image Itanium Itanium 2 JBoss Community +JetBrains JUnit JVM Kernel-based Virtual Machine +Knowledgebase Kickstart KVM LAN @@ -84,9 +91,11 @@ pop-up POSIX PostScript PowerPC +PowerShell PPP PROM Prometheus +Proof Key for Code Exchange Q and A QCOW2 qeth @@ -94,6 +103,9 @@ RAM RAM disk raw Red Hat Network Proxy Server +Red Hat Satellite +Red Hat Satellite Server +Red Hat Satellite Capsule Server Red Hat Network Satellite Server RedBoot RESTEasy @@ -119,7 +131,7 @@ SysV TTL UID UltraSPARC -UNIX +Unix socket URL VAR VDSM @@ -135,6 +147,7 @@ WAN WCA web UI WebAuthn +WebSocket Window Maker XEmacs xterm diff --git a/docs/.vale/fixtures/Quarkus/Headings/testvalid.adoc b/docs/.vale/fixtures/Quarkus/Headings/testvalid.adoc index 87beffbc6f970..1d52f974d2117 100644 --- a/docs/.vale/fixtures/Quarkus/Headings/testvalid.adoc +++ b/docs/.vale/fixtures/Quarkus/Headings/testvalid.adoc @@ -3,3 +3,11 @@ == The Infinispan project == Installing SmallRye to do xyz == Installing RESTEasy with Quarkus +== A Jira instance +== Knative is a component used in Quarkus +== Eclipse Vert.x and Netty are upgraded in Quarkus +== IBM Cloud +== Proof Key for Code Exchange +== Kogito updates +== IBM Cloud is a valid product name +== Spotify, GraphQL, and Quiltflower are proper nouns so uppercase in headings is OK. \ No newline at end of file diff --git a/docs/.vale/fixtures/Quarkus/Spelling/testinvalid.adoc b/docs/.vale/fixtures/Quarkus/Spelling/testinvalid.adoc index 632ae6c4e5f19..6b5927b191732 100644 --- a/docs/.vale/fixtures/Quarkus/Spelling/testinvalid.adoc +++ b/docs/.vale/fixtures/Quarkus/Spelling/testinvalid.adoc @@ -1,8 +1,6 @@ ansible antora api -Asciidoc -asciidoc bitbucket btrfs centos @@ -16,6 +14,7 @@ dns dotnet endevor gluster +gradle Graal VM graal vm gui @@ -33,8 +32,9 @@ kubespray lombok mattermost microsoft azure +mokitto +mokito minikube -Mongo DB Mysql mysql Netcoredebugoutput @@ -50,8 +50,10 @@ postgres podman quarkus restic +Sbt scm svg +symfony uber uri velero diff --git a/docs/.vale/fixtures/Quarkus/Spelling/testvalid.adoc b/docs/.vale/fixtures/Quarkus/Spelling/testvalid.adoc index 81dc15e3a86c1..d23b09e0c50bb 100644 --- a/docs/.vale/fixtures/Quarkus/Spelling/testvalid.adoc +++ b/docs/.vale/fixtures/Quarkus/Spelling/testvalid.adoc @@ -1,12 +1,14 @@ a .NET application +accessor adoc +Agroal Allowlist allowlist Annobin Ansible Antora API -AsciiDoc +AssertJ autostart Autostart aws @@ -15,14 +17,17 @@ Azure backfilling Backfilling backtrace +backported Backtrace Bierner bindable Bindable Bitbucket +BOM Bonjour boolean Boolean +Bouncy Castle breakpoint Breakpoint breakpoints @@ -30,6 +35,7 @@ Breakpoints btn Btrfs Bugzilla +build config CentOS Ceph cephfs @@ -52,6 +58,9 @@ config Config ConfigMap ConfigMaps +config map +config maps +configure Containerfile Containerfiles Cookiecutter @@ -59,10 +68,14 @@ CR CRD CRDs CRs +CSVs Ctrl Cygmon DaemonSet Datadog +declaratively +decompiler +deserialization Dev devfile Devfile @@ -98,9 +111,12 @@ failback Failback failover Failover +Failsafe +Fernflower findability Findability Fortran +Funqy Gbps GCC Git @@ -110,8 +126,10 @@ Gluster GraalVM Gradle Grafana +GraphQL Grayscale GUI +hardcoding Hashicorp Heatmap Helgrind @@ -135,10 +153,12 @@ IntelliJ Itanium Item Jakarta +Jandex Java Jave JBoss -Jetbrains +JetBrains +Jira Jolokia Journald Journaling @@ -149,9 +169,13 @@ JVM Kafka kbd Keycloak +Keylime Keyring Keyrings Kibana +Knative +Knowledgebase +Kogito Kompose Kubespray Kylin @@ -171,6 +195,7 @@ Mebibytes MicroProfile Microsoft Middleware +middleware Millicores Minikube Minishift @@ -178,6 +203,7 @@ Mirantis Mixin Mixins Modularization +Mockito MongoDB Multicluster Multihost @@ -191,8 +217,10 @@ MySQL Nagios Namespace Namespaces +Narayana Neoverse NetcoredebugOutput +Netty Newdoc Nginx Node.js @@ -203,15 +231,22 @@ ocp OmniSharp Onboarding OpenID +OpenJDK OpenShift OpenTracing Operator osd +overridable PHP Podman PostgreSQL +PowerShell preconfigured Preconfigured +pregenerated +prepended +preconfigured +prepend prepended productize productized @@ -222,6 +257,8 @@ Pytorch qeth Quarkiverse Quarkus +Quiltflower +Qute Readonly Rebalance Rebalances @@ -242,24 +279,30 @@ Rollout Rollouts Roundtable Roundtables +ruleset Runlevel Runlevels Runtime Runtimes Sakila +sbt SCM Scrollbar searchability Searchability SELinux Semeru +serialization +serializable Serializer Serverless +servlet Shadowman Sharding SLA SLAs SmallRye +Spotify startx su Subcommand @@ -281,10 +324,12 @@ Subusers Subvolume Subvolumes Suchow +Symfony SVG Systemd Tekton Telekom +Templated Tensorflow Texinfo Theia @@ -311,9 +356,12 @@ url URL URLs Valgrind +validator Velero +Vert.x vsix WebAuthn +WebSocket Webview Webviews Wildfly diff --git a/docs/.vale/fixtures/Quarkus/TermsSuggestions/testinvalid.adoc b/docs/.vale/fixtures/Quarkus/TermsSuggestions/testinvalid.adoc index 0e41dd2f4f0b2..7464c0532dcb4 100644 --- a/docs/.vale/fixtures/Quarkus/TermsSuggestions/testinvalid.adoc +++ b/docs/.vale/fixtures/Quarkus/TermsSuggestions/testinvalid.adoc @@ -1,5 +1,4 @@ and so on -as bare metal bare-metal between diff --git a/docs/.vale/styles/Quarkus/CaseSensitiveTerms.yml b/docs/.vale/styles/Quarkus/CaseSensitiveTerms.yml index 7754fe43cfea5..2b579afbb05d3 100644 --- a/docs/.vale/styles/Quarkus/CaseSensitiveTerms.yml +++ b/docs/.vale/styles/Quarkus/CaseSensitiveTerms.yml @@ -8,6 +8,7 @@ source: Quarkus contributor guide action: name: replace swap: + assertj|Assertj: AssertJ asciidoc|Asciidoc: AsciiDoc asciidoctor|AsciiDoctor: Asciidoctor '[nN]odejs|[nN]ode\.JS|node\.js': Node.js @@ -31,6 +32,7 @@ swap: Disk druid|disk druid|diskdruid: Disk Druid dns: DNS DVD burner|burner: DVD writer + Etcd|ETCD: etcd Exec Shield: Exec-Shield EXIF|exif: Exif Faq|faq|F.A.Q: FAQ @@ -65,10 +67,12 @@ swap: iso: ISO Itanium2: Itanium 2 JBoss.org: JBoss Community + jetbrains|Jetbrains: JetBrains Junit|junit: JUnit Jvm|jvm: JVM kernel-based virtual machine: Kernel-based Virtual Machine kickstart: Kickstart + knowledgebase: Knowledgebase kvm: KVM Lan|lan: LAN LINUX|linux: Linux @@ -91,6 +95,7 @@ swap: popup|Pop-up: pop-up Posix|posix: POSIX Postscript: PostScript + Powershell|powershell: PowerShell PPC|P-PC|PPC64: PowerPC Ppp|ppp: PPP prom|Prom: PROM @@ -109,6 +114,7 @@ swap: Red Hat Satellite Capsule server: Red Hat Satellite Capsule Server Red Hat Network Satellite server: Red Hat Network Satellite Server Redboot|Red Boot: RedBoot + redis: Redis RESTEASY|resteasy|Resteasy: RESTEasy Rom|rom: ROM rpm: RPM @@ -135,7 +141,7 @@ swap: ttl: TTL uid: UID ULTRASPARC|UltraSparc: UltraSPARC - Unix|unix|UNIX-like: UNIX + unix socket|unix Socket|UNIX Socket: Unix socket url: URL "(?runtime - 3.8.1 + 3.10.1 ${surefire-plugin.version} 11 UTF-8 @@ -876,7 +876,7 @@ $ mvn clean compile quarkus:dev [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] Copying 1 resource [INFO] -[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ greeting-app --- +[INFO] --- maven-compiler-plugin:3.10.1:compile (default-compile) @ greeting-app --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- quarkus-maven-plugin:{quarkus-version}:dev (default-cli) @ greeting-app --- diff --git a/docs/src/main/asciidoc/building-native-image.adoc b/docs/src/main/asciidoc/building-native-image.adoc index a390903d28726..2404aa36b1554 100644 --- a/docs/src/main/asciidoc/building-native-image.adoc +++ b/docs/src/main/asciidoc/building-native-image.adoc @@ -326,7 +326,7 @@ Adding `--enable-preview` to its `configuration` section is o === Excluding tests when running as a native executable -When running tests this way, the only things that actually run natively are you application endpoints, which +When running tests this way, the only things that actually run natively are your application endpoints, which you can only test via HTTP calls. Your test code does not actually run natively, so if you are testing code that does not call your HTTP endpoints, it's probably not a good idea to run them as part of native tests. @@ -484,7 +484,7 @@ Then, if you didn't delete the generated native executable, you can build the do [source,bash] ---- -docker build -f src/main/docker/Dockerfile.native -t quarkus-quickstart/getting-started . +docker build -f src/main/docker/Dockerfile.native-micro -t quarkus-quickstart/getting-started . ---- And finally, run it with: @@ -653,7 +653,7 @@ Just add your application on top of this image, and you will get a tiny containe Distroless images should not be used in production without rigorous testing. -=== Using a scratch base image +=== Build a container image from scratch IMPORTANT: Scratch image support is experimental. @@ -671,9 +671,9 @@ COPY --chown=quarkus:quarkus mvnw /code/mvnw COPY --chown=quarkus:quarkus .mvn /code/.mvn COPY --chown=quarkus:quarkus pom.xml /code/ RUN mkdir /musl && \ - curl -L -o musl.tar.gz https://more.musl.cc/10.2.1/x86_64-linux-musl/x86_64-linux-musl-native.tgz && \ + curl -L -o musl.tar.gz https://more.musl.cc/11.2.1/x86_64-linux-musl/x86_64-linux-musl-native.tgz && \ tar -xvzf musl.tar.gz -C /musl --strip-components 1 && \ - curl -L -o zlib.tar.gz https://zlib.net/zlib-1.2.12.tar.gz && \ + curl -L -o zlib.tar.gz https://www.zlib.net/zlib-1.2.13.tar.gz && \ mkdir zlib && tar -xvzf zlib.tar.gz -C zlib --strip-components 1 && \ cd zlib && ./configure --static --prefix=/musl && \ make && make install && \ @@ -683,17 +683,20 @@ USER quarkus WORKDIR /code RUN ./mvnw -B org.apache.maven.plugins:maven-dependency-plugin:3.1.2:go-offline COPY src /code/src -RUN ./mvnw package -Pnative -Dquarkus.native.additional-build-args="--static","--libc=musl" +RUN ./mvnw package -Pnative -DskipTests -Dquarkus.native.additional-build-args="--static","--libc=musl" -## Stage 2 : create the docker final image +## Stage 2 : create the final image FROM scratch COPY --from=build /code/target/*-runner /application +EXPOSE 8080 ENTRYPOINT [ "/application" ] ---- Scratch images should not be used in production without rigorous testing. -=== Native executable compression +NOTE: The versions of musl and zlib may need to be updated to meet the native-image executable requirements (and UPX if you use native image compression). + +=== Compress native images Quarkus can compress the produced native executable using UPX. More details on xref:./upx.adoc[UPX Compression documentation]. diff --git a/docs/src/main/asciidoc/cassandra.adoc b/docs/src/main/asciidoc/cassandra.adoc index 9fe0090191a37..b6ebb0c1790e5 100644 --- a/docs/src/main/asciidoc/cassandra.adoc +++ b/docs/src/main/asciidoc/cassandra.adoc @@ -119,7 +119,7 @@ Note also the special return type of the `findAll` method, link:https://docs.datastax.com/en/drivers/java/latest/com/datastax/oss/driver/api/core/PagingIterable.html[`PagingIterable`]: it's the base type of result sets returned by the driver. -Finally, let's create the Mapper interface: +Finally, let's create a Mapper interface: [source,java] ---- @@ -131,10 +131,93 @@ public interface FruitMapper { ---- The `@Mapper` annotation is yet another annotation recognized by the DataStax Object Mapper. A -mapper is responsible for constructing instances of DAOs – in this case, out mapper is constructing +mapper is responsible for constructing DAO instances – in this case, out mapper is constructing an instance of our only DAO, `FruitDao`. -== Creating a Service & JSON REST Endpoint +Think of the mapper interface as a factory for DAO beans. If you intend to construct and inject a +specific DAO bean in your own code, then you first must add a `@DaoFactory` method for it in a +`@Mapper` interface. + +TIP: `@DaoFactory` method names are irrelevant. + +`@DaoFactory` methods should return beans of the following types: + +- Any `@Dao`-annotated interface, e.g. `FruitDao`; +- A `CompletationStage` of any `@Dao`-annotated interface, e.g. `CompletionStage`. +- A `Uni` of any `@Dao`-annotated interface, e.g. `Uni`. + +TIP: `Uni` is a type from the Mutiny library, which is the reactive programming library used by +Quarkus. This will be explained in more detail in the "Reactive Programming" section below. + +== Generating the DAO and mapper implementations + +As you probably guessed already, we are not going to implement the interfaces above. Instead, the +Object Mapper will generate such implementations for us. + +The Object Mapper is composed of 2 pieces: + +1. A (compile-time) annotation processor that scans the classpath for classes annotated with +`@Mapper`, `@Dao` or `@Entity`, and generates code and CQL queries for them; and +2. A runtime module that contains the logic to execute the generated queries. + +Therefore, enabling the Object Mapper requires two steps: + +1. Declare the `cassandra-quarkus-mapper-processor` annotation processor. With Maven, this is done +by modifying the compiler plugin configuration in the project's `pom.xml` file as follows: + +[source,xml] +---- + + maven-compiler-plugin + 3.10.1 + + ${java.version} + ${java.version} + + + com.datastax.oss.quarkus + cassandra-quarkus-mapper-processor + ${cassandra-quarkus.version} + + + + +---- + +With Gradle, this is done by adding the following line to the `build.gradle` file: + +[source,groovy] +---- +annotationProcessor "com.datastax.oss.quarkus:cassandra-quarkus-mapper-processor:${cassandra-quarkus.version}" +---- + +IMPORTANT: Verify that you are enabling the right annotation processor! The Cassandra driver ships +with its Object Mapper annotation processor, called `java-driver-mapper-processor`. But the +Cassandra Quarkus extension also ships with its own annotation processor: +`cassandra-quarkus-mapper-processor`, which has more capabilities than the driver's. This annotation +processor is the only one suitable for use in a Quarkus application, so check that this is the one +in use. Also, never use both annotation processors together. + +[start=2] +1. Declare the `java-driver-mapper-runtime` dependency in compile scope in the project's `pom.xml` + file as follows: + +[source,xml] +---- + + com.datastax.oss + java-driver-mapper-runtime + +---- + +IMPORTANT: Although this module is called "runtime", it must be declared in compile scope. + +If your project is correctly set up, you should now be able to compile it without errors, and you +should see the generated code in the `target/generated-sources/annotations` directory (if you are +using Maven). It's not required to get familiar with the generated code though, as it is mostly +internal machinery to interact with the database. + +== Creating a service & JSON REST endpoint Now let's create a `FruitService` that will be the business layer of our application and store/load the fruits from the Cassandra database. @@ -157,19 +240,20 @@ public class FruitService { ---- Note how the service is being injected a `FruitDao` instance. This DAO instance is injected -automatically. +automatically, thanks to the generated implementations. The Cassandra Quarkus extension allows you to inject any of the following beans in your own components: - All `@Mapper`-annotated interfaces in your project. -- All `@Dao`-annotated interfaces in your project, as long as they are produced by a corresponding -`@DaoFactory`-annotated method declared in a mapper interface from your project. +- You can also inject a `CompletionStage` or `Uni` of any `@Mapper`-annotated interface. +- Any bean returned by a `@DaoFactory` method (see above for possible bean types). - The link:https://javadoc.io/doc/com.datastax.oss.quarkus/cassandra-quarkus-client/latest/com/datastax/oss/quarkus/runtime/api/session/QuarkusCqlSession.html[`QuarkusCqlSession`] bean: this application-scoped, singleton bean is your main entry point to the Cassandra client; it is a specialized Cassandra driver session instance with a few methods tailored especially for Quarkus. Read its javadocs carefully! +- You can also inject `CompletationStage` or `Uni`. In our example, both `FruitMapper` and `FruitDao` could be injected anywhere. We chose to inject `FruitDao` in `FruitService`. @@ -242,7 +326,7 @@ below snippet to your application's ppm.xml file: io.quarkus - quarkus-resteasy-jackson + quarkus-resteasy-reactive-jackson ---- @@ -723,17 +807,19 @@ you can run the native executable as follows: You can then point your browser to `http://localhost:8080/fruits.html` and use your application. -== Eager vs Lazy Initialization +== Choosing between eager and lazy initialization -This extension allows you to inject either: +As explained above, this extension allows you to inject many types of beans: -- a `QuarkusCqlSession` bean; -- or the asynchronous version of this bean, that is, `CompletionStage`; -- or the reactive version of this bean, that is, `Uni`. +- A simple bean like `QuarkusCqlSession` or `FruitDao`; +- The asynchronous version of that bean, for example `CompletionStage` or + `CompletionStage; +- The reactive version of that bean, for example `Uni` or `Uni`. -The most straightforward approach is obviously to inject `QuarkusCqlSession` directly. This should -work just fine for most applications; however, the `QuarkusCqlSession` bean needs to be initialized -before it can be used, and this process is blocking. +The most straightforward approach is obviously to inject the bean directly. This should work just +fine for most applications. However, the `QuarkusCqlSession` bean, and all DAO beans that depend on +it, might take some time to initialize before they can be used for the first time, and this process +is blocking. Fortunately, it is possible to control when the initialization should happen: the `quarkus.cassandra.init.eager-init` parameter determines if the `QuarkusCqlSession` bean should be @@ -744,21 +830,21 @@ that needs to interact with the Cassandra database. Using lazy initialization speeds up your application startup time, and avoids startup failures if the Cassandra database is not available. However, it could also prove dangerous if your code is -fully asynchronous, e.g. if you are using https://quarkus.io/guides/reactive-routes[reactive -routes]: indeed, the lazy initialization could accidentally happen on a thread that is not allowed +fully non-blocking, for example if it uses https://quarkus.io/guides/reactive-routes[reactive +routes]. Indeed, the lazy initialization could accidentally happen on a thread that is not allowed to block, such as a Vert.x event loop thread. Therefore, setting `quarkus.cassandra.init.eager-init` to `false` and injecting `QuarkusCqlSession` should be avoided in these contexts. -If you want to use Vert.x (or any other reactive framework) and keep the lazy initialization -behavior, you should instead inject only `CompletionStage` or -`Uni`. When injecting these beans, the initialization process will be triggered -lazily, but it will happen in the background, in a non-blocking way, leveraging the Vert.x event -loop. This way you don't risk blocking the Vert.x thread. +If you want to use Vert.x (or any other non-blocking framework) and keep the lazy initialization +behavior, you should instead inject only a `CompletionStage` or a `Uni` of the desired bean. When +injecting these beans, the initialization process will be triggered lazily, but it will happen in +the background, in a non-blocking way, leveraging the Vert.x event loop. This way you don't risk +blocking the Vert.x thread. Alternatively, you can set `quarkus.cassandra.init.eager-init` to true: in this case the session -bean will be initialized eagerly during application startup, on the Quarkus main thread. This would -eliminate any risk of blocking a Vert.x thread, at the cost of making your startup time (much) -longer. +bean and all DAO beans will be initialized eagerly during application startup, on the Quarkus main +thread. This would eliminate any risk of blocking a Vert.x thread, at the cost of making your +startup time (much) longer. == Conclusion diff --git a/docs/src/main/asciidoc/config-reference.adoc b/docs/src/main/asciidoc/config-reference.adoc index d9e7651c562b5..c9f84169337e3 100644 --- a/docs/src/main/asciidoc/config-reference.adoc +++ b/docs/src/main/asciidoc/config-reference.adoc @@ -297,14 +297,9 @@ quarkus.http.port=9090 Setting `quarkus.profile` to `staging` will activate the `staging` profile. -IMPORTANT: Only a single profile may be active at a time. - [NOTE] ==== -The `io.quarkus.runtime.configuration.ProfileManager#getActiveProfile` API provides a way to retrieve the active profile -programmatically. - -Using `@ConfigProperty("quarkus.profile")` will *not* work properly. +The `io.smallrye.config.SmallRyeConfig#getProfiles` API provides a way to retrieve the active profiles programmatically. ==== === Profile aware files diff --git a/docs/src/main/asciidoc/deploying-to-google-cloud.adoc b/docs/src/main/asciidoc/deploying-to-google-cloud.adoc index bdf62b6ff05ce..7d37416a04c58 100644 --- a/docs/src/main/asciidoc/deploying-to-google-cloud.adoc +++ b/docs/src/main/asciidoc/deploying-to-google-cloud.adoc @@ -113,7 +113,7 @@ First, add the plugin to your `pom.xml`: com.google.cloud.tools appengine-maven-plugin - 2.4.0 + 2.4.4 GCLOUD_CONFIG <1> gettingstarted @@ -163,7 +163,7 @@ It uses Cloud Build to build your Docker image and deploy it to Google Container When done, the output will display the URL of your application (target url), you can use it with curl or directly open it in your browser using `gcloud app browse`. -NOTE: App Engine Flexible custom runtimes support link:https://cloud.google.com/appengine/docs/flexible/custom-runtimes/configuring-your-app-with-app-yaml#updated_health_checks[health checks], +NOTE: App Engine Flexible custom runtimes support link:https://cloud.google.com/appengine/docs/flexible/reference/app-yaml?tab=java#updated_health_checks[health checks], it is strongly advised to provide them thanks to Quarkus xref:smallrye-health.adoc[Smallrye Health] support. == Deploying to Google Cloud Run @@ -211,7 +211,7 @@ Finally, use Cloud Run to launch your application. [source, shell script] ---- -gcloud run deploy --image gcr.io/PROJECT-ID/helloworld --platform managed +gcloud run deploy --image gcr.io/PROJECT-ID/helloworld ---- Cloud run will ask you questions on the service name, the region and whether unauthenticated calls are allowed. @@ -219,6 +219,9 @@ After you answer to these questions, it will deploy your application. When the deployment is done, the output will display the URL to access your application. +NOTE: Cloud Run supports link:https://cloud.google.com/run/docs/configuring/healthchecks[health checks], +it is strongly advised to provide them thanks to Quarkus xref:smallrye-health.adoc[Smallrye Health] support. + == Using Cloud SQL Google Cloud SQL provides managed instances for MySQL, PostgreSQL and Microsoft SQL Server. @@ -295,4 +298,4 @@ WARNING: This only works when your application is running inside a Google Cloud You can find a set of extensions to access various Google Cloud Services in the Quarkiverse (a GitHub organization for Quarkus extensions maintained by the community), including PubSub, BigQuery, Storage, Spanner, Firestore, Secret Manager (visit the repository for an accurate list of supported services). -You can find some documentation about them in the link:https://github.com/quarkiverse/quarkiverse-google-cloud-services[Quarkiverse Google Cloud Services repository]. +You can find some documentation about them in the link:https://quarkiverse.github.io/quarkiverse-docs/quarkus-google-cloud-services/main/index.html[Quarkiverse Google Cloud Services documentation]. diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 9d22270d17fa3..7cf863c7b8449 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -76,7 +76,7 @@ The full source of the `kubernetes.json` file looks something like this: "kind" : "Deployment", "metadata" : { "annotations": { - "app.quarkus.io/vcs-url" : "", + "app.quarkus.io/vcs-uri" : "", "app.quarkus.io/commit-id" : "", }, "labels" : { @@ -123,7 +123,7 @@ The full source of the `kubernetes.json` file looks something like this: "kind" : "Service", "metadata" : { "annotations": { - "app.quarkus.io/vcs-url" : "", + "app.quarkus.io/vcs-uri" : "", "app.quarkus.io/commit-id" : "", }, "labels" : { @@ -305,7 +305,7 @@ Out of the box, the generated resources will be annotated with version control r [source,json] ---- "annotations": { - "app.quarkus.io/vcs-url" : "", + "app.quarkus.io/vcs-uri" : "", "app.quarkus.io/commit-id" : "", } ---- @@ -1014,6 +1014,22 @@ OpenShift's users might want to use `oc` rather than `kubectl`: oc apply -f target/kubernetes/openshift.json ---- +For users that prefer to keep the `application.properties` independent of the deployment platform, the deployment target can be specified directly in the deploy command by adding `-Dquarkus.kubernetes.deployment-target=openshift` +in addition to `-Dquarkus.kubernetes.deploy=true`. Furthermore, Quarkus allows collapsing the two properties into one: `-Dquarkus.openshift.deploy=true`. + +[source,bash] +---- +./mvnw clean package -Dquarkus.openshift.deploy=true +---- + +The equivalent with gradle: + +[source,bash] +---- +./gradlew build -Dquarkus.openshift.deploy=true +---- + +In case that both properties are used with conflicting values `quarkus.kubernetes.deployment-target` is used. NOTE: Quarkus also provides the xref:deploying-to-openshift.adoc[OpenShift] extension. This extension is basically a wrapper around the Kubernetes extension and relieves OpenShift users of the necessity of setting the `deployment-target` property to `openshift` @@ -1049,7 +1065,7 @@ The full source of the `knative.json` file looks something like this: "kind" : "Service", "metadata" : { "annotations": { - "app.quarkus.io/vcs-url" : "", + "app.quarkus.io/vcs-uri" : "", "app.quarkus.io/commit-id" : "" }, "labels" : { @@ -1098,6 +1114,23 @@ By default, when no `deployment-target` is set, then only vanilla Kubernetes res `quarkus.kubernetes.deployment-target=kubernetes,openshift`) then the resources for all targets are generated, but only the resources that correspond to the *first* target are applied to the cluster (if deployment is enabled). +For users that prefer to keep the `application.properties` independent of the deployment platform, the deployment target can be specified directly in the deploy command by adding `-Dquarkus.kubernetes.deployment-target=knative` +in addition to `-Dquarkus.knative.deploy=true`. Furthermore, Quarkus allows collapsing the two properties into one: `-Dquarkus.knative.deploy=true`. + +[source,bash] +---- +./mvnw clean package -Dquarkus.knative.deploy=true +---- + +The equivalent with gradle: + +[source,bash] +---- +./gradlew build -Dquarkus.knative.deploy=true +---- + +In case that both properties are used with conflicting values `-Dquarkus.kubernetes.deployment-target` is used. + In the case of wrapper extensions like OpenShift and Minikube, when these extensions have been explicitly added to the project, the default `deployment-target` is set by those extensions. For example if `quarkus-minikube` has been added to a project, then `minikube` becomes the default deployment target and its resources will be applied to the Kubernetes cluster when deployment via `quarkus.kubernetes.deploy` has been set. diff --git a/docs/src/main/asciidoc/doc-concepts.adoc b/docs/src/main/asciidoc/doc-concept.adoc similarity index 99% rename from docs/src/main/asciidoc/doc-concepts.adoc rename to docs/src/main/asciidoc/doc-concept.adoc index c0c69f7f06160..60bf0c94d05dc 100644 --- a/docs/src/main/asciidoc/doc-concepts.adoc +++ b/docs/src/main/asciidoc/doc-concept.adoc @@ -3,7 +3,7 @@ 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="concepts-quarkus-documentation"] +[id="doc-concept"] = Quarkus documentation concepts include::_attributes.adoc[] :categories: contributing diff --git a/docs/src/main/asciidoc/doc-contribute-docs-howto.adoc b/docs/src/main/asciidoc/doc-contribute-docs-howto.adoc index 5e1d21106ca5f..3e057e24e7a06 100644 --- a/docs/src/main/asciidoc/doc-contribute-docs-howto.adoc +++ b/docs/src/main/asciidoc/doc-contribute-docs-howto.adoc @@ -3,7 +3,7 @@ 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="howto-contribute-docs"] +[id="doc-contribute-howto"] = How to contribute documentation include::_attributes.adoc[] :categories: contributing diff --git a/docs/src/main/asciidoc/doc-create-tutorial.adoc b/docs/src/main/asciidoc/doc-create-tutorial.adoc index d8d22c2705d28..c85a6c7b38e76 100644 --- a/docs/src/main/asciidoc/doc-create-tutorial.adoc +++ b/docs/src/main/asciidoc/doc-create-tutorial.adoc @@ -3,7 +3,7 @@ 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="tutorial-doc-create-tutorial"] +[id="doc-create-tutorial"] = Creating a tutorial include::_attributes.adoc[] :categories: contributing @@ -14,7 +14,7 @@ Create a new tutorial that guides users through creating, running, and testing a - Roughly 15 minutes - An editor or IDE that provides syntax highlighting and previews for AsciiDoc, either natively or using a plugin. -- You should be familiar with the overview of what a xref:doc-concepts.adoc#tutorial[Tutorial] is. +- You should be familiar with the overview of what a xref:doc-concept.adoc#tutorial[Tutorial] is. - Have the xref:doc-reference.adoc[Quarkus documentation reference] handy as a reference for required syntax and other conventions. :sectnums: @@ -39,7 +39,7 @@ Copy `docs/src/main/diataxis/_templates/template-tutorial.adoc` from the Quarkus [source,asciidoc] ---- -[id="tutorial-acme-serve-http-requests"] // <1> +[id="acme-serve-http-requests-tutorial"] // <1> = Serve Http requests using the Acme extension // <2> include::_attributes.adoc[] // <3> :categories: web // <4> diff --git a/docs/src/main/asciidoc/doc-reference.adoc b/docs/src/main/asciidoc/doc-reference.adoc index f56a528ca3aea..103cfc7f591ee 100644 --- a/docs/src/main/asciidoc/doc-reference.adoc +++ b/docs/src/main/asciidoc/doc-reference.adoc @@ -3,7 +3,7 @@ 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="reference-doc-quarkus-documentation"] +[id="doc-reference"] = About Quarkus documentation include::_attributes.adoc[] :categories: contributing @@ -108,7 +108,7 @@ AsciiDoc output to HTML:: AsciiDoc processing creates HTML files in `docs/target Create new documentation files with the appropriate template for the content type: -Concepts:: Use `docs/src/main/asciidoc/_templates/template-concepts.adoc` +Concepts:: Use `docs/src/main/asciidoc/_templates/template-concept.adoc` How-To Guides:: Use `docs/src/main/asciidoc/_templates/template-howto.adoc` Reference:: Use `docs/src/main/asciidoc/_templates/template-reference.adoc` Tutorials:: Use `docs/src/main/asciidoc/_templates/template-tutorial.adoc` @@ -124,7 +124,7 @@ Prefix:: Use a common prefix to group related documents. Documents related to wr Suffix:: The file name should reflect the document type: -- Concept documents should end in `-concepts.adoc` +- Concept documents should end in `-concept.adoc` - How-to guides should end in `-howto.adoc` - References should end in `-reference.adoc` - Tutorials should end in `-tutorial.adoc` @@ -138,14 +138,13 @@ Minimally, each document should define and id and a title, and include common at [source,asciidoc] ---- -[id="reference-doc-quarkus-documentation"] // <1> +[id="doc-reference"] // <1> = Quarkus documentation reference // <2> \include::_attributes.adoc[] // <3> :categories: contributing // <4> ---- -<1> Specify an id for your document. -It should start with the document type: `concept-`, `howto-`, `reference-`, or `tutorial-`. +<1> Use the filename as the ID for the document. <2> Define the document title following guidance in <> <3> Include common document attributes <4> Specify the relevant <> (comma separated) @@ -199,7 +198,7 @@ If you end up with deeply nested sections, think about the following: For example, if this is a reference, should some of this content be moved to a concept doc or how-to guide instead? - Can the content be reorganized to make it simpler to consume? -See xref:{doc-guides}/doc-concepts.adoc[Quarkus documentation concepts] for more information about content types and organization. +See xref:{doc-guides}/doc-concept.adoc[Quarkus documentation concepts] for more information about content types and organization. ==== === Links @@ -242,7 +241,7 @@ When cross-referencing content, always use the inter-document `xref:` syntax and .Cross-reference example [source,asciidoc] ---- -xref:{doc-guides}/doc-concepts.adoc[Quarkus Documentation concepts] <1> +xref:{doc-guides}/doc-concept.adoc[Quarkus Documentation concepts] <1> ---- <1> The cross reference starts with `xref:`, uses a cross-reference source attribute(`\{doc-guides}`), and provides a readable description: `[Quarkus Documentation concepts]`. diff --git a/docs/src/main/asciidoc/funqy-gcp-functions.adoc b/docs/src/main/asciidoc/funqy-gcp-functions.adoc index 3ab05fd2bb955..8ce831a04917c 100644 --- a/docs/src/main/asciidoc/funqy-gcp-functions.adoc +++ b/docs/src/main/asciidoc/funqy-gcp-functions.adoc @@ -142,7 +142,10 @@ quarkus.funqy.export=helloPubSubWorld To build your application, you can package your application via `mvn clean package`. You will have a single JAR inside the `target/deployment` repository that contains your classes and all your dependencies in it. -Then you will be able to use `gcloud` to deploy your function to Google Cloud. The `gcloud` command will be different depending on which event you want to trigger. +Then you will be able to use `gcloud` to deploy your function to Google Cloud. +The `gcloud` command will be different depending on which event triggers your function. + +NOTE: We will use the Java 17 runtime but you can switch to the Java 11 runtime by using `--runtime=java11` instead of `--runtime=java17` on the deploy commands. [WARNING] ==== @@ -163,7 +166,7 @@ Use this command to deploy to Google Cloud Functions: ---- gcloud functions deploy quarkus-example-funky-pubsub \ --entry-point=io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction \ - --runtime=java11 --trigger-resource hello_topic --trigger-event google.pubsub.topic.publish \ + --runtime=java17 --trigger-resource hello_topic --trigger-event google.pubsub.topic.publish \ --source=target/deployment ---- @@ -173,11 +176,6 @@ The entry point always needs to be `io.quarkus.funqy.gcp.functions.FunqyBackgrou that will bootstrap Quarkus. ==== -[NOTE] -==== -You can also use the new Java 17 runtime by using `--runtime=java17` in the gcloud command line. -==== - The `--trigger-resource` option defines the name of the PubSub topic, and the `--trigger-event google.pubsub.topic.publish` option define that this function will be triggered by all message publication inside the topic. @@ -205,7 +203,7 @@ Then, use this command to deploy to Google Cloud Functions: ---- gcloud functions deploy quarkus-example-funky-storage \ --entry-point=io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction \ - --runtime=java11 --trigger-resource quarkus-hello --trigger-event google.storage.object.finalize \ + --runtime=java17 --trigger-resource quarkus-hello --trigger-event google.storage.object.finalize \ --source=target/deployment ---- @@ -215,11 +213,6 @@ The entry point always needs to be `io.quarkus.funqy.gcp.functions.FunqyBackgrou that will bootstrap Quarkus. ==== -[NOTE] -==== -You can also use the new Java 17 runtime by using `--runtime=java17` in the gcloud command line. -==== - The `--trigger-resource` option defines the name of the Cloud Storage bucket, and the `--trigger-event google.storage.object.finalize` option define that this function will be triggered by all new file inside this bucket. @@ -251,7 +244,7 @@ Then, use this command to deploy to Google Cloud Functions: ---- gcloud functions deploy quarkus-example-cloud-event --gen2 \ --entry-point=io.quarkus.funqy.gcp.functions.FunqyCloudEventsFunction \ - --runtime=java11 --trigger-bucket=example-cloud-event --source=target/deployment + --runtime=java17 --trigger-bucket=example-cloud-event --source=target/deployment ---- [IMPORTANT] @@ -260,11 +253,6 @@ The entry point always needs to be `io.quarkus.funqy.gcp.functions.FunqyCloudEve that will bootstrap Quarkus. ==== -[NOTE] -==== -You can also use the new Java 17 runtime by using `--runtime=java17` in the gcloud command line. -==== - The `--trigger-bucket=` option defines the name of the Cloud Storage bucket. To trigger the event, you can send a file to the GCS `example-cloud-event` bucket. diff --git a/docs/src/main/asciidoc/gcp-functions-http.adoc b/docs/src/main/asciidoc/gcp-functions-http.adoc index fa7fb80c220d9..3e08fa4086779 100644 --- a/docs/src/main/asciidoc/gcp-functions-http.adoc +++ b/docs/src/main/asciidoc/gcp-functions-http.adoc @@ -160,11 +160,13 @@ The result of the previous command is a single JAR file inside the `target/deplo Then you will be able to use `gcloud` to deploy your function to Google Cloud. +NOTE: We will use the Java 17 runtime but you can switch to the Java 11 runtime by using `--runtime=java11` instead of `--runtime=java17` on the deploy commands. + [source,bash] ---- gcloud functions deploy quarkus-example-http \ --entry-point=io.quarkus.gcp.functions.http.QuarkusHttpFunction \ - --runtime=java11 --trigger-http --allow-unauthenticated --source=target/deployment + --runtime=java17 --trigger-http --allow-unauthenticated --source=target/deployment ---- [IMPORTANT] @@ -172,11 +174,6 @@ gcloud functions deploy quarkus-example-http \ The entry point must always be set to `io.quarkus.gcp.functions.http.QuarkusHttpFunction` as this is the class that integrates Cloud Functions with Quarkus. ==== -[NOTE] -==== -You can also use the new Java 17 runtime by using `--runtime=java17` in the gcloud command line. -==== - [WARNING] ==== The first time you launch this command, you can have the following error message: diff --git a/docs/src/main/asciidoc/gcp-functions.adoc b/docs/src/main/asciidoc/gcp-functions.adoc index e3f5aeb873dd8..54e6de864825b 100644 --- a/docs/src/main/asciidoc/gcp-functions.adoc +++ b/docs/src/main/asciidoc/gcp-functions.adoc @@ -237,7 +237,10 @@ include::{includes}/devtools/build.adoc[] The result of the previous command is a single JAR file inside the `target/deployment` repository that contains classes and dependencies of the project. -Then you will be able to use `gcloud functions deploy` command to deploy your function to Google Cloud. +Then you will be able to use `gcloud` to deploy your function to Google Cloud. +The `gcloud` command will be different depending on which event triggers your function. + +NOTE: We will use the Java 17 runtime but you can switch to the Java 11 runtime by using `--runtime=java11` instead of `--runtime=java17` on the deploy commands. [WARNING] ==== @@ -257,7 +260,7 @@ This is an example command to deploy your `HttpFunction` to Google Cloud: ---- gcloud functions deploy quarkus-example-http \ --entry-point=io.quarkus.gcp.functions.QuarkusHttpFunction \ - --runtime=java11 --trigger-http --allow-unauthenticated --source=target/deployment + --runtime=java17 --trigger-http --allow-unauthenticated --source=target/deployment ---- [IMPORTANT] @@ -265,11 +268,6 @@ gcloud functions deploy quarkus-example-http \ The entry point must always be set to `io.quarkus.gcp.functions.QuarkusHttpFunction` as this is the class that integrates Cloud Functions with Quarkus. ==== -[NOTE] -==== -You can also use the new Java 17 runtime by using `--runtime=java17` in the gcloud command line. -==== - This command will give you as output a `httpsTrigger.url` that points to your function. === The BackgroundFunction @@ -289,7 +287,7 @@ it needs to use `--trigger-event google.storage.object.finalize` and the `--trig gcloud functions deploy quarkus-example-storage \ --entry-point=io.quarkus.gcp.functions.QuarkusBackgroundFunction \ --trigger-resource quarkus-hello --trigger-event google.storage.object.finalize \ - --runtime=java11 --source=target/deployment + --runtime=java17 --source=target/deployment ---- [IMPORTANT] @@ -297,11 +295,6 @@ gcloud functions deploy quarkus-example-storage \ The entry point must always be set to `io.quarkus.gcp.functions.QuarkusBackgroundFunction` as this is the class that integrates Cloud Functions with Quarkus. ==== -[NOTE] -==== -You can also use the new Java 17 runtime by using `--runtime=java17` in the gcloud command line. -==== - To trigger the event, you can send a file to the GCS `quarkus-hello` bucket, or you can use gcloud to simulate one: [source,bash] @@ -320,7 +313,7 @@ it needs to use `--trigger-event google.pubsub.topic.publish` and the `--trigger ---- gcloud functions deploy quarkus-example-pubsub \ --entry-point=io.quarkus.gcp.functions.QuarkusBackgroundFunction \ - --runtime=java11 --trigger-resource hello_topic --trigger-event google.pubsub.topic.publish --source=target/deployment + --runtime=java17 --trigger-resource hello_topic --trigger-event google.pubsub.topic.publish --source=target/deployment ---- [IMPORTANT] @@ -328,11 +321,6 @@ gcloud functions deploy quarkus-example-pubsub \ The entry point must always be set to `io.quarkus.gcp.functions.QuarkusBackgroundFunction` as this is the class that integrates Cloud Functions with Quarkus. ==== -[NOTE] -==== -You can also use the new Java 17 runtime by using `--runtime=java17` in the gcloud command line. -==== - To trigger the event, you can send a file to the `hello_topic` topic, or you can use gcloud to simulate one: [source,bash] @@ -351,7 +339,7 @@ it needs to use `--trigger-bucket` parameter with the name of a previously creat ---- gcloud functions deploy quarkus-example-cloud-event --gen2 \ --entry-point=io.quarkus.gcp.functions.QuarkusCloudEventsFunction \ - --runtime=java11 --trigger-bucket=example-cloud-event --source=target/deployment + --runtime=java17 --trigger-bucket=example-cloud-event --source=target/deployment ---- [IMPORTANT] @@ -359,11 +347,6 @@ gcloud functions deploy quarkus-example-cloud-event --gen2 \ The entry point must always be set to `io.quarkus.gcp.functions.QuarkusCloudEventsFunction` as this is the class that integrates Cloud Functions with Quarkus. ==== -[NOTE] -==== -You can also use the new Java 17 runtime by using `--runtime=java17` in the gcloud command line. -==== - To trigger the event, you can send a file to the GCS `example-cloud-event` bucket. == Running locally diff --git a/docs/src/main/asciidoc/grpc-service-consumption.adoc b/docs/src/main/asciidoc/grpc-service-consumption.adoc index 89ac4bca2e7d0..8f6db53e03ac8 100644 --- a/docs/src/main/asciidoc/grpc-service-consumption.adoc +++ b/docs/src/main/asciidoc/grpc-service-consumption.adoc @@ -240,9 +240,9 @@ quarkus.grpc.clients.hello.ssl.key=tls/client.key quarkus.grpc.clients.hello.ssl.trust-store=tls/ca.pem ---- -=== Client Deadlines +=== Client Stub Deadlines -It's always reasonable to set a deadline (timeout) for a gRPC client, i.e. to specify a duration of time after which the RPC times out and the client receives the status error `DEADLINE_EXCEEDED`. +If you need to configure a deadline for a gRPC stub, i.e. to specify a duration of time after which the stub will always return the status error `DEADLINE_EXCEEDED`. You can specify the deadline via the `quarkus.grpc.clients."service-name".deadline` configuration property, e.g.: [source,properties] @@ -250,7 +250,10 @@ You can specify the deadline via the `quarkus.grpc.clients."service-name".deadli quarkus.grpc.clients.hello.host=localhost quarkus.grpc.clients.hello.deadline=2s <1> ---- -<1> Set the deadline for all injected clients. +<1> Set the deadline for all injected stubs. + +IMPORTANT: Do not use this feature to implement an RPC timeout. +To implement an RPC timeout, either use Mutiny `call.ifNoItem().after(...)` or Fault Tolerance `@Timeout`. == gRPC Headers Similarly to HTTP, alongside the message, gRPC calls can carry headers. diff --git a/docs/src/main/asciidoc/hibernate-orm-panache-kotlin.adoc b/docs/src/main/asciidoc/hibernate-orm-panache-kotlin.adoc index d1f109e6f0e62..1e5cd6e717092 100644 --- a/docs/src/main/asciidoc/hibernate-orm-panache-kotlin.adoc +++ b/docs/src/main/asciidoc/hibernate-orm-panache-kotlin.adoc @@ -25,7 +25,7 @@ change or two. To Panache-enable your entity, you would define it something lik [source,kotlin] ---- @Entity -class Person: PanacheEntity { +class Person: PanacheEntity() { lateinit var name: String lateinit var birth: LocalDate lateinit var status: Status diff --git a/docs/src/main/asciidoc/hibernate-reactive.adoc b/docs/src/main/asciidoc/hibernate-reactive.adoc index 744a298b0014c..890e27eb0831d 100644 --- a/docs/src/main/asciidoc/hibernate-reactive.adoc +++ b/docs/src/main/asciidoc/hibernate-reactive.adoc @@ -216,6 +216,40 @@ You can also inject an instance of `Uni` using the exact same me Uni session; ---- +=== Testing + +Using Hibernate Reactive in a `@QuarkusTest` is slightly more involved than using Hibernate ORM due to the asynchronous nature of the APIs and the fact that all operations need to run on a Vert.x Event Loop. + +Two components are necessary to write these tests: + +* The use of `@io.quarkus.test.vertx.RunOnVertxContext` or `@io.quarkus.test.TestReactiveTransaction` on the test methods +* The use of `io.quarkus.test.vertx.UniAsserter` as a test method parameter. + +IMPORTANT: These classes are provided by the `quarkus-test-vertx` dependency. + +A very simple example usage looks like: + +[source,java] +---- +@QuarkusTest +public class SomeTest { + + @Inject + Mutiny.SessionFactory sessionFactory; + + @Test + @RunOnVertxContext + public void testQuery(UniAsserter asserter) { + asserter.assertThat(() -> sessionFactory.withSession(s -> s.createQuery( + "from Gift g where g.name = :name").setParameter("name", "Lego").getResultList()), + list -> org.junit.jupiter.api.Assertions.assertEquals(list.size(), 1)); + } + +} +---- + +NOTE: See the Javadoc of `UniAsserter` for a full description of the various methods that can be used for creating assertions. + [[hr-limitations]] == Limitations and other things you should know diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index 65d52e55c4617..4a98d84efe846 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -78,6 +78,13 @@ TIP: By default, the following list of media types is compressed: `text/html`, ` NOTE: If the client does not support HTTP compression then the response body is not compressed. +[[static-resources-config]] +=== Other Configurations + +Additionally, the index page for static resources can be changed from default `index.html`, the hidden files (e.g. dot files) can be indicated as not served, the range requests can be disabled, and the caching support (e.g. caching headers and file properties cache) can be configured. + +include::{generated-dir}/config/quarkus-vertx-http-config-group-static-resources-config.adoc[leveloffset=+1, opts=optional] + [[context-path]] == Configuring the Context path diff --git a/docs/src/main/asciidoc/infinispan-client.adoc b/docs/src/main/asciidoc/infinispan-client.adoc index 75e343843d1b0..a09fb6f755a2d 100644 --- a/docs/src/main/asciidoc/infinispan-client.adoc +++ b/docs/src/main/asciidoc/infinispan-client.adoc @@ -44,9 +44,9 @@ This command adds the following dependency to your build file: .build.gradle ---- implementation 'io.quarkus:quarkus-infinispan-client' -annotationProcessor 'org.infinispan.protostream:protostream-processor:4.5.0.Final' <1> +annotationProcessor 'org.infinispan.protostream:protostream-processor:4.5.1.Final' <1> ---- -<1> Mandatory in the gradle build to enable the generation of the files in the annotation based serialization +<1> Mandatory in the Gradle build to enable the generation of the files in the annotation based serialization == Configuring the Infinispan client diff --git a/docs/src/main/asciidoc/jreleaser.adoc b/docs/src/main/asciidoc/jreleaser.adoc index 677dca385ec75..d7b54cc7a3378 100644 --- a/docs/src/main/asciidoc/jreleaser.adoc +++ b/docs/src/main/asciidoc/jreleaser.adoc @@ -607,7 +607,7 @@ As a reference, these are the full contents of the `pom.xml`: ${project.build.directory}/distributions - 3.8.1 + 3.10.1 true 11 11 diff --git a/docs/src/main/asciidoc/kafka-dev-services.adoc b/docs/src/main/asciidoc/kafka-dev-services.adoc index 30588ba1e1381..e48283d851827 100644 --- a/docs/src/main/asciidoc/kafka-dev-services.adoc +++ b/docs/src/main/asciidoc/kafka-dev-services.adoc @@ -50,16 +50,32 @@ Note that the Kafka advertised address is automatically configured with the chos [[configuring-the-image]] == Configuring the image -Dev Services for Kafka supports https://redpanda.com[Redpanda] and https://strimzi.io[Strimzi] (in https://github.com/apache/kafka/blob/trunk/config/kraft/README.md[Kraft] mode). +Dev Services for Kafka supports https://redpanda.com[Redpanda], https://github/ozangunalp/kafka-native[kafka-native] +and https://strimzi.io[Strimzi] (in https://github.com/apache/kafka/blob/trunk/config/kraft/README.md[Kraft] mode) images. -Redpanda is a Kafka compatible event streaming platform. -Because it provides a faster startup time dev services defaults to `vectorized/redpanda` images. +**Redpanda** is a Kafka compatible event streaming platform. +Because it provides a fast startup times, dev services defaults to Redpanda images from `vectorized/redpanda`. You can select any version from https://hub.docker.com/r/vectorized/redpanda. -Strimzi provides container images and Operators for running Apache Kafka on Kubernetes. +**kafka-native** provides images of standard Apache Kafka distribution compiled to native binary using Quarkus and GraalVM. +While still being _experimental_, it provides very fast startup times with small footprint. + +Image type can be configured using + +[source, properties] +---- +quarkus.kafka.devservices.provider=kafka-native +---- + +**Strimzi** provides container images and Operators for running Apache Kafka on Kubernetes. While Strimzi is optimized for Kubernetes, the images work perfectly in classic container environments. Strimzi container images run "genuine" Kafka broker on JVM, which is slower to start. +[source, properties] +---- +quarkus.kafka.devservices.provider=strimzi +---- + For Strimzi, you can select any image with a Kafka version which has Kraft support (2.8.1 and higher) from https://quay.io/repository/strimzi-test-container/test-container?tab=tags [source, properties] diff --git a/docs/src/main/asciidoc/kubernetes-dev-services.adoc b/docs/src/main/asciidoc/kubernetes-dev-services.adoc index 0f4c4fc0af9be..bd749c6d220e3 100644 --- a/docs/src/main/asciidoc/kubernetes-dev-services.adoc +++ b/docs/src/main/asciidoc/kubernetes-dev-services.adoc @@ -5,7 +5,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Dev Services for Kubernetes include::_attributes.adoc[] -:categories: messaging +:categories: cloud :summary: Start a Kubernetes API server automatically in dev and test modes. Dev Services for Kubernetes automatically starts a Kubernetes API server in dev mode and when running tests. diff --git a/docs/src/main/asciidoc/maven-tooling.adoc b/docs/src/main/asciidoc/maven-tooling.adoc index 9aac9d4771223..cee4744a2d2e4 100644 --- a/docs/src/main/asciidoc/maven-tooling.adoc +++ b/docs/src/main/asciidoc/maven-tooling.adoc @@ -59,6 +59,10 @@ If you are using the Maven command, the following table lists the attributes you | The version currently recommended by the https://quarkus.io/guides/extension-registry-user[Quarkus Extension Registry] | The version of the platform you want the project to use. It can also accept a version range, in which case the latest from the specified range will be used. +| `javaVersion` +| 17 +| The version of Java you want the project to use. + | `className` | _Not created if omitted_ | The fully qualified name of the generated resource diff --git a/docs/src/main/asciidoc/performance-measure.adoc b/docs/src/main/asciidoc/performance-measure.adoc index cd261ce68ef95..51ca1c4b1e1b9 100644 --- a/docs/src/main/asciidoc/performance-measure.adoc +++ b/docs/src/main/asciidoc/performance-measure.adoc @@ -224,13 +224,6 @@ all services it's able to find on the classpath. We prefer listing services explicitly as it produces better optimised binaries. Disable it as well by setting `-H:-UseServiceLoaderFeature`. -=== Better default for Garbage Collection implementation - -The default in GraalVM seems meant to optimise for short-lived processes. - -Quarkus defaults to server applications, so we switch to a better default by setting - `-H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime`. - === Others ... This section is provided as high level guidance, but can't presume to be comprehensive as some flags are controlled diff --git a/docs/src/main/asciidoc/podman.adoc b/docs/src/main/asciidoc/podman.adoc index 3dc68c1c3e842..6d83e73b80fcd 100644 --- a/docs/src/main/asciidoc/podman.adoc +++ b/docs/src/main/asciidoc/podman.adoc @@ -71,23 +71,8 @@ This action only needs to be done once. === Linux The Podman package is available in several Linux distributions. -Podman can be used the same way as Docker with the `podman-docker` package. -To install it for your OS, please refer to the https://podman.io/getting-started/installation[Podman installation guide]. -Below is the short installation instruction for popular Linux distributions: - -==== Fedora - -[source,bash] ----- -sudo dnf install podman podman-docker docker-compose ----- - -==== Ubuntu (21.04 and later) - -[source,bash] ----- -sudo apt install podman podman-docker docker-compose ----- +Podman can in most cases be used as an drop-in-replacement for Docker, either with the `podman-docker` package, or using an alias (`alias docker=podman`). +To install it for your Linux OS, please refer to the https://podman.io/getting-started/installation#installing-on-linux[Podman installation guide]. === Setting DOCKER_HOST on Linux @@ -96,17 +81,24 @@ On Linux, the REST API Unix socket is, by default, restricted to only allow the This prevents someone from using a container to achieve a privilege escalation on the system. While these restrictions can be softened to allow a special group instead of just root, the recommended approach is to use rootless Podman on Linux. To use rootless Podman, you need to set a `DOCKER_HOST` environment variable to point to the user-specific socket. -In both cases, you need to start the REST API by enabling the Podman socket service through systemd. -[source] +NOTE: In both cases, you need to start the REST API by enabling the Podman socket service through systemd, or at least by making sure Podman is running as a service. + +[source,bash] ---- -# Enable the podman socket with Docker REST API (only needs to be done once) +# Example 1: Enable the podman socket with Docker REST API with systemd (only needs to be done once) systemctl --user enable podman.socket --now ---- +[source,bash] +---- +# Example 2: Enable the podman socket with Docker REST API on a system where systemd is not running (WSL etc) +podman system service --time=0 +---- + Then, you can obtain the path of the socket with the following command: -[source] +[source,bash] ---- $ podman info | grep -A2 'remoteSocket' @@ -117,7 +109,7 @@ remoteSocket: Setting the `DOCKER_HOST` environment variable must be done every time or added to the profile: -[source] +[source,bash] ---- export DOCKER_HOST=unix:///path/to/podman.sock <1> ---- diff --git a/docs/src/main/asciidoc/quartz.adoc b/docs/src/main/asciidoc/quartz.adoc index 0cb5dc145c3b0..3df48d7d5243d 100644 --- a/docs/src/main/asciidoc/quartz.adoc +++ b/docs/src/main/asciidoc/quartz.adoc @@ -417,6 +417,27 @@ include::{includes}/devtools/build-native.adoc[] WARNING: It's the responsibility of the deployer to clear/remove the previous state, i.e. stale jobs and triggers. Moreover, the applications that form the "Quartz cluster" should be identical, otherwise an unpredictable result may occur. +[[quartz-configure-instance-id]] +== Configuring the Instance ID + +By default, the scheduler is configured with a simple instance ID generator using the machine hostname and the current timestamp, so you don't need to worry about setting a appropriate `instance-id` for each node when running in clustered mode. However, you can define a specific `instance-id` by yourself setting a configuration property reference or using other generators. + +[source,properties] +---- +quarkus.quartz.instance-id=${HOST:AUTO} <1> +---- +<1> This will expand the `HOST` environment variable and use `AUTO` as the default value if `HOST` is not set. + +The example below configure the generator `org.quartz.simpl.HostnameInstanceIdGenerator` named as `hostname`, so you can use its name as `instance-id` to be used. That generator uses just the machine hostname and can be appropriate in environments providing unique names for the nodes. + +[source,properties] +---- +quarkus.quartz.instance-id=hostname +quarkus.quartz.instance-id-generators.hostname.class=org.quartz.simpl.HostnameInstanceIdGenerator +---- + +WARNING: It's the responsibility of the deployer to define appropriate instance identifiers. Moreover, the applications that form the "Quartz cluster" should contain unique instance identifiers, otherwise an unpredictable result may occur. It's recommended to use an appropriate instance ID generator rather than specifying explicit identifiers. + [[quartz-register-plugin-listeners]] == Registering Plugin and Listeners diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 91bd23616f34d..0109d8088aa3a 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -1987,6 +1987,9 @@ TIP: A list element can be accessed directly via an index: `{list.10}` or even ` * `str:fmt` or `str:format`: Formats the supplied string value via `java.lang.String.format()` ** `{str:format("Hello %s!",name)}` ** `{str:fmt(locale,'%tA',now)}` +* `+`: Concatenation +** `{item.name + '_' + mySuffix}` +** `{name + 10}` ===== Config diff --git a/docs/src/main/asciidoc/redis-reference.adoc b/docs/src/main/asciidoc/redis-reference.adoc index feb7f4198f440..d46772acb4080 100644 --- a/docs/src/main/asciidoc/redis-reference.adoc +++ b/docs/src/main/asciidoc/redis-reference.adoc @@ -134,9 +134,9 @@ public class RedisExample { } ---- -TIP: When using `@RedisClientName`, you can omit the `@Inject` annotation. +TIP: You can omit the `@Inject` annotation when using `@RedisClientName`. -== Connecting to Redis +== Connection to Redis The Redis extension can operate in 4 distinct modes: @@ -152,7 +152,7 @@ The connection url is configured with the `quarkus.redis.hosts` (or `quarkus.red quarkus.redis.hosts=redis://[:password@]host[:port][/db-number] ---- -=== Using Unix Socket +=== Unix Socket When using unix-socket, you need: @@ -196,7 +196,7 @@ quarkus.redis.hosts=redis://localhost:7000 quarkus.redis.client-type=replication ---- -=== Connecting to Redis Cloud +=== Redis Cloud To connect to redis cloud, you need the following properties: @@ -213,13 +213,13 @@ We recommend the latter, and if possible, using secrets or an environment variab The associated environment variable is `QUARKUS_REDIS_PASSWORD`, or `QUARKUS_REDIS__PASSWORD` for named clients. -== Using the high-level clients (data sources) +== Quarkus client API for data sources Quarkus exposes a high-level API on top of Redis. This API is type-safe and structured around the notion of _group_, inherited from the https://redis.io/commands/command-docs/[Redis command organization]. This API lets you execute Redis commands more conveniently and safely. -=== Injecting data sources +=== Inject data sources For each configured Redis client, two Redis data sources are exposed: @@ -334,7 +334,7 @@ To store binary data, use `byte[]`. The `value` group is used to manipulate https://redis.io/docs/manual/data-types/#strings[Redis Strings]. Thus, this group is not limited to Java Strings but can be used for integers (like a counter) or binary content (like images). -==== Caching values +==== Work with cached values You can use Redis as a cache using the `setex` command, which stores a given value to a given key for a given duration. The following snippet shows how such a command can be used to store `BusinessObject` for 1 second. @@ -377,7 +377,7 @@ The `set` method can also receive a `SetArgs` argument that modify the behavior: - `keepttl()` - Retain the time to live associated with the key. ==== -==== Storing binary data +==== Store binary data Redis _strings_ can be used to store binary data, such as images. In this case, we will use `byte[]` as value type: @@ -411,7 +411,7 @@ public static class MyBinaryRepository { } ---- -==== Storing a counter +==== Store a counter You can store counters in Redis as demonstrated below: @@ -454,10 +454,10 @@ There are other methods that can be useful to manipulate counters, such as: - `set` - to set an initial value if needed - `decr` and `decrby` - allows decrementing the stored value -==== Using pub/sub +==== Communicate with pub/sub Redis allows sending _messages_ to channels and listening for these messages. -These features are available from the the `pubsub` group. +These features are available from the `pubsub` group. The following snippets shows how a _cache_ can emit a `Notification` after every `set`, and how a subscriber can receive the notification. @@ -521,7 +521,7 @@ public static class MyCache { } ---- -==== Using transactions +==== Redis transactions Redis transactions are slightly different from relational database transactions. Redis transactions are a batch of commands executed altogether. @@ -585,7 +585,7 @@ TransactionResult result = ds.withTransaction(tx -> { IMPORTANT: You cannot use the pub/sub feature from within a transaction. -==== Using optimistic locking +==== Optimistic locking To use optimistic locking, you need to use a variant of the `withTransaction` method, allowing the execution of code before the transaction starts. In other words, it will be executed as follows: @@ -634,7 +634,7 @@ These commands must not modify the watched keys. The transaction is aborted if the pre-transaction block throws an exception (or produces a failure when using the reactive API). -==== Executing custom commands +==== Execute custom commands To execute a custom command, or a command not supported by the API, use the following approach: @@ -656,6 +656,81 @@ The reactive variant returns a `Uni`. NOTE: You can also execute custom command in a transaction. +== Preload data into Redis + +On startup, you can configure the Redis client to preload data into the Redis database. + +=== Load scripts + +Specify the _load script_ you want to load using: + +[source, properties] +---- +quarkus.redis.load-script=import.sql # import.sql is the default in dev, no-file is the default in prod +quarkus.redis.my-redis.load-script=actors.redis, movies.redis +---- + +IMPORTANT: `load-script` is a build time property than cannot be overridden at runtime. + +Note that each client can have a different script, even a list of scripts. +In the case of a list, the data is imported in the list order (for example, first `actors.redis`, then `movies.redis` for the `my-redis` client). + +=== Load Script format + +The `.redis` file follows a _one command per line_ format: + +[source, text] +---- +# Line starting with # and -- are ignored, as well as empty lines + +-- One command per line: +HSET foo field1 abc field2 123 + +-- Parameters with spaces must be wrapped into single or double quotes +HSET bar field1 "abc def" field2 '123 456 ' + +-- Parameters with double quotes must be wrapped into single quotes and the opposite +SET key1 'A value using "double-quotes"' +SET key2 "A value using 'single-quotes'" +---- + +Quarkus batches all the commands from a single file and sends all the commands. +The loading process fails if there is any error, but the previous instructions may have been executed. +To avoid that, you can wrap your command into a Redis _transaction_: + +[source, text] +---- +-- Run inside a transaction +MULTI +SET key value +SET space:key 'another value' +INCR counter +EXEC +---- + +=== Configuration + +The data is loaded when the application starts. +By default, it drops the whole database before importing. +You can prevent this using `quarkus.redis.flush-before-load=false`. + +Also, the import process only runs if the database is empty (no key). +You can force to import even if there is data using the `quarkus.redis.load-only-if-empty=false` + +=== Dev/Test vs. Prod + +As mentioned above, in dev and test modes, Quarkus tries to import data by looking for the `src/main/resources/import.redis`. +This behavior is disabled in _prod_ mode, and if you want to import even in production, add: + +[source, properties] +---- +%prod.quarkus.redis.load-script=import.redis +---- + +Before importing in _prod_ mode, mae sure you configured `quarkus.redis.flush-before-load` accordingly. + +IMPORTANT: In dev mode, to reload the content of the `.redis` load scripts, you need to add: `%dev.quarkus.vertx.caching=false` + == Vert.x Redis Client In addition to the high-level API, you can use the Vertx Redis clients directly in your code. @@ -669,7 +744,7 @@ So when you access the `/q/health/ready` endpoint of your application you will h This behavior can be disabled by setting the `quarkus.redis.health.enabled` property to `false` in your `application.properties`. -== Providing Redis Hosts Programmatically +== Programmatic Redis Hosts The `RedisHostsProvider` programmatically provides redis hosts. This allows for configuration of properties like redis connection password coming from other sources. @@ -700,7 +775,7 @@ The host provider can be used to configure the redis client like shown below quarkus.redis.hosts-provider-name=hosts-provider ---- -== Customizing the Redis options programmatically +== Customize the Redis options programmatically You can expose a bean implementing the `io.quarkus.redis.client.RedisOptionsCustomizer` interface to customize the Redis client options. The bean is called for each configured Redis client: @@ -727,7 +802,78 @@ public static class MyExampleCustomizer implements RedisOptionsCustomizer { See xref:redis-dev-services.adoc[Redis Dev Service]. +== Redis client metrics + +=== Enable metrics collection + +Redis client metrics are automatically enabled when the application also uses the xref:micrometer.adoc[`quarkus-micrometer`] extension. +Micrometer collects the metrics of all the Redis clients implemented by the application. + +As an example, if you export the metrics to Prometheus, you will get: + +[source, text] +---- +# HELP redis_commands_duration_seconds The duration of the operations (commands of batches +# TYPE redis_commands_duration_seconds summary +redis_commands_duration_seconds_count{client_name="",} 3.0 +redis_commands_duration_seconds_sum{client_name="",} 0.047500042 +# HELP redis_commands_duration_seconds_max The duration of the operations (commands of batches +# TYPE redis_commands_duration_seconds_max gauge +redis_commands_duration_seconds_max{client_name="",} 0.033273167 +# HELP redis_pool_active The number of resources from the pool currently used +# TYPE redis_pool_active gauge +redis_pool_active{pool_name="",pool_type="redis",} 0.0 +# HELP redis_pool_ratio Pool usage ratio +# TYPE redis_pool_ratio gauge +redis_pool_ratio{pool_name="",pool_type="redis",} 0.0 +# HELP redis_pool_queue_size Number of pending elements in the waiting queue +# TYPE redis_pool_queue_size gauge +redis_pool_queue_size{pool_name="",pool_type="redis",} 0.0 +# HELP redis_commands_failure_total The number of operations (commands or batches) that have been failed +# TYPE redis_commands_failure_total counter +redis_commands_failure_total{client_name="",} 0.0 +# HELP redis_commands_success_total The number of operations (commands or batches) that have been executed successfully +# TYPE redis_commands_success_total counter +redis_commands_success_total{client_name="",} 3.0 +# HELP redis_pool_idle The number of resources from the pool currently used +# TYPE redis_pool_idle gauge +redis_pool_idle{pool_name="",pool_type="redis",} 6.0 +# HELP redis_pool_completed_total Number of times resources from the pool have been acquired +# TYPE redis_pool_completed_total counter +redis_pool_completed_total{pool_name="",pool_type="redis",} 3.0 +# HELP redis_commands_count_total The number of operations (commands or batches) executed +# TYPE redis_commands_count_total counter +redis_commands_count_total{client_name="",} 3.0 +# HELP redis_pool_usage_seconds Time spent using resources from the pool +# TYPE redis_pool_usage_seconds summary +redis_pool_usage_seconds_count{pool_name="",pool_type="redis",} 3.0 +redis_pool_usage_seconds_sum{pool_name="",pool_type="redis",} 0.024381375 +# HELP redis_pool_usage_seconds_max Time spent using resources from the pool +# TYPE redis_pool_usage_seconds_max gauge +redis_pool_usage_seconds_max{pool_name="",pool_type="redis",} 0.010671542 +# HELP redis_pool_queue_delay_seconds Time spent in the waiting queue before being processed +# TYPE redis_pool_queue_delay_seconds summary +redis_pool_queue_delay_seconds_count{pool_name="",pool_type="redis",} 3.0 +redis_pool_queue_delay_seconds_sum{pool_name="",pool_type="redis",} 0.022341249 +# HELP redis_pool_queue_delay_seconds_max Time spent in the waiting queue before being processed +# TYPE redis_pool_queue_delay_seconds_max gauge +redis_pool_queue_delay_seconds_max{pool_name="",pool_type="redis",} 0.021926083 +---- + +The Redis client name can be found in the _tags_. + +The metrics contain both the Redis connection pool metrics (`redis_pool_*`) and the metrics about the command execution (`redis_commands_*`) such as the number of command, successes, failures, and durations. + +=== Disable metrics collection + +To disable the Redis client metrics when `quarkus-micrometer` is used, add the following property to the application configuration: + +[source, properties] +---- +quarkus.micrometer.binder.redis.enabled=false +---- + [[redis-configuration-reference]] -== Configuration Reference +== Configuration reference include::{generated-dir}/config/quarkus-redis-client.adoc[opts=optional, leveloffset=+1] diff --git a/docs/src/main/asciidoc/rest-client-reactive.adoc b/docs/src/main/asciidoc/rest-client-reactive.adoc index 08cc292a1fcc4..842ac7f244db2 100644 --- a/docs/src/main/asciidoc/rest-client-reactive.adoc +++ b/docs/src/main/asciidoc/rest-client-reactive.adoc @@ -215,7 +215,7 @@ Because we used the `${...}` syntax, the actual value of the parameter will be o [IMPORTANT] ==== -Note that if an interface method contains an argument annotated with `@QueryParam````, that argument will take +Note that if an interface method contains an argument annotated with `@QueryParam`, that argument will take priority over anything specified in any `@ClientQueryParam` annotation. ==== @@ -283,6 +283,19 @@ quarkus.rest-client.extensions-api.url=https://stage.code.quarkus.io/api quarkus.rest-client.extensions-api.scope=javax.inject.Singleton ---- +=== Disabling Hostname Verification + +To disable the SSL hostname verification for a specific REST client, add the following property to your configuration: + +[source,properties] +---- +quarkus.rest-client.extensions-api.verify-host=false +---- +[WARNING] +==== +This setting should not be used in production as it will disable the SSL hostname verification. +==== + == Create the JAX-RS resource Create the `src/main/java/org/acme/rest/client/ExtensionsResource.java` file with the following content: @@ -658,7 +671,7 @@ extensionsService.getByIdAsUni(id) If you use a `CompletionStage`, you would need to call the service's method to retry. This difference comes from the laziness aspect of Mutiny and its subscription protocol. -More details about this can be found in https://smallrye.io/smallrye-mutiny/#_uni_and_multi[the Mutiny documentation]. +More details about this can be found in https://smallrye.io/smallrye-mutiny/latest/reference/uni-and-multi/[the Mutiny documentation]. == Custom headers support diff --git a/docs/src/main/asciidoc/rest-client.adoc b/docs/src/main/asciidoc/rest-client.adoc index 4e51d0674663a..680315e9376d8 100644 --- a/docs/src/main/asciidoc/rest-client.adoc +++ b/docs/src/main/asciidoc/rest-client.adoc @@ -232,8 +232,24 @@ To disable the SSL hostname verification for a specific REST client, add the fol [source,properties] ---- -quarkus.rest-client.extensions-api.hostname-verifier=io.quarkus.restclient.NoopHostnameVerifier +quarkus.rest-client.extensions-api.verify-host=false ---- +[WARNING] +==== +This setting should not be used in production as it will disable the SSL hostname verification. +==== + +Moreover, you can configure a REST client to use your custom hostname verify strategy. All you need to do is to provide a class that implements the interface `javax.net.ssl.HostnameVerifier` and add the following property to your configuration: + +[source,properties] +---- +quarkus.rest-client.extensions-api.hostname-verifier= +---- + +[NOTE] +==== +Quarkus REST client provides an embedded hostname verifier strategy to disable the hostname verification called `io.quarkus.restclient.NoopHostnameVerifier`. +==== === Disabling SSL verifications @@ -498,7 +514,7 @@ extensionsService.getByIdAsUni(id) If you use a `CompletionStage`, you would need to call the service's method to retry. This difference comes from the laziness aspect of Mutiny and its subscription protocol. -More details about this can be found in https://smallrye.io/smallrye-mutiny/#_uni_and_multi[the Mutiny documentation]. +More details about this can be found in https://smallrye.io/smallrye-mutiny/latest/reference/uni-and-multi/[the Mutiny documentation]. == Custom headers support diff --git a/docs/src/main/asciidoc/rest-data-panache.adoc b/docs/src/main/asciidoc/rest-data-panache.adoc index 7846ecc2c62fd..333dfeb454ce1 100644 --- a/docs/src/main/asciidoc/rest-data-panache.adoc +++ b/docs/src/main/asciidoc/rest-data-panache.adoc @@ -86,6 +86,8 @@ implementation("io.quarkus:quarkus-resteasy-reactive-jackson") * Implement the Panache entities and/or repositories as explained in the xref:hibernate-orm-panache.adoc[Hibernate ORM with Panache guide]. * Define the interfaces for generation as explained in <>. +To see the Hibernate ORM REST Data with Panache in action, you can check out the {quickstarts-tree-url}/hibernate-orm-rest-data-panache-quickstart[hibernate-orm-rest-data-panache-quickstart] quickstart. + [[hr-hibernate-reactive]] === Hibernate Reactive diff --git a/docs/src/main/asciidoc/resteasy-reactive-migration.adoc b/docs/src/main/asciidoc/resteasy-reactive-migration.adoc index f4e6674e015a9..96caaba04f23f 100644 --- a/docs/src/main/asciidoc/resteasy-reactive-migration.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive-migration.adoc @@ -87,6 +87,10 @@ The following table matches the legacy RESTEasy annotations with the new RESTEas |`org.jboss.resteasy.reactive.RestStreamElementType` | +|`org.jboss.resteasy.annotations.Separator` +|`org.jboss.resteasy.reactive.Separator` +| + |=== NOTE: The previous table does not include the `org.jboss.resteasy.annotations.Form` annotation because there is no RESTEasy Reactive specific replacement for it. diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index d475ccbf2c6d1..a33ab14484ae5 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -2637,12 +2637,14 @@ TIP: By default, the following list of media types is compressed: `text/html`, ` NOTE: If the client does not support HTTP compression then the response body is not compressed. -== Include/Exclude JAX-RS classes with build time conditions +== Include/Exclude JAX-RS classes + +=== Using Build time conditions Quarkus enables the inclusion or exclusion of JAX-RS Resources, Providers and Features directly thanks to build time conditions in the same that it does for CDI beans. Thus, the various JAX-RS classes can be annotated with profile conditions (`@io.quarkus.arc.profile.IfBuildProfile` or `@io.quarkus.arc.profile.UnlessBuildProfile`) and/or with property conditions (`io.quarkus.arc.properties.IfBuildProperty` or `io.quarkus.arc.properties.UnlessBuildProperty`) to indicate to Quarkus at build time under which conditions these JAX-RS classes should be included. -In the following example, Quarkus includes the endpoint `sayHello` if and only if the build profile `app1` has been enabled. +In the following example, Quarkus includes the `ResourceForApp1Only` Resource class if and only if the build profile `app1` has been enabled. [source,java] ---- @@ -2659,6 +2661,25 @@ public class ResourceForApp1Only { Please note that if a JAX-RS Application has been detected and the method `getClasses()` and/or `getSingletons()` has/have been overridden, Quarkus will ignore the build time conditions and consider only what has been defined in the JAX-RS Application. +=== Using a runtime property + +Quarkus can also conditionally disable JAX-RS Resources based on the value of runtime properties using the `@io.quarkus.resteasy.reactive.server.EndpointDisabled` annotation. + +In the following example, Quarkus will exclude `RuntimeResource` at runtime if the application has `some.property` configured to `"disable"`. + +[source,java] +---- +@EndpointDisabled(name = "some.property", stringValue = "disable") +public class RuntimeResource { + + @GET + @Path("sayHello") + public String sayHello() { + return "hello"; + } +} +---- + == RESTEasy Reactive client diff --git a/docs/src/main/asciidoc/scheduler-reference.adoc b/docs/src/main/asciidoc/scheduler-reference.adoc index 71255cef94a30..368d84b6b6bad 100644 --- a/docs/src/main/asciidoc/scheduler-reference.adoc +++ b/docs/src/main/asciidoc/scheduler-reference.adoc @@ -277,6 +277,7 @@ The main idea is to keep the logic to skip the execution outside the scheduled b TIP: A CDI event of type `io.quarkus.scheduler.SkippedExecution` is fired when an execution of a scheduled method is skipped. +[[non-blocking-methods]] === Non-blocking Methods By default, a scheduled method is executed on the main executor for blocking tasks. @@ -377,6 +378,12 @@ If the xref:micrometer.adoc[Micrometer extension] is present, then a `@io.microm If the xref:smallrye-metrics.adoc[SmallRye Metrics extension] is present, then a `@org.eclipse.microprofile.metrics.annotation.Timed` interceptor binding is added to all `@Scheduled` methods automatically (unless it's already present) and a `org.eclipse.microprofile.metrics.Timer` is created for each `@Scheduled` method. The name consists of the fully qualified name of the declaring class and the name of a `@Scheduled` method. The timer has a tag `scheduled=true`. +== OpenTelemetry Tracing + +If `quarkus.scheduler.tracing.enabled` is set to `true` and the xref:opentelemetry.adoc[OpenTelemetry extension] is present then the `@io.opentelemetry.instrumentation.annotations.WithSpan` annotation is added automatically to every `@Scheduled` method. As a result, each execution of this method has a new `io.opentelemetry.api.trace.Span` associated. + +IMPORTANT: <> are not supported, i.e. a new span is associated with the _actual_ invocation but it's not available within the asynchronous computation. + == Configuration Reference include::{generated-dir}/config/quarkus-scheduler.adoc[leveloffset=+1, opts=optional] diff --git a/docs/src/main/asciidoc/security-authorization-of-web-endpoints-reference.adoc b/docs/src/main/asciidoc/security-authorization-of-web-endpoints-reference.adoc index ededae5a66872..e39525d881292 100644 --- a/docs/src/main/asciidoc/security-authorization-of-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorization-of-web-endpoints-reference.adoc @@ -273,6 +273,75 @@ public class SubjectExposingResource { CAUTION: Please refer to the xref:security-built-in-authentication-support-concept.adoc#proactive-authentication[Proactive Authentication] section of the Built-In Authentication Support guide if you plan to use standard security annotations on IO thread. +The `@RolesAllowed` annotation value supports <> including default values and nested Property Expressions. +Configuration properties used with the annotation are resolved at runtime. + +[source,properties] +---- +admin=Administrator +tester.group=Software +tester.role=Tester +%prod.secured=User +%dev.secured=** +---- + +[source,java] +---- +import java.security.Principal; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; + +@Path("subject") +public class SubjectExposingResource { + + @GET + @Path("admin") + @RolesAllowed("${admin}") <1> + public String getSubjectSecuredAdmin(@Context SecurityContext sec) { + Principal user = sec.getUserPrincipal(); + String name = user != null ? user.getName() : "anonymous"; + return name; + } + + @GET + @Path("software-tester") + @RolesAllowed("${tester.group}-${tester.role}") <2> + public String getSubjectSoftwareTester(@Context SecurityContext sec) { + Principal user = sec.getUserPrincipal(); + String name = user != null ? user.getName() : "anonymous"; + return name; + } + + @GET + @Path("user") + @RolesAllowed("${customer:User}") <3> + public String getSubjectUser(@Context SecurityContext sec) { + Principal user = sec.getUserPrincipal(); + String name = user != null ? user.getName() : "anonymous"; + return name; + } + + @GET + @Path("secured") + @RolesAllowed("${secured}") <4> + public String getSubjectSecured(@Context SecurityContext sec) { + Principal user = sec.getUserPrincipal(); + String name = user != null ? user.getName() : "anonymous"; + return name; + } +} +---- +<1> The `@RolesAllowed` annotation value is set to the value of the `admin`. +<2> This `/subject/software-tester` endpoint requires an authenticated user that has been granted the role "Software-Tester". It is possible to use multiple expressions in the role definition. +<3> This `/subject/user` endpoint requires an authenticated user that has been granted the role "User" through the use of the `@RolesAllowed("${customer:User}")` annotation, as we did not set the configuration property `customer`. +<4> This `/subject/secured` endpoint requires an authenticated user that has been granted the role `User` in production, but allows any authenticated user in development mode. + == References * xref:security-overview-concept.adoc[Quarkus Security overview] diff --git a/docs/src/main/asciidoc/security-basic-authentication-howto.adoc b/docs/src/main/asciidoc/security-basic-authentication-howto.adoc index 2c9911eff3ff6..20349b9695a91 100644 --- a/docs/src/main/asciidoc/security-basic-authentication-howto.adoc +++ b/docs/src/main/asciidoc/security-basic-authentication-howto.adoc @@ -8,7 +8,9 @@ Enable Basic authentication for your Quarkus project and allow users to authenti == Prerequisites * You have installed at least one extension that provides an `IdentityProvider` based on username and password, such as xref:security-jdbc.adoc[Elytron JDBC]. - + +== Procedure + . Enable Basic authentication by setting the value of `quarkus.http.auth.basic` property to `true`. + [source,properties] @@ -16,7 +18,30 @@ Enable Basic authentication for your Quarkus project and allow users to authenti quarkus.http.auth.basic=true ---- -For a Basic authentication configuration walk-through that uses `JPA`, see the xref:security-basic-authentication-tutorial.adoc[Secure a Quarkus application with Basic authentication] guide. +An easy way to configure the required user credentials for Basic authentication to work is to configure the user name, secret, and roles directly in the `application.properties` file. + +.Example of Basic authentication properties + +[source,properties] +---- +quarkus.http.auth.basic=true +quarkus.security.users.embedded.plain-text=true +quarkus.security.users.embedded.users.alice=alice +quarkus.security.users.embedded.users.bob=bob +quarkus.security.users.embedded.roles.alice=admin +quarkus.security.users.embedded.roles.bob=user +---- + +In this configuration the credentials for users `alice` and `bob` are configured: `alice` has a password `alice` and an `admin` role, `bob` has a password `bob` and a `user` role. + +For more information, see xref:security-testing.adoc#configuring-user-information[Configuring User Information] in the "Security Testing" guide. + +[IMPORTANT] +==== +Configuring user names, secrets, and roles in the `application.properties` file is only suitable for testing scenarios. If you are securing an application for production, always use a database to store this information. +==== + +To walk through how to configure Basic authentication together with JPA for storing user credentials in a database, see the xref:security-basic-authentication-tutorial.adoc[Secure a Quarkus application with Basic authentication] tutorial. == Additional resources diff --git a/docs/src/main/asciidoc/security-built-in-authentication-support-concept.adoc b/docs/src/main/asciidoc/security-built-in-authentication-support-concept.adoc index c3f7ad629d664..ecc879b17d7d5 100644 --- a/docs/src/main/asciidoc/security-built-in-authentication-support-concept.adoc +++ b/docs/src/main/asciidoc/security-built-in-authentication-support-concept.adoc @@ -30,6 +30,18 @@ value. This cookie contains an expiry time as part of the encrypted value, so al clocks synchronized. At one minute intervals a new cookie will be generated with an updated expiry time if the session is in use. +Single Page Application (SPA) typically wants to avoid redirects, this can be done by removing default page paths: + +[source,properties] +---- +# do not redirect, respond with HTTP 200 OK +quarkus.http.auth.form.landing-page= + +# do not redirect, respond with HTTP 401 Unauthorized +quarkus.http.auth.form.login-page= +quarkus.http.auth.form.error-page= +---- + The following properties can be used to configure form based auth: include::{generated-dir}/config/quarkus-vertx-http-config-group-form-auth-config.adoc[opts=optional, leveloffset=+1] @@ -175,7 +187,39 @@ public class HelloService { === How to customize authentication exception responses -By default, the authentication security constraints are enforced before the JAX-RS chain starts and only way to handle Quarkus Security authentication exceptions is to provide a failure handler like this one: +You can use JAX-RS `ExceptionMapper` to capture Quarkus Security authentication exceptions such as `io.quarkus.security.AuthenticationFailedException`, for example: + +[source,java] +---- +package io.quarkus.it.keycloak; + +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import io.quarkus.security.AuthenticationFailedException; + +@Provider +@Priority(Priorities.AUTHENTICATION) +public class AuthenticationFailedExceptionMapper implements ExceptionMapper { + + @Context + UriInfo uriInfo; + + @Override + public Response toResponse(AuthenticationFailedException exception) { + return Response.status(401).header("WWW-Authenticate", "Basic realm=\"Quarkus\"").build(); + } +} +---- + +CAUTION: Some HTTP authentication mechanisms need to handle authentication exceptions themselves in order to create a correct authentication challenge. +For example, `io.quarkus.oidc.runtime.CodeAuthenticationMechanism` which manages OpenId Connect authorization code flow authentication, needs to build a correct redirect URL, cookies, etc. +For that reason, using custom exception mappers to customize authentication exceptions thrown by such mechanisms is not recommended. +In such cases, a safer way to customize authentication exceptions is to make sure the proactive authentication is not disabled and use Vert.x HTTP route failure handlers, as events come to the handler with the correct response status and headers. +To that end, the only thing that needs to be done is to customize the response like this: [source,java] ---- @@ -197,7 +241,7 @@ public class AuthenticationFailedExceptionHandler { @Override public void handle(RoutingContext event) { if (event.failure() instanceof AuthenticationFailedException) { - event.response().setStatusCode(401).end(CUSTOMIZED_RESPONSE); + event.response().end("CUSTOMIZED_RESPONSE"); } else { event.next(); } @@ -207,34 +251,6 @@ public class AuthenticationFailedExceptionHandler { } ---- -Disabling the proactive authentication effectively shifts this process to the moment when the JAX-RS chain starts running thus making it possible to use JAX-RS `ExceptionMapper` to capture Quarkus Security authentication exceptions such as `io.quarkus.security.AuthenticationFailedException`, for example: - -[source,java] ----- -package io.quarkus.it.keycloak; - -import javax.annotation.Priority; -import javax.ws.rs.Priorities; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -import io.quarkus.security.AuthenticationFailedException; - -@Provider -@Priority(Priorities.AUTHENTICATION) -public class AuthenticationFailedExceptionMapper implements ExceptionMapper { - - @Context - UriInfo uriInfo; - - @Override - public Response toResponse(AuthenticationFailedException exception) { - return Response.status(401).header("WWW-Authenticate", "Basic realm=\"Quarkus\"").build(); - } -} ----- - == References * xref:security-overview-concept.adoc[Quarkus Security overview] diff --git a/docs/src/main/asciidoc/security-customization.adoc b/docs/src/main/asciidoc/security-customization.adoc index 0760c51cd7c12..ff3345fa31444 100644 --- a/docs/src/main/asciidoc/security-customization.adoc +++ b/docs/src/main/asciidoc/security-customization.adoc @@ -70,6 +70,7 @@ public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism } ---- +[[dealing-with-more-than-one-http-auth-mechanisms]] == Dealing with more than one HttpAuthenticationMechanism More than one `HttpAuthenticationMechanism` can be combined, for example, the built-in `Basic` or `JWT` mechanism provided by `quarkus-smallrye-jwt` has to be used to verify the service clients credentials passed as the HTTP `Authorization` `Basic` or `Bearer` scheme values while the `Authorization Code` mechanism provided by `quarkus-oidc` has to be used to authenticate the users with Keycloak or other OpenID Connect providers. diff --git a/docs/src/main/asciidoc/security-jwt.adoc b/docs/src/main/asciidoc/security-jwt.adoc index 420d1044e9aaa..ddfcfe0d1bb39 100644 --- a/docs/src/main/asciidoc/security-jwt.adoc +++ b/docs/src/main/asciidoc/security-jwt.adoc @@ -811,11 +811,27 @@ If you work with Keycloak and configure `mp.jwt.verify.publickey.location` to po [source, properties] ---- -# keycloak.url is set by OidcWiremockTestResource +# keycloak.url is set by DevServices for Keycloak mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus ---- +Note that the tokens issued by Keycloak have an `iss` (issuer) claim set to the realm endpoint address. + +If your Quarkus application is running in a docker container then it may share a network interface with a Keycloak docker container launched by DevServices for Keycloak, with the Quarkus application and Keycloak communicating with each other via an internal shared docker network. + +In such cases, use the following configuration instead: + +[source, properties] +---- +# keycloak.url is set by DevServices for Keycloak, +# Quarkus will access it via an internal shared docker network interface. +mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs + +# Issuer is set to the docker bridge localhost endpoint address represented by the `client.quarkus.oidc.auth-server-url` property +mp.jwt.verify.issuer=${client.quarkus.oidc.auth-server-url} +---- + [[integration-testing-public-key]] ==== Local Public Key @@ -918,6 +934,28 @@ public class ProtectedResource { Note that `@TestSecurity` annotation must always be used and its `user` property is returned as `JsonWebToken.getName()` and `roles` property - as `JsonWebToken.getGroups()`. `@JwtSecurity` annotation is optional and can be used to set the additional token claims. +[TIP] +==== +`@TestSecurity` and `@JwtSecurity` can be combined in a meta-annotation, for example like so: + +[source, java] +---- + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD }) + @TestSecurity(user = "userOidc", roles = "viewer") + @OidcSecurity(introspectionRequired = true, + introspection = { + @TokenIntrospection(key = "email", value = "user@gmail.com") + } + ) + public @interface TestSecurityMetaAnnotation { + + } +---- + +This is particularly useful if the same set of security settings needs to be used in multiple test methods. +==== + === How to check the errors in the logs Please enable `io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator` `TRACE` level logging to see more details about the token verification or decryption errors: diff --git a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc index 86c625ca2b5ba..1d648704ed2a1 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -208,6 +208,7 @@ public class OidcClientResource { } ---- +[[use-oidc-clients]] === Use OidcClients `io.quarkus.oidc.client.OidcClients` is a container of ``OidcClient``s - it includes a default `OidcClient` and named clients which can be configured like this: @@ -419,7 +420,25 @@ public interface ProtectedResourceService { ---- `OidcClientRequestReactiveFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-client-reactive-filter.client-name` configuration property. +You can also select `OidcClient` by setting `value` attribute of the `@OidcClientFilter` annotation. The client name set through annotation has higher priority than the `quarkus.oidc-client-reactive-filter.client-name` configuration property. +For example, given <> `jwt-secret` named OIDC client declaration, you can refer to this client like this: +[source,java] +---- +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import io.quarkus.oidc.client.filter.OidcClientFilter; +import io.smallrye.mutiny.Uni; + +@RegisterRestClient +@OidcClientFilter("jwt-secret") +@Path("/") +public interface ProtectedResourceService { + + @GET + Uni getUserName(); +} +---- [[oidc-client-filter]] === Use OidcClient in RestClient ClientFilter @@ -478,6 +497,23 @@ public interface ProtectedResourceService { Alternatively, `OidcClientRequestFilter` can be registered automatically with all MP Rest or JAX-RS clients if `quarkus.oidc-client-filter.register-filter=true` property is set. `OidcClientRequestFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-client-filter.client-name` configuration property. +You can also select `OidcClient` by setting `value` attribute of the `@OidcClientFilter` annotation. The client name set through annotation has higher priority than the `quarkus.oidc-client-filter.client-name` configuration property. +For example, given <> `jwt-secret` named OIDC client declaration, you can refer to this client like this: + +[source,java] +---- +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import io.quarkus.oidc.client.filter.OidcClientFilter; + +@RegisterRestClient +@OidcClientFilter("jwt-secret") +@Path("/") +public interface ProtectedResourceService { + + @GET + String getUserName(); +} +---- === Use Custom RestClient ClientFilter diff --git a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc index 76e5664411c92..1153b605d5f3b 100644 --- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc @@ -217,8 +217,9 @@ Please see xref:security-openid-connect.adoc#integration-testing-keycloak-devser [[keycloak-initialization]] === Keycloak Initialization -The `quay.io/keycloak/keycloak:19.0.2` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default. -`quarkus.keycloak.devservices.image-name` can be used to change the Keycloak image name. For example, set it to `quay.io/keycloak/keycloak:19.0.2-legacy` to use a Keycloak distribution powered by WildFly. +The `quay.io/keycloak/keycloak:20.0.1` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default. +`quarkus.keycloak.devservices.image-name` can be used to change the Keycloak image name. For example, set it to `quay.io/keycloak/keycloak:19.0.3-legacy` to use a Keycloak distribution powered by WildFly. +Note that only a Quarkus based Keycloak distribution is available starting from Keycloak `20.0.0`. `Dev Services for Keycloak` will initialize a launched Keycloak server next. diff --git a/docs/src/main/asciidoc/security-openid-connect-providers.adoc b/docs/src/main/asciidoc/security-openid-connect-providers.adoc index 0672a7b2896c5..668609f81c1a2 100644 --- a/docs/src/main/asciidoc/security-openid-connect-providers.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-providers.adoc @@ -49,6 +49,8 @@ quarkus.oidc.client-id= quarkus.oidc.credentials.secret= ---- +TIP: You can also use GitHub provider with `quarkus.oidc.application-type=service`, just set `quarkus.oidc.verify-access-token-with-user-info` configuration property to `true`. + === Google In order to set up OIDC for Google you need to create a new project in your https://console.cloud.google.com/projectcreate[Google Cloud Platform console]: diff --git a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc index 3fd5893ef29f1..87f0feb9c3b87 100644 --- a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc @@ -677,7 +677,7 @@ quarkus.oidc.authentication.pkce-required=true quarkus.oidc.authentication.pkce-secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU ---- -If you already have a 32 character long client secret then `quarkus.oidc.authentication.pkce-secret` does not have to be set unless you prefer to use a different secret key. +If you already have a 32 characters long client secret then `quarkus.oidc.authentication.pkce-secret` does not have to be set unless you prefer to use a different secret key. The secret key is required for encrypting a randomly generated `PKCE` `code_verifier` while the user is being redirected with the `code_challenge` query parameter to OpenID Connect Provider to authenticate. The `code_verifier` will be decrypted when the user is redirected back to Quarkus and sent to the token endpoint alongside the `code`, client secret and other parameters to complete the code exchange. The provider will fail the code exchange if a `SHA256` digest of the `code_verifier` does not match the `code_challenge` provided during the authentication request. @@ -700,7 +700,7 @@ public class SecurityEventListener { public void event(@Observes SecurityEvent event) { String tenantId = event.getSecurityIdentity().getAttribute("tenant-id"); - RoutingContext vertxContext = event.getSecurityIdentity().getCredential(IdTokenCredential.class).getRoutingContext(); + RoutingContext vertxContext = event.getSecurityIdentity().getAttribute(RoutingContext.class.getName()); vertxContext.put("listener-message", String.format("event:%s,tenantId:%s", event.getEventType().name(), tenantId)); } } diff --git a/docs/src/main/asciidoc/security-openid-connect.adoc b/docs/src/main/asciidoc/security-openid-connect.adoc index 6a27a989460f8..70ac3581decbf 100644 --- a/docs/src/main/asciidoc/security-openid-connect.adoc +++ b/docs/src/main/asciidoc/security-openid-connect.adoc @@ -1128,6 +1128,28 @@ public class ProtectedResource { Note that `@TestSecurity` `user` and `roles` attributes are available as `TokenIntrospection` `username` and `scope` properties and you can use `io.quarkus.test.security.oidc.TokenIntrospection` to add the additional introspection response properties such as an `email`, etc. +[TIP] +==== +`@TestSecurity` and `@OidcSecurity` can be combined in a meta-annotation, for example like so: + +[source, java] +---- + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD }) + @TestSecurity(user = "userOidc", roles = "viewer") + @OidcSecurity(introspectionRequired = true, + introspection = { + @TokenIntrospection(key = "email", value = "user@gmail.com") + } + ) + public @interface TestSecurityMetaAnnotation { + + } +---- + +This is particularly useful if the same set of security settings needs to be used in multiple test methods. +==== + === How to check the errors in the logs Please enable `io.quarkus.oidc.runtime.OidcProvider` `TRACE` level logging to see more details about the token verification errors: diff --git a/docs/src/main/asciidoc/security-overview-concept.adoc b/docs/src/main/asciidoc/security-overview-concept.adoc index e69dcc9c9fd37..2af860fe09c56 100644 --- a/docs/src/main/asciidoc/security-overview-concept.adoc +++ b/docs/src/main/asciidoc/security-overview-concept.adoc @@ -1,7 +1,7 @@ [id="security-overview-concept"] = Quarkus Security overview include::_attributes.adoc[] -:categories: security, getting-started +:categories: security Quarkus Security is a framework that provides the architecture, multiple authentication and authorization mechanisms, and other tools for you to build secure and production-quality Java applications. diff --git a/docs/src/main/asciidoc/security-testing.adoc b/docs/src/main/asciidoc/security-testing.adoc index 4cea1aac6c27a..4a924ea85dbac 100644 --- a/docs/src/main/asciidoc/security-testing.adoc +++ b/docs/src/main/asciidoc/security-testing.adoc @@ -96,6 +96,23 @@ See xref:security-openid-connect.adoc#integration-testing-security-annotation[Op The feature is only available for `@QuarkusTest` and will **not** work on a `@NativeImageTest` or `@QuarkusIntegrationTest`. ==== +[TIP] +==== +`@TestSecurity` can also be used in meta-annotations, for example like so: + +[source, java] +---- + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD }) + @TestSecurity(user = "testUser", roles = {"admin", "user"}) + public @interface TestSecurityMetaAnnotation { + + } +---- + +This is particularly useful if the same set of security settings needs to be used in multiple test methods. +==== + === Mixing security tests If it becomes necessary to test security features using both `@TestSecurity` and Basic Auth (which is the fallback auth diff --git a/docs/src/main/asciidoc/security-webauthn.adoc b/docs/src/main/asciidoc/security-webauthn.adoc index 4d199cc126d7f..5c21349b3614f 100644 --- a/docs/src/main/asciidoc/security-webauthn.adoc +++ b/docs/src/main/asciidoc/security-webauthn.adoc @@ -87,7 +87,7 @@ The solution is located in the `security-webauthn-quickstart` {quickstarts-tree- First, we need a new project. Create a new project with the following command: :create-app-artifact-id: security-webauthn-quickstart -:create-app-extensions: security-webauthn,reactive-pg-client,resteasy-reactive,hibernate-reactive-panache,test-security-webauthn +:create-app-extensions: security-webauthn,reactive-pg-client,resteasy-reactive,hibernate-reactive-panache include::{includes}/devtools/create-app.adoc[] [NOTE] @@ -1059,7 +1059,7 @@ Quarkus WebAuthn endpoints to defer those calls to the worker pool. == Testing WebAuthn Testing WebAuthn can be complicated because normally you need a hardware token, which is why we've made the -`quarkus-test-security-webauthn` extension: +`quarkus-test-security-webauthn` helper library: [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml diff --git a/docs/src/main/asciidoc/smallrye-health.adoc b/docs/src/main/asciidoc/smallrye-health.adoc index 3c85c82dd6067..a0e0039e69fc5 100644 --- a/docs/src/main/asciidoc/smallrye-health.adoc +++ b/docs/src/main/asciidoc/smallrye-health.adoc @@ -393,9 +393,9 @@ public class LivenessAsync implements AsyncHealthCheck { Some extension may provide default health checks, including the extension will automatically register its health checks. For example, `quarkus-agroal` that is used to manage Quarkus datasource(s) automatically register a readiness health check -that will validate each datasources: xref:datasource.adoc#datasource-health-check[Datasource Health Check]. +that will validate each datasource: xref:datasource.adoc#datasource-health-check[Datasource Health Check]. -You can disable extension health check via the property `quarkus.health.extensions.enabled` so none will be automatically registered. +You can disable extension health checks via the property `quarkus.health.extensions.enabled` so none will be automatically registered. [[ui]] == Health UI diff --git a/docs/src/main/asciidoc/transaction.adoc b/docs/src/main/asciidoc/transaction.adoc index 6fae203cc7f0b..bc8561521b227 100644 --- a/docs/src/main/asciidoc/transaction.adoc +++ b/docs/src/main/asciidoc/transaction.adoc @@ -175,13 +175,15 @@ public class TransactionExample { QuarkusTransaction.rollback(); } - public void lambdaExample() { - QuarkusTransaction.run(() -> { + public void runnerExample() { + QuarkusTransaction.requiringNew().run(() -> { + //do work + }); + QuarkusTransaction.joiningExisting().run(() -> { //do work }); - - int result = QuarkusTransaction.call(QuarkusTransaction.runOptions() + int result = QuarkusTransaction.requiringNew() .timeout(10) .exceptionHandler((throwable) -> { if (throwable instanceof SomeException) { @@ -189,33 +191,37 @@ public class TransactionExample { } return RunOptions.ExceptionResult.ROLLBACK; }) - .semantic(RunOptions.Semantic.REQUIRE_NEW), () -> { - //do work - return 0; - }); + .call(() -> { + //do work + return 0; + }); } } ---- -The above example shows a few different ways the API can be used. The first method simply calls begin, does some work and commits it. +The above example shows a few different ways the API can be used. + +The first method simply calls begin, does some work and commits it. This created transaction is tied to the CDI request scope, so if it is still active when the request scope is destroyed then it will be automatically rolled back. This removes the need to explicitly catch exceptions and call `rollback`, and acts as a safety net against inadvertent transaction leaks, however it does mean that this can only be used when the request scope is active. The second example in the method calls begin with a timeout option, and then rolls back the transaction. -The second example shows the use of lambda scoped transactions, the first just runs a `Runnable` within a transaction, the second, -runs `Callable` with some specific options. In particular the `exceptionHandler` method can be used to control if the transaction -is rolled back or not on exception, and the `semantic` method controls the behaviour if an existing transaction is already started. +The second method shows the use of lambda scoped transactions with `QuarkusTransaction.runner(...)`; +the first example just runs a `Runnable` within a new transaction, +the second does the same but joining the existing transaction (if any), +and the third calls a `Callable` with some specific options. +In particular the `exceptionHandler` method can be used to control if the transaction is rolled back or not on exception. The following semantics are supported: -DISALLOW_EXISTING:: +`QuarkusTransaction.disallowingExisting()`/`DISALLOW_EXISTING`:: If a transaction is already associated with the current thread a `QuarkusTransactionException` will be thrown, otherwise a new transaction is started, and follows all the normal lifecycle rules. -JOIN_EXISTING:: +`QuarkusTransaction.joiningExisting()`/`JOIN_EXISTING`:: If no transaction is active then a new transaction will be started, and committed when the method ends. If an exception is thrown the exception handler registered by `#exceptionHandler(Function)` will be called to @@ -225,21 +231,20 @@ exception is thrown the exception handler will be called, however a result of `ExceptionResult#ROLLBACK` will result in the TX marked as rollback only, while a result of `ExceptionResult#COMMIT` will result in no action being taken. -REQUIRE_NEW:: +`QuarkusTransaction.requiringNew()`/`REQUIRE_NEW`:: -This is the default semantic. -If an existing transaction is already associated with the current thread then the transaction is suspended, and -resumed once -the current transaction is complete. -A new transaction is started after the existing transaction is suspended, and follows all the normal lifecycle rules. +If an existing transaction is already associated with the current thread then the transaction is suspended, +then a new transaction is started which follows all the normal lifecycle rules, +and when it's complete the original transaction is resumed. +Otherwise, a new transaction is started, and follows all the normal lifecycle rules. -SUSPEND_EXISTING:: +`QuarkusTransaction.suspendingExisting()`/`SUSPEND_EXISTING`:: -If no transaction is active then this semantic is basically a no-op. +If no transaction is active then these semantics are basically a no-op. If a transaction is active then it is suspended, and resumed after the task is run. -The exception handler will never be consulted when this semantic is in use, specifying both an exception handler and -this semantic is considered an error. -This semantic allows for code to easily be run outside the scope of a transaction. +The exception handler will never be consulted when these semantics are in use, specifying both an exception handler and +these semantics are considered an error. +These semantics allows for code to easily be run outside the scope of a transaction. diff --git a/docs/src/main/asciidoc/writing-extensions.adoc b/docs/src/main/asciidoc/writing-extensions.adoc index f58249f52a563..51eebdcda1435 100644 --- a/docs/src/main/asciidoc/writing-extensions.adoc +++ b/docs/src/main/asciidoc/writing-extensions.adoc @@ -559,7 +559,7 @@ The name of the deployment module can be configured in the plugin by setting the ---- plugins { id 'java' - id 'io.quarkus.extensions' + id 'io.quarkus.extension' } quarkusExtension { @@ -1522,6 +1522,15 @@ The following objects can be passed to recorders: - Any arbitrary object via the `io.quarkus.deployment.recording.RecorderContext#registerSubstitution(Class, Class, Class)` mechanism - Arrays, Lists and Maps of the above +[NOTE] +==== +In cases where some fields of an object to be recorded should be ignored (i.e. the value that being at build time should not be reflected at runtime), the `@IgnoreProperty` can be placed on the field. + +If the class cannot depend on Quarkus, then Quarkus can use any custom annotation, as long as the extension implements the `io.quarkus.deployment.recording.RecordingAnnotationsProvider` SPI. + +This same SPI can also be used to provide a custom annotation that will substitute for `@RecordableConstructor`. +==== + ==== Injecting Configuration into Recorders Configuration objects with phase `RUNTIME` or `BUILD_AND_RUNTIME_FIXED` can be injected into recorders via constructor diff --git a/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java b/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java index f2c0446e735be..db0f19f711297 100644 --- a/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java +++ b/docs/src/main/java/io/quarkus/docs/generation/YamlMetadataGenerator.java @@ -304,26 +304,24 @@ public static void addAll(Set set, Object source, Path path) { } enum Type { - concepts("concepts", "Concepts", "concept"), - howto("howto", "How-To Guides"), + concept("concepts", "Concept", "concept"), + howto("howto", "How-To Guide"), tutorial("tutorial", "Tutorial"), reference("reference", "Reference"), - other("guide", "General Guides"); + other("guide", "General Guide"); final String name; final String id; - final String prefix; + final String suffix; Type(String id, String name) { - this.name = name; - this.id = id; - this.prefix = id; + this(id, name, id); } - Type(String id, String name, String prefix) { + Type(String id, String name, String suffix) { this.name = name; this.id = id; - this.prefix = prefix; + this.suffix = suffix; } } @@ -452,8 +450,8 @@ static class DocMetadata implements Comparable { if (this.categories.contains(Category.getting_started)) { this.type = Type.tutorial; - } else if (filename.endsWith("-concepts.adoc")) { - this.type = Type.concepts; + } else if (filename.endsWith("-concept.adoc")) { + this.type = Type.concept; } else if (filename.endsWith("-howto.adoc")) { this.type = Type.howto; } else if (filename.endsWith("-tutorial.adoc")) { @@ -467,11 +465,11 @@ static class DocMetadata implements Comparable { if (id == null) { errors.record("missing-id", path); - } else if (type != Type.other && !id.startsWith(type.prefix)) { + } else if (type != Type.other && !id.endsWith(type.suffix)) { errors.record("incorrect-id", path, String.format( - "The document id (%s) does not start with the correct prefix, should start with '%s-'%n", - id, type.prefix)); + "The document id (%s) does not end with the correct suffix, should end with '-%s'%n", + id, type.suffix)); } if (this.categories.isEmpty()) { diff --git a/extensions/amazon-lambda-http/maven-archetype/src/main/resources/archetype-resources/pom.xml b/extensions/amazon-lambda-http/maven-archetype/src/main/resources/archetype-resources/pom.xml index ba5993f75d90c..b06a4f66bae9b 100644 --- a/extensions/amazon-lambda-http/maven-archetype/src/main/resources/archetype-resources/pom.xml +++ b/extensions/amazon-lambda-http/maven-archetype/src/main/resources/archetype-resources/pom.xml @@ -8,7 +8,7 @@ \${version} 3.1.0 - 3.8.1 + 3.10.1 true 11 11 diff --git a/extensions/amazon-lambda-rest/maven-archetype/src/main/resources/archetype-resources/pom.xml b/extensions/amazon-lambda-rest/maven-archetype/src/main/resources/archetype-resources/pom.xml index a0518654fe7f2..04a0d6f91da6a 100644 --- a/extensions/amazon-lambda-rest/maven-archetype/src/main/resources/archetype-resources/pom.xml +++ b/extensions/amazon-lambda-rest/maven-archetype/src/main/resources/archetype-resources/pom.xml @@ -8,7 +8,7 @@ \${version} 3.1.0 - 3.8.1 + 3.10.1 true 11 11 diff --git a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml index ec1094ddc0d83..d4afeffcb02a5 100644 --- a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml +++ b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml @@ -7,7 +7,7 @@ \${artifactId} \${version} - 3.8.1 + 3.10.1 true 11 11 diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java index 00fe8ebcf5324..f2fb782c542b7 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java @@ -277,7 +277,8 @@ public void transform(TransformationContext transformationContext) { } }); - builder.setBeanArchiveIndex(index); + builder.setComputingBeanArchiveIndex(index); + builder.setImmutableBeanArchiveIndex(beanArchiveIndex.getImmutableIndex()); builder.setApplicationIndex(combinedIndex.getIndex()); List beanDefiningAnnotations = additionalBeanDefiningAnnotations.stream() .map((s) -> new BeanDefiningAnnotation(s.getName(), s.getDefaultScope())).collect(Collectors.toList()); diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveIndexBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveIndexBuildItem.java index fbb2813c63846..0c0550f7d7e4a 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveIndexBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveIndexBuildItem.java @@ -14,26 +14,42 @@ * Compared to {@link io.quarkus.deployment.builditem.CombinedIndexBuildItem} this index can contain additional classes * that were indexed while bean discovery was in progress. * - * It also holds information about all programmatically registered beans and all generated bean classes. - * * @see GeneratedBeanBuildItem - * @see AdditionalBeanBuildItem * @see io.quarkus.deployment.builditem.CombinedIndexBuildItem */ public final class BeanArchiveIndexBuildItem extends SimpleBuildItem { private final IndexView index; + private final IndexView immutableIndex; private final Set generatedClassNames; - public BeanArchiveIndexBuildItem(IndexView index, Set generatedClassNames) { + public BeanArchiveIndexBuildItem(IndexView index, IndexView immutableIndex, Set generatedClassNames) { this.index = index; + this.immutableIndex = immutableIndex; this.generatedClassNames = generatedClassNames; } + /** + * This index is built on top of the immutable index. + * + * @return the computing index that can also index classes on demand + */ public IndexView getIndex() { return index; } + /** + * + * @return an immutable index that represents the bean archive + */ + public IndexView getImmutableIndex() { + return immutableIndex; + } + + /** + * + * @return the set of classes generated via {@link GeneratedBeanBuildItem} + */ public Set getGeneratedClassNames() { return generatedClassNames; } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java index 29283e4df9516..cb75954e4b327 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java @@ -84,12 +84,12 @@ public BeanArchiveIndexBuildItem build(ArcConfig config, ApplicationArchivesBuil additionalClasses.put(knownMissingClass, Optional.empty()); } - // Finally, index ArC/CDI API built-in classes - return new BeanArchiveIndexBuildItem( - BeanArchives.buildBeanArchiveIndex(Thread.currentThread().getContextClassLoader(), additionalClasses, - applicationIndex, - additionalBeanIndexer.complete()), - generatedClassNames); + IndexView immutableBeanArchiveIndex = BeanArchives.buildImmutableBeanArchiveIndex(applicationIndex, + additionalBeanIndexer.complete()); + IndexView computingBeanArchiveIndex = BeanArchives.buildComputingBeanArchiveIndex( + Thread.currentThread().getContextClassLoader(), + additionalClasses, immutableBeanArchiveIndex); + return new BeanArchiveIndexBuildItem(computingBeanArchiveIndex, immutableBeanArchiveIndex, generatedClassNames); } private IndexView buildApplicationIndex(ArcConfig config, ApplicationArchivesBuildItem applicationArchivesBuildItem, diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledProcessor.java index af0554f12876c..d5ae68152c298 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BuildTimeEnabledProcessor.java @@ -1,11 +1,14 @@ package io.quarkus.arc.deployment; +import static java.util.Arrays.asList; +import static java.util.Collections.emptySet; import static java.util.function.Predicate.not; +import static java.util.stream.Collectors.groupingBy; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -57,10 +60,9 @@ public class BuildTimeEnabledProcessor { @BuildStep void ifBuildProfile(CombinedIndexBuildItem index, BuildProducer producer) { - Collection annotationInstances = index.getIndex().getAnnotations(IF_BUILD_PROFILE); + List annotationInstances = getAnnotations(index.getIndex(), IF_BUILD_PROFILE); for (AnnotationInstance instance : annotationInstances) { - String profileOnInstance = instance.value().asString(); - boolean enabled = ConfigUtils.isProfileActive(profileOnInstance); + boolean enabled = BuildProfile.from(instance).enabled(); if (enabled) { LOGGER.debug("Enabling " + instance.target() + " since the profile value matches the active profile."); } else { @@ -72,14 +74,13 @@ void ifBuildProfile(CombinedIndexBuildItem index, BuildProducer producer) { - Collection annotationInstances = index.getIndex().getAnnotations(UNLESS_BUILD_PROFILE); + List annotationInstances = getAnnotations(index.getIndex(), UNLESS_BUILD_PROFILE); for (AnnotationInstance instance : annotationInstances) { - String profileOnInstance = instance.value().asString(); - boolean enabled = !ConfigUtils.isProfileActive(profileOnInstance); + boolean enabled = BuildProfile.from(instance).disabled(); if (enabled) { - LOGGER.debug("Enabling " + instance.target() + " since the profile value does not match the active profile."); + LOGGER.debug("Enabling " + instance.target() + " since the profile value matches the active profile."); } else { - LOGGER.debug("Disabling " + instance.target() + " since the profile value matches the active profile."); + LOGGER.debug("Disabling " + instance.target() + " since the profile value does not match the active profile."); } producer.produce(new BuildTimeConditionBuildItem(instance.target(), enabled)); } @@ -118,17 +119,7 @@ public void accept(AnnotationTarget target, Boolean enabled) { void buildProperty(DotName annotationName, DotName containingAnnotationName, BiFunction testFun, IndexView index, BiConsumer producer) { Config config = ConfigProviderResolver.instance().getConfig(); - List annotationInstances = new ArrayList<>(); - annotationInstances.addAll(index.getAnnotations(annotationName)); - // Collect containing annotation instances - // Note that we can't just use the IndexView.getAnnotationsWithRepeatable() method because the containing annotation is not part of the index - for (AnnotationInstance containingInstance : index.getAnnotations(containingAnnotationName)) { - for (AnnotationInstance nestedInstance : containingInstance.value().asNestedArray()) { - // We need to set the target of the containing instance - annotationInstances.add( - AnnotationInstance.create(nestedInstance.name(), containingInstance.target(), nestedInstance.values())); - } - } + List annotationInstances = getAnnotations(index, annotationName, containingAnnotationName); for (AnnotationInstance instance : annotationInstances) { String propertyName = instance.value("name").asString(); String expectedStringValue = instance.value("stringValue").asString(); @@ -230,7 +221,7 @@ BuildExclusionsBuildItem buildExclusions(List build final Map> map = buildTimeConditions.stream() .filter(not(BuildTimeConditionBuildItem::isEnabled)) .map(BuildTimeConditionBuildItem::getTarget) - .collect(Collectors.groupingBy( + .collect(groupingBy( AnnotationTarget::kind, Collectors.mapping(BuildExclusionsBuildItem::targetMapper, Collectors.toSet()))); return new BuildExclusionsBuildItem( @@ -256,4 +247,90 @@ private void transformBean(AnnotationTarget target, TransformationContext ctx, b transform.done(); } } + + private static List getAnnotations(IndexView index, DotName annotationName) { + return new ArrayList<>(index.getAnnotations(annotationName)); + } + + private static List getAnnotations( + IndexView index, + DotName annotationName, + DotName containingAnnotationName) { + + // Single annotation + List annotationInstances = getAnnotations(index, annotationName); + // Collect containing annotation instances + // Note that we can't just use the IndexView.getAnnotationsWithRepeatable() method because the containing annotation is not part of the index + for (AnnotationInstance containingInstance : index.getAnnotations(containingAnnotationName)) { + for (AnnotationInstance nestedInstance : containingInstance.value().asNestedArray()) { + // We need to set the target of the containing instance + annotationInstances.add( + AnnotationInstance.create(nestedInstance.name(), containingInstance.target(), nestedInstance.values())); + } + } + + return annotationInstances; + } + + private static class BuildProfile { + private final Set allOf; + private final Set anyOf; + + BuildProfile(final Set allOf, final Set anyOf) { + this.allOf = allOf; + this.anyOf = anyOf; + } + + boolean allMatch() { + if (allOf.isEmpty()) { + return true; + } + + for (String profile : allOf) { + if (!ConfigUtils.isProfileActive(profile)) { + return false; + } + } + return true; + } + + boolean anyMatch() { + if (anyOf.isEmpty()) { + return true; + } + + for (String profile : anyOf) { + if (ConfigUtils.isProfileActive(profile)) { + return true; + } + } + return false; + } + + boolean enabled() { + return allMatch() && anyMatch(); + } + + boolean disabled() { + return !enabled(); + } + + private static BuildProfile from(AnnotationInstance instance) { + AnnotationValue value = instance.value(); + + AnnotationValue allOfValue = instance.value("allOf"); + Set allOf = allOfValue != null ? new HashSet<>(asList(allOfValue.asStringArray())) : emptySet(); + + AnnotationValue anyOfValue = instance.value("anyOf"); + Set anyOf = new HashSet<>(); + if (value != null) { + anyOf.add(value.asString()); + } + if (anyOfValue != null) { + Collections.addAll(anyOf, anyOfValue.asStringArray()); + } + + return new BuildProfile(allOf, anyOf); + } + } } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java index 2f2bf5cc46bc7..978b99960df4e 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java @@ -383,6 +383,7 @@ void registerConfigMappingConverters(CombinedIndexBuildItem indexBuildItem, void validateConfigMappingsInjectionPoints( ArcConfig arcConfig, ValidationPhaseBuildItem validationPhase, + List unremovableBeans, List configClasses, BuildProducer configMappings) { @@ -422,10 +423,17 @@ void validateConfigMappingsInjectionPoints( } } - for (ConfigClassBuildItem configClass : configMappingTypes.values()) { - // We don't look in the beans here, because SR Config has an API that can retrieve the mapping without CDI - if (!arcConfig.shouldEnableBeanRemoval() || configClass.getConfigClass().isAnnotationPresent(Unremovable.class)) { - toRegister.add(new ConfigMappingBuildItem(configClass.getConfigClass(), configClass.getPrefix())); + if (arcConfig.shouldEnableBeanRemoval()) { + Set unremovableClassNames = unremovableBeans.stream() + .map(UnremovableBeanBuildItem::getClassNames) + .flatMap(Collection::stream) + .collect(toSet()); + + for (ConfigClassBuildItem configClass : configMappingTypes.values()) { + if (configClass.getConfigClass().isAnnotationPresent(Unremovable.class) + || unremovableClassNames.contains(configClass.getName().toString())) { + toRegister.add(new ConfigMappingBuildItem(configClass.getConfigClass(), configClass.getPrefix())); + } } } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/UnremovableBeanBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/UnremovableBeanBuildItem.java index a8103b6a5ae76..098b5115a60e2 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/UnremovableBeanBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/UnremovableBeanBuildItem.java @@ -47,15 +47,41 @@ public final class UnremovableBeanBuildItem extends MultiBuildItem { private final Predicate predicate; + private final Set classNames; public UnremovableBeanBuildItem(Predicate predicate) { this.predicate = predicate; + this.classNames = Collections.emptySet(); + } + + public UnremovableBeanBuildItem(BeanClassNameExclusion predicate) { + this.predicate = predicate; + this.classNames = Collections.singleton(predicate.className); + } + + public UnremovableBeanBuildItem(BeanClassNamesExclusion predicate) { + this.predicate = predicate; + this.classNames = predicate.classNames; + } + + public UnremovableBeanBuildItem(BeanTypeExclusion predicate) { + this.predicate = predicate; + this.classNames = Collections.singleton(predicate.dotName.toString()); + } + + public UnremovableBeanBuildItem(BeanTypesExclusion predicate) { + this.predicate = predicate; + this.classNames = predicate.dotNames.stream().map(DotName::toString).collect(Collectors.toSet()); } public Predicate getPredicate() { return predicate; } + public Set getClassNames() { + return classNames; + } + /** * Match beans whose bean class matches any of the specified class names. * diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/WrongAnnotationUsageProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/WrongAnnotationUsageProcessor.java index d7597589eb6c1..e10d994a4e877 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/WrongAnnotationUsageProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/WrongAnnotationUsageProcessor.java @@ -17,6 +17,7 @@ import org.jboss.jandex.IndexView; import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; +import io.quarkus.arc.processor.Annotations; import io.quarkus.arc.processor.DotNames; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -26,7 +27,8 @@ public class WrongAnnotationUsageProcessor { @BuildStep void detect(ArcConfig config, ApplicationIndexBuildItem applicationIndex, CustomScopeAnnotationsBuildItem scopeAnnotations, - TransformedAnnotationsBuildItem transformedAnnotations, BuildProducer validationErrors) { + TransformedAnnotationsBuildItem transformedAnnotations, BuildProducer validationErrors, + InterceptorResolverBuildItem interceptorResolverBuildItem) { if (!config.detectWrongAnnotations) { return; @@ -90,6 +92,14 @@ public String apply(AnnotationInstance annotationInstance) { new IllegalStateException(String.format( "The %s class %s declares a producer but it must be ignored per the CDI rules", clazz.nestingType().toString(), clazz.name().toString())))); + } else if (Annotations.containsAny(classAnnotations, interceptorResolverBuildItem.getInterceptorBindings()) + || Annotations.containsAny(clazz.annotations(), + interceptorResolverBuildItem.getInterceptorBindings())) { + // detect interceptor bindings on nested classes + validationErrors.produce(new ValidationErrorBuildItem( + new IllegalStateException(String.format( + "The %s class %s declares an interceptor binding but it must be ignored per CDI rules", + clazz.nestingType().toString(), clazz.name().toString())))); } } } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/DevBeanInfo.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/DevBeanInfo.java index d8ce275ab7a3c..7ff0417790d2e 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/DevBeanInfo.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/DevBeanInfo.java @@ -49,6 +49,7 @@ public static DevBeanInfo from(BeanInfo bean, CompletedApplicationClassPredicate DevBeanKind kind; String memberName; boolean isApplicationBean; + boolean isGenerated = false; Name declaringClass; if (target.kind() == Kind.METHOD) { MethodInfo method = target.asMethod(); @@ -56,35 +57,39 @@ public static DevBeanInfo from(BeanInfo bean, CompletedApplicationClassPredicate kind = DevBeanKind.METHOD; isApplicationBean = predicate.test(bean.getDeclaringBean().getBeanClass()); declaringClass = Name.from(bean.getDeclaringBean().getBeanClass()); + isGenerated = bean.getDeclaringBean().getImplClazz().isSynthetic(); } else if (target.kind() == Kind.FIELD) { FieldInfo field = target.asField(); memberName = field.name(); kind = DevBeanKind.FIELD; isApplicationBean = predicate.test(bean.getDeclaringBean().getBeanClass()); declaringClass = Name.from(bean.getDeclaringBean().getBeanClass()); + isGenerated = bean.getDeclaringBean().getImplClazz().isSynthetic(); } else if (target.kind() == Kind.CLASS) { ClassInfo clazz = target.asClass(); kind = DevBeanKind.CLASS; memberName = null; isApplicationBean = predicate.test(clazz.name()); + isGenerated = clazz.isSynthetic(); declaringClass = null; } else { throw new IllegalArgumentException("Invalid annotation target: " + target); } return new DevBeanInfo(bean.getIdentifier(), kind, isApplicationBean, providerType, memberName, types, qualifiers, scope, declaringClass, - interceptors); + interceptors, isGenerated); } else { // Synthetic bean return new DevBeanInfo(bean.getIdentifier(), DevBeanKind.SYNTHETIC, false, providerType, null, types, qualifiers, scope, null, - interceptors); + interceptors, bean.getImplClazz().isSynthetic()); } } public DevBeanInfo(String id, DevBeanKind kind, boolean isApplicationBean, Name providerType, String memberName, Set types, - Set qualifiers, Name scope, Name declaringClass, List boundInterceptors) { + Set qualifiers, Name scope, Name declaringClass, List boundInterceptors, + boolean isGenerated) { this.id = id; this.kind = kind; this.isApplicationBean = isApplicationBean; @@ -95,6 +100,7 @@ public DevBeanInfo(String id, DevBeanKind kind, boolean isApplicationBean, Name this.scope = scope; this.declaringClass = declaringClass; this.interceptors = boundInterceptors; + this.isGenerated = isGenerated; } private final String id; @@ -107,6 +113,7 @@ public DevBeanInfo(String id, DevBeanKind kind, boolean isApplicationBean, Name private final Name scope; private final Name declaringClass; private final List interceptors; + private final boolean isGenerated; public String getId() { return id; @@ -161,6 +168,10 @@ public List getInterceptors() { return interceptors; } + public boolean isGenerated() { + return isGenerated; + } + public String getDescription() { return description(false); } @@ -203,11 +214,18 @@ public String typeInfo(boolean simple) { @Override public int compareTo(DevBeanInfo o) { - // Application beans should go first - if (isApplicationBean == o.isApplicationBean) { - return providerType.compareTo(o.providerType); + // application beans come first + int result = Boolean.compare(o.isApplicationBean, isApplicationBean); + if (result != 0) { + return result; + } + // generated beans comes last + result = Boolean.compare(isGenerated, o.isGenerated); + if (result != 0) { + return result; } - return isApplicationBean ? -1 : 1; + // fallback to name comparison + return providerType.compareTo(o.providerType); } @Override diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/Name.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/Name.java index 828c49f7ccb4e..027db93f79175 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/Name.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/Name.java @@ -101,6 +101,11 @@ static String createSimple(AnnotationInstance annotation) { @Override public int compareTo(Name other) { + // Quarkus classes should be last + int result = Boolean.compare(isQuarkusClassName(), other.isQuarkusClassName()); + if (result != 0) { + return result; + } return name.compareTo(other.name); } @@ -109,4 +114,7 @@ public String toString() { return name; } + private boolean isQuarkusClassName() { + return name.startsWith("io.quarkus"); + } } diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/IfBuildProfileAllAnyTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/IfBuildProfileAllAnyTest.java new file mode 100644 index 0000000000000..f28c330a15e6d --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/IfBuildProfileAllAnyTest.java @@ -0,0 +1,197 @@ +package io.quarkus.arc.test.profile; + +import static java.util.stream.Collectors.toSet; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.profile.IfBuildProfile; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.config.SmallRyeConfig; + +public class IfBuildProfileAllAnyTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot( + (jar) -> jar.addAsResource(new StringAsset("quarkus.test.profile=test,build,any"), + "application.properties")); + + @Inject + SmallRyeConfig config; + @Inject + Instance ifBuildProfiles; + + @Test + void ifBuildProfile() { + assertTrue(config.getProfiles().contains("test")); + assertTrue(config.getProfiles().contains("build")); + assertTrue(config.getProfiles().contains("any")); + + Set ifProfiles = ifBuildProfiles.stream().map(IfBuildProfileBean::profile).collect(toSet()); + assertEquals(11, ifProfiles.size()); + assertTrue(ifProfiles.contains("test")); + assertTrue(ifProfiles.contains("allOf-test")); + assertTrue(ifProfiles.contains("anyOf-test")); + assertTrue(ifProfiles.contains("build")); + assertTrue(ifProfiles.contains("allOf-build")); + assertTrue(ifProfiles.contains("anyOf-build")); + assertTrue(ifProfiles.contains("allOf-test,allOf-build")); + assertTrue(ifProfiles.contains("anyOf-dev,anyOf-test,anyOf-build")); + assertTrue(ifProfiles.contains("allOf-test,anyOf-build")); + assertTrue(ifProfiles.contains("allOf-test-build,anyOf-any")); + assertTrue(ifProfiles.contains("allOf-test-build,anyOf-any-dev")); + } + + public interface IfBuildProfileBean { + String profile(); + } + + // Not active, the "dev" profile is not active + @ApplicationScoped + @IfBuildProfile("dev") + public static class DevBean implements IfBuildProfileBean { + @Override + public String profile() { + return "dev"; + } + } + + // Active, the "test" profile is active (when used as single value is treated as anyOf) + @ApplicationScoped + @IfBuildProfile("test") + public static class TestBean implements IfBuildProfileBean { + @Override + public String profile() { + return "test"; + } + } + + // Active, the "test" profile is active, and it is the only one required by allOf + @ApplicationScoped + @IfBuildProfile(allOf = "test") + public static class AllOfTestBean implements IfBuildProfileBean { + @Override + public String profile() { + return "allOf-test"; + } + } + + // Active, the "test" profile is active + @ApplicationScoped + @IfBuildProfile(anyOf = "test") + public static class AnyOfTestBean implements IfBuildProfileBean { + @Override + public String profile() { + return "anyOf-test"; + } + } + + // Active, the "build" profile is active (when used as single value is treated as anyOf) + @ApplicationScoped + @IfBuildProfile("build") + public static class BuildBean implements IfBuildProfileBean { + @Override + public String profile() { + return "build"; + } + } + + // Active, the "build" profile is active, and it is the only one required by allOf + @ApplicationScoped + @IfBuildProfile(allOf = "build") + public static class AllOfBuildBean implements IfBuildProfileBean { + @Override + public String profile() { + return "allOf-build"; + } + } + + // Active, the "build" profile is active, and it is the only one required by allOf + @ApplicationScoped + @IfBuildProfile(anyOf = "build") + public static class AnyOfBuildBean implements IfBuildProfileBean { + @Override + public String profile() { + return "anyOf-build"; + } + } + + // Active, both "test" and "build" profiles are active + @ApplicationScoped + @IfBuildProfile(allOf = { "test", "build" }) + public static class AllOfTestBuildBean implements IfBuildProfileBean { + @Override + public String profile() { + return "allOf-test,allOf-build"; + } + } + + // Not active, the "dev" profile is not active + @ApplicationScoped + @IfBuildProfile(allOf = { "dev", "test", "build" }) + public static class AllOfDevTestBuildBean implements IfBuildProfileBean { + @Override + public String profile() { + return "allOf-dev,allOf-test,allOf-build"; + } + } + + // Active, both "test" and "build" profiles are active, only one is required + @ApplicationScoped + @IfBuildProfile(anyOf = { "dev", "test", "build" }) + public static class AnyOfDevTestBuildBean implements IfBuildProfileBean { + @Override + public String profile() { + return "anyOf-dev,anyOf-test,anyOf-build"; + } + } + + // Not active, missing the "dev" profile in allOf + @ApplicationScoped + @IfBuildProfile(allOf = "dev", anyOf = "test") + public static class AllOfDevAnyOfTestBean implements IfBuildProfileBean { + @Override + public String profile() { + return "allOf-dev,anyOf-test"; + } + } + + // Active + @ApplicationScoped + @IfBuildProfile(allOf = "test", anyOf = "build") + public static class AllOfTestAnyOfBuildBean implements IfBuildProfileBean { + @Override + public String profile() { + return "allOf-test,anyOf-build"; + } + } + + // Active + @ApplicationScoped + @IfBuildProfile(allOf = { "test", "build" }, anyOf = "any") + public static class AllOfTestBuildAnyOfAnyBean implements IfBuildProfileBean { + @Override + public String profile() { + return "allOf-test-build,anyOf-any"; + } + } + + // Active + @ApplicationScoped + @IfBuildProfile(allOf = { "test", "build" }, anyOf = { "any", "dev" }) + public static class AllOfTestBuildAnyOfAnyDevBean implements IfBuildProfileBean { + @Override + public String profile() { + return "allOf-test-build,anyOf-any-dev"; + } + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/UnlessBuildProfileAllAnyTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/UnlessBuildProfileAllAnyTest.java new file mode 100644 index 0000000000000..23ff729b46940 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/profile/UnlessBuildProfileAllAnyTest.java @@ -0,0 +1,146 @@ +package io.quarkus.arc.test.profile; + +import static java.util.stream.Collectors.toSet; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.profile.UnlessBuildProfile; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.config.SmallRyeConfig; + +public class UnlessBuildProfileAllAnyTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot( + (jar) -> jar.addAsResource(new StringAsset("quarkus.test.profile=test,build"), "application.properties")); + + @Inject + SmallRyeConfig config; + @Inject + Instance unlessBuildProfiles; + + public interface UnlessBuildProfileBean { + String profile(); + } + + @Test + void unlessBuildProfile() { + assertTrue(config.getProfiles().contains("test")); + assertTrue(config.getProfiles().contains("build")); + + Set unlessProfiles = unlessBuildProfiles.stream().map(UnlessBuildProfileBean::profile).collect(toSet()); + assertEquals(2, unlessProfiles.size()); + assertTrue(unlessProfiles.contains("dev")); + assertTrue(unlessProfiles.contains("allOf-dev,allOf-test,allOf-build")); + } + + // Active, the "dev" profile is not active + @ApplicationScoped + @UnlessBuildProfile("dev") + public static class DevBean implements UnlessBuildProfileBean { + @Override + public String profile() { + return "dev"; + } + } + + // Not active, the "test" profile is active + @ApplicationScoped + @UnlessBuildProfile("test") + public static class TestBean implements UnlessBuildProfileBean { + @Override + public String profile() { + return "test"; + } + } + + // Not active, the "test" profile is active + @ApplicationScoped + @UnlessBuildProfile(allOf = "test") + public static class AllOfTestBean implements UnlessBuildProfileBean { + @Override + public String profile() { + return "allOf-test"; + } + } + + // Active, the "test" profile is active + @ApplicationScoped + @UnlessBuildProfile(anyOf = "test") + public static class AnyOfTestBean implements UnlessBuildProfileBean { + @Override + public String profile() { + return "anyOf-test"; + } + } + + // Not active, the "build" profile is active + @ApplicationScoped + @UnlessBuildProfile("build") + public static class BuildBean implements UnlessBuildProfileBean { + @Override + public String profile() { + return "build"; + } + } + + // Not active, the "build" profile is active + @ApplicationScoped + @UnlessBuildProfile(allOf = "build") + public static class AllOfBuildBean implements UnlessBuildProfileBean { + @Override + public String profile() { + return "allOf-build"; + } + } + + // Not active, the "build" profile is active + @ApplicationScoped + @UnlessBuildProfile(anyOf = "build") + public static class AnyOfBuildBean implements UnlessBuildProfileBean { + @Override + public String profile() { + return "anyOf-build"; + } + } + + // Not Active, both "test" and "build" profiles are active + @ApplicationScoped + @UnlessBuildProfile(allOf = { "test", "build" }) + public static class AllOfTestBuildBean implements UnlessBuildProfileBean { + @Override + public String profile() { + return "allOf-test,allOf-build"; + } + } + + // Active, the "dev" profile is not active, and it fails the allOf match + @ApplicationScoped + @UnlessBuildProfile(allOf = { "dev", "test", "build" }) + public static class AllOfDevTestBuildBean implements UnlessBuildProfileBean { + @Override + public String profile() { + return "allOf-dev,allOf-test,allOf-build"; + } + } + + // Not active, the "test" and "build" are active, and either profile fail the anyOf match + @ApplicationScoped + @UnlessBuildProfile(anyOf = { "dev", "test", "build" }) + public static class AnyOfDevTestBuildBean implements IfBuildProfileAllAnyTest.IfBuildProfileBean { + @Override + public String profile() { + return "anyOf-dev,anyOf-test,anyOf-build"; + } + } +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/profile/IfBuildProfile.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/profile/IfBuildProfile.java index 293db758b814f..ddf7ca9532a08 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/profile/IfBuildProfile.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/profile/IfBuildProfile.java @@ -7,11 +7,57 @@ /** * When applied to a bean class or producer method (or field), the bean will only be enabled - * if the Quarkus build time profile matches the specified annotation value. + * if the Quarkus build time profile matches the rules of the annotation values. + * + *
+ * + *
+ *    Enabled when "dev" profile is active:
+ *
+ *    @ApplicationScoped
+ *    @IfBuildProfile("dev")
+ *    public class DevBean {
+ *    }
+ *
+ *    Enabled when both "build" and "dev" profiles are active:
+ *
+ *    @ApplicationScoped
+ *    @IfBuildProfile(allOf = {"build", "dev"})
+ *    public class BuildDevBean {
+ *    }
+ *
+ *    Enabled if either "build" or "dev" profile is active:
+ *
+ *    @ApplicationScoped
+ *    @IfBuildProfile(anyOf = {"build", "dev"})
+ *    public class BuildDevBean {
+ *    }
+ *
+ *    Enabled when both "build" and "dev" profiles are active and either "test" or "prod" profile is active:
+ *
+ *    @ApplicationScoped
+ *    @IfBuildProfile(allOf = {"build", "dev"}, anyOf = {"test", "prod"})
+ *    public class BuildDevBean {
+ *    }
+ * 
+ * + *
*/ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE, ElementType.FIELD }) public @interface IfBuildProfile { + /** + * A single profile name to enable a bean if a profile with the same name is active in Quarkus build time config. + */ + String value() default ""; - String value(); + /** + * Multiple profiles names to enable a bean if all the profile names are active in Quarkus build time config. + */ + String[] allOf() default {}; + + /** + * Multiple profiles names to enable a bean if any the profile names is active in Quarkus build time config. + */ + String[] anyOf() default {}; } diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/profile/UnlessBuildProfile.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/profile/UnlessBuildProfile.java index f6f3951c37886..158f4cacd0a82 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/profile/UnlessBuildProfile.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/profile/UnlessBuildProfile.java @@ -7,11 +7,61 @@ /** * When applied to a bean class or producer method (or field), the bean will only be enabled - * if the Quarkus build time profile does not match the specified annotation value. + * if the Quarkus build time profile does not match the rules of the annotation values. + * + *
+ * + *
+ *    Enabled when "dev" profile is not active:
+ *
+ *    @ApplicationScoped
+ *    @IfBuildProfile("dev")
+ *    public class NotDevBean {
+ *    }
+ *
+ *    Enabled when both "build" and "dev" profiles are not active:
+ *
+ *    @ApplicationScoped
+ *    @IfBuildProfile(allOf = {"build", "dev"})
+ *    public class NotBuildDevBean {
+ *    }
+ *
+ *    Enabled if either "build" or "dev" profile is not active:
+ *
+ *    @ApplicationScoped
+ *    @IfBuildProfile(anyOf = {"build", "dev"})
+ *    public class NotBuildDevBean {
+ *    }
+ *
+ *    Enabled when both "build" and "dev" profiles are not active and either "test" or "prod" profile is
+ *    not active:
+ *
+ *    @ApplicationScoped
+ *    @IfBuildProfile(allOf = {"build", "dev"}, anyOf = {"test", "prod"})
+ *    public class NotBuildDevBean {
+ *    }
+ * 
+ * + *
*/ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE, ElementType.FIELD }) public @interface UnlessBuildProfile { + /** + * A single profile name to enable a bean if a profile with the same name is not active in Quarkus build + * time config. + */ + String value() default ""; - String value(); + /** + * Multiple profiles names to enable a bean if all the profile names are not active in Quarkus build time + * config. + */ + String[] allOf() default {}; + + /** + * Multiple profiles names to enable a bean if any the profile names is not active in Quarkus build time + * config. + */ + String[] anyOf() default {}; } diff --git a/extensions/azure-functions-http/maven-archetype/src/main/resources/archetype-resources/pom.xml b/extensions/azure-functions-http/maven-archetype/src/main/resources/archetype-resources/pom.xml index 643e676782a42..9fb478b3726f8 100644 --- a/extensions/azure-functions-http/maven-archetype/src/main/resources/archetype-resources/pom.xml +++ b/extensions/azure-functions-http/maven-archetype/src/main/resources/archetype-resources/pom.xml @@ -6,7 +6,7 @@ \${artifactId} \${version} - 3.8.1 + 3.10.1 true 11 11 diff --git a/extensions/cache/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/cache/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 186ae789f8fa1..24b7c04af0ad8 100644 --- a/extensions/cache/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/cache/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -8,6 +8,6 @@ metadata: guide: "https://quarkus.io/guides/cache" categories: - "data" - status: "preview" + status: "stable" config: - "quarkus.cache." diff --git a/extensions/csrf-reactive/deployment/src/main/java/io/quarkus/csrf/reactive/CsrfReactiveBuildStep.java b/extensions/csrf-reactive/deployment/src/main/java/io/quarkus/csrf/reactive/CsrfReactiveBuildStep.java index 1028bfac08b5e..886289a8c1810 100644 --- a/extensions/csrf-reactive/deployment/src/main/java/io/quarkus/csrf/reactive/CsrfReactiveBuildStep.java +++ b/extensions/csrf-reactive/deployment/src/main/java/io/quarkus/csrf/reactive/CsrfReactiveBuildStep.java @@ -1,31 +1,15 @@ package io.quarkus.csrf.reactive; -import java.util.Collections; -import java.util.List; -import java.util.Map; import java.util.function.BooleanSupplier; -import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.MethodInfo; -import org.jboss.resteasy.reactive.server.model.FixedHandlersChainCustomizer; -import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; -import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner; - import io.quarkus.arc.deployment.AdditionalBeanBuildItem; -import io.quarkus.csrf.reactive.runtime.CsrfHandler; -import io.quarkus.csrf.reactive.runtime.CsrfReactiveConfig; -import io.quarkus.csrf.reactive.runtime.CsrfRecorder; -import io.quarkus.csrf.reactive.runtime.CsrfResponseFilter; +import io.quarkus.csrf.reactive.runtime.CsrfRequestResponseReactiveFilter; import io.quarkus.csrf.reactive.runtime.CsrfTokenParameterProvider; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; -import io.quarkus.deployment.annotations.ExecutionTime; -import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.resteasy.reactive.server.spi.HandlerConfigurationProviderBuildItem; -import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; @BuildSteps(onlyIf = CsrfReactiveBuildStep.IsEnabled.class) public class CsrfReactiveBuildStep { @@ -34,33 +18,11 @@ public class CsrfReactiveBuildStep { void registerProvider(BuildProducer additionalBeans, BuildProducer reflectiveClass, BuildProducer additionalIndexedClassesBuildItem) { - additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CsrfResponseFilter.class)); - reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, CsrfResponseFilter.class)); - additionalIndexedClassesBuildItem - .produce(new AdditionalIndexedClassesBuildItem(CsrfResponseFilter.class.getName())); - + additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CsrfRequestResponseReactiveFilter.class)); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, CsrfRequestResponseReactiveFilter.class)); additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CsrfTokenParameterProvider.class)); - } - - @BuildStep - public MethodScannerBuildItem configureHandler() { - return new MethodScannerBuildItem(new MethodScanner() { - @Override - public List scan(MethodInfo method, ClassInfo actualEndpointClass, - Map methodContext) { - return Collections.singletonList( - new FixedHandlersChainCustomizer( - List.of(new CsrfHandler()), - HandlerChainCustomizer.Phase.BEFORE_METHOD_INVOKE)); - } - }); - } - - @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - public HandlerConfigurationProviderBuildItem applyRuntimeConfig(CsrfRecorder recorder, - CsrfReactiveConfig csrfReactiveConfig) { - return new HandlerConfigurationProviderBuildItem(CsrfReactiveConfig.class, recorder.configure(csrfReactiveConfig)); + additionalIndexedClassesBuildItem + .produce(new AdditionalIndexedClassesBuildItem(CsrfRequestResponseReactiveFilter.class.getName())); } public static class IsEnabled implements BooleanSupplier { diff --git a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRecorder.java b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRecorder.java deleted file mode 100644 index f1580c9b023dd..0000000000000 --- a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRecorder.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.quarkus.csrf.reactive.runtime; - -import java.util.function.Supplier; - -import io.quarkus.runtime.annotations.Recorder; - -@Recorder -public class CsrfRecorder { - - public Supplier configure(CsrfReactiveConfig csrfReactiveConfig) { - return new Supplier() { - @Override - public CsrfReactiveConfig get() { - return csrfReactiveConfig; - } - }; - } - -} diff --git a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfHandler.java b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java similarity index 64% rename from extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfHandler.java rename to extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java index ac8f2c340fdf7..29e7b1458c65c 100644 --- a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfHandler.java +++ b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java @@ -1,23 +1,29 @@ package io.quarkus.csrf.reactive.runtime; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; import java.security.SecureRandom; import java.util.Base64; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.server.ServerRequestFilter; +import org.jboss.resteasy.reactive.server.ServerResponseFilter; +import org.jboss.resteasy.reactive.server.WithFormRead; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; -import org.jboss.resteasy.reactive.server.spi.GenericRuntimeConfigurableServerRestHandler; +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext; import io.vertx.core.http.Cookie; +import io.vertx.core.http.impl.CookieImpl; +import io.vertx.core.http.impl.ServerCookie; import io.vertx.ext.web.RoutingContext; -public class CsrfHandler implements GenericRuntimeConfigurableServerRestHandler { - private static final Logger LOG = Logger.getLogger(CsrfHandler.class); +public class CsrfRequestResponseReactiveFilter { + private static final Logger LOG = Logger.getLogger(CsrfRequestResponseReactiveFilter.class); /** * CSRF token key. @@ -30,27 +36,12 @@ public class CsrfHandler implements GenericRuntimeConfigurableServerRestHandler< */ private static final String CSRF_TOKEN_VERIFIED = "csrf_token_verified"; - // although technically the field does not need to be volatile (since the access mode is determined by the VarHandle use) - // it is a recommended practice by Doug Lea meant to catch cases where the field is accessed directly (by accident) - @SuppressWarnings("unused") - private volatile SecureRandom secureRandom; - - // use a VarHandle to access the secureRandom as the value is written only by the main thread - // and all other threads simply read the value, and thus we can use the Release / Acquire access mode - private static final VarHandle SECURE_RANDOM_VH; - - static { - try { - SECURE_RANDOM_VH = MethodHandles.lookup().findVarHandle(CsrfHandler.class, "secureRandom", - SecureRandom.class); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new Error(e); - } - } + private final SecureRandom secureRandom = new SecureRandom(); - private CsrfReactiveConfig config; + @Inject + Instance configInstance; - public CsrfHandler() { + public CsrfRequestResponseReactiveFilter() { } /** @@ -68,10 +59,10 @@ public CsrfHandler() { * {@value #CSRF_TOKEN_KEY} and value that is equal to the one supplied in the cookie. * */ - public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { - final ContainerRequestContext requestContext = reactiveRequestContext.getContainerRequestContext(); - - final RoutingContext routing = reactiveRequestContext.serverRequest().unwrap(RoutingContext.class); + @ServerRequestFilter + @WithFormRead + public void filter(ResteasyReactiveContainerRequestContext requestContext, RoutingContext routing) { + final CsrfReactiveConfig config = this.configInstance.get(); String cookieToken = getCookieToken(routing, config); if (cookieToken != null) { @@ -99,7 +90,7 @@ public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { if (cookieToken == null && isCsrfTokenRequired(routing, config)) { // Set the CSRF cookie with a randomly generated value byte[] tokenBytes = new byte[config.tokenSize]; - getSecureRandom().nextBytes(tokenBytes); + secureRandom.nextBytes(tokenBytes); routing.put(CSRF_TOKEN_BYTES_KEY, tokenBytes); routing.put(CSRF_TOKEN_KEY, Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes)); } @@ -115,7 +106,6 @@ public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { } else { LOG.debugf("Request has the media type: %s, skipping the token verification", requestContext.getMediaType().toString()); - requestContext.abortWith(badClientRequest()); return; } } @@ -132,8 +122,9 @@ public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { return; } - String csrfToken = (String) reactiveRequestContext.getFormParameter(config.formFieldName, true, true); - + ResteasyReactiveRequestContext rrContext = (ResteasyReactiveRequestContext) requestContext + .getServerRequestContext(); + String csrfToken = (String) rrContext.getFormParameter(config.formFieldName, true, false); if (csrfToken == null) { LOG.debug("CSRF token is not found"); requestContext.abortWith(badClientRequest()); @@ -148,6 +139,7 @@ public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { return; } else { routing.put(CSRF_TOKEN_VERIFIED, true); + return; } } } else if (cookieToken == null) { @@ -156,10 +148,6 @@ public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { } } - private SecureRandom getSecureRandom() { - return (SecureRandom) SECURE_RANDOM_VH.getAcquire(this); - } - private static boolean isMatchingMediaType(MediaType contentType, MediaType expectedType) { return contentType.getType().equals(expectedType.getType()) && contentType.getSubtype().equals(expectedType.getSubtype()); @@ -169,6 +157,47 @@ private static Response badClientRequest() { return Response.status(400).build(); } + /** + * If the requirements below are true, sets a cookie by the name {@value #CSRF_TOKEN_KEY} that contains a CSRF token. + *
    + *
  • The request method is {@code GET}.
  • + *
  • The request does not contain a valid CSRF token cookie.
  • + *
+ * + * @throws IllegalStateException if the {@link RoutingContext} does not have a value for the key {@value #CSRF_TOKEN_KEY} + * and a cookie needs to be set. + */ + @ServerResponseFilter + public void filter(ContainerRequestContext requestContext, + ContainerResponseContext responseContext, RoutingContext routing) { + final CsrfReactiveConfig config = configInstance.get(); + if (requestContext.getMethod().equals("GET") && isCsrfTokenRequired(routing, config) + && getCookieToken(routing, config) == null) { + + String cookieValue = null; + if (config.tokenSignatureKey.isPresent()) { + byte[] csrfTokenBytes = (byte[]) routing.get(CSRF_TOKEN_BYTES_KEY); + + if (csrfTokenBytes == null) { + throw new IllegalStateException( + "CSRF Filter should have set the property " + CSRF_TOKEN_KEY + ", but it is null"); + } + cookieValue = CsrfTokenUtils.signCsrfToken(csrfTokenBytes, config.tokenSignatureKey.get()); + } else { + String csrfToken = (String) routing.get(CSRF_TOKEN_KEY); + + if (csrfToken == null) { + throw new IllegalStateException( + "CSRF Filter should have set the property " + CSRF_TOKEN_KEY + ", but it is null"); + } + cookieValue = csrfToken; + } + + createCookie(cookieValue, routing, config); + } + + } + /** * Gets the CSRF token from the CSRF cookie from the current {@code RoutingContext}. * @@ -189,6 +218,19 @@ private boolean isCsrfTokenRequired(RoutingContext routing, CsrfReactiveConfig c return config.createTokenPath.isPresent() ? config.createTokenPath.get().contains(routing.request().path()) : true; } + private void createCookie(String csrfToken, RoutingContext routing, CsrfReactiveConfig config) { + + ServerCookie cookie = new CookieImpl(config.cookieName, csrfToken); + cookie.setHttpOnly(true); + cookie.setSecure(config.cookieForceSecure || routing.request().isSSL()); + cookie.setMaxAge(config.cookieMaxAge.toSeconds()); + cookie.setPath(config.cookiePath); + if (config.cookieDomain.isPresent()) { + cookie.setDomain(config.cookieDomain.get()); + } + routing.response().addCookie(cookie); + } + private static boolean requestMethodIsSafe(ContainerRequestContext context) { switch (context.getMethod()) { case "GET": @@ -199,14 +241,4 @@ private static boolean requestMethodIsSafe(ContainerRequestContext context) { return false; } } - - public void configure(CsrfReactiveConfig configuration) { - this.config = configuration; - SECURE_RANDOM_VH.setRelease(this, new SecureRandom()); - } - - @Override - public Class getConfigurationClass() { - return CsrfReactiveConfig.class; - } } diff --git a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfResponseFilter.java b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfResponseFilter.java deleted file mode 100644 index 7cb34dbd5eaa7..0000000000000 --- a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfResponseFilter.java +++ /dev/null @@ -1,104 +0,0 @@ -package io.quarkus.csrf.reactive.runtime; - -import javax.enterprise.inject.Instance; -import javax.inject.Inject; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.container.ContainerResponseContext; - -import org.jboss.logging.Logger; -import org.jboss.resteasy.reactive.server.ServerResponseFilter; - -import io.vertx.core.http.Cookie; -import io.vertx.core.http.impl.CookieImpl; -import io.vertx.core.http.impl.ServerCookie; -import io.vertx.ext.web.RoutingContext; - -public class CsrfResponseFilter { - private static final Logger LOG = Logger.getLogger(CsrfResponseFilter.class); - - /** - * CSRF token key. - */ - private static final String CSRF_TOKEN_KEY = "csrf_token"; - private static final String CSRF_TOKEN_BYTES_KEY = "csrf_token_bytes"; - - @Inject - Instance config; - - public CsrfResponseFilter() { - } - - /** - * If the requirements below are true, sets a cookie by the name {@value #CSRF_TOKEN_KEY} that contains a CSRF token. - *
    - *
  • The request method is {@code GET}.
  • - *
  • The request does not contain a valid CSRF token cookie.
  • - *
- * - * @throws IllegalStateException if the {@link RoutingContext} does not have a value for the key {@value #CSRF_TOKEN_KEY} - * and a cookie needs to be set. - */ - @ServerResponseFilter - public void filter(ContainerRequestContext requestContext, - ContainerResponseContext responseContext, RoutingContext routing) { - if (requestContext.getMethod().equals("GET") && isCsrfTokenRequired(routing, config.get()) - && getCookieToken(routing, config.get()) == null) { - - String cookieValue = null; - if (config.get().tokenSignatureKey.isPresent()) { - byte[] csrfTokenBytes = (byte[]) routing.get(CSRF_TOKEN_BYTES_KEY); - - if (csrfTokenBytes == null) { - throw new IllegalStateException( - "CSRF Filter should have set the property " + CSRF_TOKEN_KEY + ", but it is null"); - } - cookieValue = CsrfTokenUtils.signCsrfToken(csrfTokenBytes, config.get().tokenSignatureKey.get()); - } else { - String csrfToken = (String) routing.get(CSRF_TOKEN_KEY); - - if (csrfToken == null) { - throw new IllegalStateException( - "CSRF Filter should have set the property " + CSRF_TOKEN_KEY + ", but it is null"); - } - cookieValue = csrfToken; - } - - createCookie(cookieValue, routing, config.get()); - } - - } - - /** - * Gets the CSRF token from the CSRF cookie from the current {@code RoutingContext}. - * - * @return An Optional containing the token, or an empty Optional if the token cookie is not present or is invalid - */ - private String getCookieToken(RoutingContext routing, CsrfReactiveConfig config) { - Cookie cookie = routing.getCookie(config.cookieName); - - if (cookie == null) { - LOG.debug("CSRF token cookie is not set"); - return null; - } - - return cookie.getValue(); - } - - private boolean isCsrfTokenRequired(RoutingContext routing, CsrfReactiveConfig config) { - return config.createTokenPath.isPresent() ? config.createTokenPath.get().contains(routing.request().path()) : true; - } - - private void createCookie(String csrfToken, RoutingContext routing, CsrfReactiveConfig config) { - - ServerCookie cookie = new CookieImpl(config.cookieName, csrfToken); - cookie.setHttpOnly(true); - cookie.setSecure(config.cookieForceSecure || routing.request().isSSL()); - cookie.setMaxAge(config.cookieMaxAge.toSeconds()); - cookie.setPath(config.cookiePath); - if (config.cookieDomain.isPresent()) { - cookie.setDomain(config.cookieDomain.get()); - } - routing.response().addCookie(cookie); - } - -} diff --git a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java index 627712c359bfa..468fff82eae9d 100644 --- a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java +++ b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java @@ -200,8 +200,8 @@ private RunningDevService startDevDb(String dbName, boolean explicitlyDisabled = !(dataSourceBuildTimeConfig.devservices.enabled.orElse(true)); if (explicitlyDisabled) { //explicitly disabled - log.debug("Not starting devservices for " + (dbName == null ? "default datasource" : dbName) - + " as it has been disabled in the config"); + log.debug("Not starting Dev Services for " + (dbName == null ? "default datasource" : dbName) + + " as it has been disabled in the configuration"); return null; } @@ -222,8 +222,8 @@ private RunningDevService startDevDb(String dbName, List configHandlers = configurationHandlerBuildItems .get(defaultDbKind.get()); if (devDbProvider == null || configHandlers == null) { - log.warn("Unable to start devservices for " + (dbName == null ? "default datasource" : dbName) - + " as this datasource type (" + defaultDbKind.get() + ") does not support devservices"); + log.warn("Unable to start Dev Services for " + (dbName == null ? "default datasource" : dbName) + + " as this datasource type (" + defaultDbKind.get() + ") does not support Dev Services"); return null; } @@ -232,7 +232,7 @@ private RunningDevService startDevDb(String dbName, if (i.getCheckConfiguredFunction().test(dbName)) { //this database has explicit configuration //we don't start the devservices - log.debug("Not starting devservices for " + (dbName == null ? "default datasource" : dbName) + log.debug("Not starting Dev Services for " + (dbName == null ? "default datasource" : dbName) + " as it has explicit configuration"); return null; } diff --git a/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java b/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java index 50046b1b90d37..3668d11dcfee4 100644 --- a/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java +++ b/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java @@ -118,7 +118,7 @@ public String getEffectiveJdbcUrl() { } public String getReactiveUrl() { - return getEffectiveJdbcUrl().replaceFirst("jdbc:", "vertx-reactive:"); + return getEffectiveJdbcUrl().replaceFirst("jdbc:mariadb:", "vertx-reactive:mysql:"); } } } diff --git a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/ElasticsearchRestClientProcessor.java b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/ElasticsearchRestClientProcessor.java index ff0bf4f57690c..a83ec89f278c8 100644 --- a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/ElasticsearchRestClientProcessor.java +++ b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/ElasticsearchRestClientProcessor.java @@ -4,7 +4,6 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; class ElasticsearchRestClientProcessor { @@ -14,10 +13,4 @@ public void build(BuildProducer extensionSsl extensionSslNativeSupport.produce(new ExtensionSslNativeSupportBuildItem(Feature.ELASTICSEARCH_REST_CLIENT_COMMON)); } - @BuildStep - public ReflectiveClassBuildItem registerForReflection() { - return new ReflectiveClassBuildItem(true, true, - "org.apache.logging.log4j.message.ReusableMessageFactory", - "org.apache.logging.log4j.message.DefaultFlowMessageFactory"); - } } diff --git a/extensions/elasticsearch-rest-high-level-client/deployment/src/main/java/io/quarkus/elasticsearch/restclient/highlevel/deployment/ElasticsearchHighLevelClientProcessor.java b/extensions/elasticsearch-rest-high-level-client/deployment/src/main/java/io/quarkus/elasticsearch/restclient/highlevel/deployment/ElasticsearchHighLevelClientProcessor.java index c78c74997b975..4d1877d2c65d9 100644 --- a/extensions/elasticsearch-rest-high-level-client/deployment/src/main/java/io/quarkus/elasticsearch/restclient/highlevel/deployment/ElasticsearchHighLevelClientProcessor.java +++ b/extensions/elasticsearch-rest-high-level-client/deployment/src/main/java/io/quarkus/elasticsearch/restclient/highlevel/deployment/ElasticsearchHighLevelClientProcessor.java @@ -4,6 +4,7 @@ import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.elasticsearch.restclient.highlevel.runtime.ElasticsearchRestHighLevelClientProducer; class ElasticsearchHighLevelClientProcessor { @@ -18,4 +19,11 @@ AdditionalBeanBuildItem build() { return AdditionalBeanBuildItem.unremovableOf(ElasticsearchRestHighLevelClientProducer.class); } + @BuildStep + public ReflectiveClassBuildItem registerForReflection() { + return new ReflectiveClassBuildItem(true, false, + "org.apache.logging.log4j.message.ReusableMessageFactory", + "org.apache.logging.log4j.message.DefaultFlowMessageFactory"); + } + } diff --git a/extensions/elytron-security-ldap/deployment/src/main/java/io/quarkus/elytron/security/ldap/deployment/ElytronSecurityLdapProcessor.java b/extensions/elytron-security-ldap/deployment/src/main/java/io/quarkus/elytron/security/ldap/deployment/ElytronSecurityLdapProcessor.java index 6265b60c3c2b7..108070e3c76e3 100644 --- a/extensions/elytron-security-ldap/deployment/src/main/java/io/quarkus/elytron/security/ldap/deployment/ElytronSecurityLdapProcessor.java +++ b/extensions/elytron-security-ldap/deployment/src/main/java/io/quarkus/elytron/security/ldap/deployment/ElytronSecurityLdapProcessor.java @@ -61,7 +61,11 @@ ElytronPasswordMarkerBuildItem marker(LdapSecurityRealmBuildTimeConfig ldapSecur } @BuildStep - ReflectiveClassBuildItem enableReflection() { - return new ReflectiveClassBuildItem(true, true, QuarkusDirContextFactory.INITIAL_CONTEXT_FACTORY); + void registerForReflection(BuildProducer reflection) { + // All JDK provided InitialContextFactory impls via the module descriptors: + // com.sun.jndi.ldap.LdapCtxFactory, com.sun.jndi.dns.DnsContextFactory and com.sun.jndi.rmi.registry.RegistryContextFactory + reflection.produce(new ReflectiveClassBuildItem(true, true, QuarkusDirContextFactory.INITIAL_CONTEXT_FACTORY)); + reflection.produce(new ReflectiveClassBuildItem(false, false, "com.sun.jndi.dns.DnsContextFactory")); + reflection.produce(new ReflectiveClassBuildItem(false, false, "com.sun.jndi.rmi.registry.RegistryContextFactory")); } } diff --git a/extensions/funqy/funqy-amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml b/extensions/funqy/funqy-amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml index 919bcad791b75..78ff2cf9929e6 100644 --- a/extensions/funqy/funqy-amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml +++ b/extensions/funqy/funqy-amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml @@ -7,7 +7,7 @@ \${artifactId} \${version} - 3.8.1 + 3.10.1 true 11 11 diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java index ce7dd0ba1b0ea..d50a7c48ad6bf 100644 --- a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java +++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java @@ -2,6 +2,7 @@ import static io.quarkus.deployment.Feature.GRPC_SERVER; import static io.quarkus.grpc.deployment.GrpcDotNames.BLOCKING; +import static io.quarkus.grpc.deployment.GrpcDotNames.MUTINY_SERVICE; import static io.quarkus.grpc.deployment.GrpcDotNames.NON_BLOCKING; import static io.quarkus.grpc.deployment.GrpcDotNames.TRANSACTIONAL; import static io.quarkus.grpc.deployment.GrpcInterceptors.MICROMETER_INTERCEPTORS; @@ -369,12 +370,31 @@ private static boolean methodIsBlocking(List classes, String methodNa * */ static Set gatherBlockingMethodNames(ClassInfo service, IndexView index) { - List classes = classHierarchy(service, index); Set result = new HashSet<>(); - // Collect all gRPC methods from the *ImplBase class, if present - List implBaseMethods = classes.get(classes.size() - 1).methods(); + // We need to check if the service implementation extends the generated Mutiny interface + // or the regular "ImplBase" class. + + boolean isExtendingMutinyService = false; + for (DotName interfaceName : service.interfaceNames()) { + ClassInfo info = index.getClassByName(interfaceName); + if (info != null && info.interfaceNames().contains(MUTINY_SERVICE)) { + isExtendingMutinyService = true; + break; + } + } + + ClassInfo classInfo; + var classes = classHierarchy(service, index); + if (isExtendingMutinyService) { + classInfo = service; + } else { + // Collect all gRPC methods from the *ImplBase class, if present + classInfo = classes.get(classes.size() - 1); + } + + List implBaseMethods = classInfo.methods(); for (MethodInfo implBaseMethod : implBaseMethods) { String methodName = implBaseMethod.name(); diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/deployment/GrpcServerProcessorTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/deployment/GrpcServerProcessorTest.java index 329feb872b1f9..2c416a3e594f5 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/deployment/GrpcServerProcessorTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/deployment/GrpcServerProcessorTest.java @@ -20,6 +20,7 @@ import io.smallrye.common.annotation.Blocking; import io.smallrye.common.annotation.NonBlocking; +import io.smallrye.mutiny.Uni; public class GrpcServerProcessorTest { @@ -30,6 +31,7 @@ static Stream blockingAnnotations() { arguments(NonBlockingExtendsBlockingRoot.class, NonBlockingExtendsBlockingRoot.EXPECTED), arguments(ExtendsBlockingRoot.class, ExtendsBlockingRoot.EXPECTED), arguments(NonBlockingRoot.class, NonBlockingRoot.EXPECTED), + arguments(NoClassAnnotationMutinyRoot.class, NoClassAnnotationMutinyRoot.EXPECTED), arguments(BlockingExtendsNonBlockingRoot.class, BlockingExtendsNonBlockingRoot.EXPECTED), arguments(NonBlockingExtendsNonBlockingRoot.class, NonBlockingExtendsNonBlockingRoot.EXPECTED), arguments(ExtendsNonBlockingRoot.class, ExtendsNonBlockingRoot.EXPECTED), @@ -149,6 +151,29 @@ void noAnnotation() { } } + static class NoClassAnnotationMutinyRoot { + static final Set EXPECTED = ImmutableSet.of("blocking", "transactional"); + + @NonBlocking + Uni nonBlocking() { + return Uni.createFrom().nullItem(); + } + + @Blocking + Uni blocking() { + return Uni.createFrom().nullItem(); + } + + @Transactional + Uni transactional() { + return Uni.createFrom().nullItem(); + } + + Uni noAnnotation() { + return Uni.createFrom().nullItem(); + } + } + static class NoClassAnnotationsReverseMeaning extends NoClassAnnotationsRoot { static final Set EXPECTED = ImmutableSet.of("nonBlocking", "noAnnotation"); diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/deployment/MutinyServiceBlockingMethodTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/deployment/MutinyServiceBlockingMethodTest.java new file mode 100644 index 0000000000000..53ca300bed45c --- /dev/null +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/deployment/MutinyServiceBlockingMethodTest.java @@ -0,0 +1,35 @@ +package io.quarkus.grpc.deployment; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; +import org.jboss.jandex.Indexer; +import org.junit.jupiter.api.Test; + +import io.quarkus.grpc.server.services.BlockingMutinyHelloService; + +/** + * Verify that methods annotated with @Blocking from services implementing the MutinyService interface are considered blocking. + */ +public class MutinyServiceBlockingMethodTest { + + @Test + public void testBlocking() throws Exception { + Class clazz = BlockingMutinyHelloService.class; + DotName className = DotName.createSimple(clazz.getName()); + + Indexer indexer = new Indexer(); + indexer.indexClass(BlockingMutinyHelloService.class); + Index index = indexer.complete(); + + ClassInfo classInfo = index.getClassByName(className); + + assertThat(GrpcServerProcessor.gatherBlockingMethodNames(classInfo, index)) + .containsExactlyInAnyOrderElementsOf(List.of("sayHello", "wEIRD")); + } + +} diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/services/BlockingMutinyHelloService.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/services/BlockingMutinyHelloService.java index 03132730abda3..cfa91fafcd8dd 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/services/BlockingMutinyHelloService.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/services/BlockingMutinyHelloService.java @@ -14,6 +14,10 @@ public class BlockingMutinyHelloService implements Greeter { @Override @Blocking public Uni sayHello(HelloRequest request) { + + // Force failure if awaiting + Uni.createFrom().nullItem().await().indefinitely(); + return Uni.createFrom().item(request.getName()) .map(s -> Thread.currentThread().getName() + " " + s) .map(s -> HelloReply.newBuilder().setMessage(s).build()); diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java index 3d7fa832cde4b..84384ff7b0ce0 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java @@ -329,9 +329,16 @@ public enum NullOrdering { @ConfigItem(defaultValue = "none") public NullOrdering defaultNullOrdering; + /** + * Enables IN clause parameter padding which improves statement caching. + */ + @ConfigItem(defaultValue = "true") + public boolean inClauseParameterPadding; + public boolean isAnyPropertySet() { return queryPlanCacheMaxSize != DEFAULT_QUERY_PLAN_CACHE_MAX_SIZE - || defaultNullOrdering != NullOrdering.NONE; + || defaultNullOrdering != NullOrdering.NONE + || !inClauseParameterPadding; } } diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java index 3b9d8b956f323..0159dc8f6fa87 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java @@ -1072,6 +1072,9 @@ private static void producePersistenceUnitDescriptorFromConfig( descriptor.getProperties().setProperty(AvailableSettings.DEFAULT_NULL_ORDERING, persistenceUnitConfig.query.defaultNullOrdering.name().toLowerCase(Locale.ROOT)); + descriptor.getProperties().setProperty(AvailableSettings.IN_CLAUSE_PARAMETER_PADDING, + String.valueOf(persistenceUnitConfig.query.inClauseParameterPadding)); + // Disable sequence validations: they are reportedly slow, and people already get the same validation from normal schema validation descriptor.getProperties().put(AvailableSettings.SEQUENCE_INCREMENT_SIZE_MISMATCH_STRATEGY, SequenceMismatchStrategy.NONE); diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/unsupportedproperties/UnsupportedPropertiesTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/unsupportedproperties/UnsupportedPropertiesTest.java index 057d6dcf9efc6..7645a5ccc8e26 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/unsupportedproperties/UnsupportedPropertiesTest.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/unsupportedproperties/UnsupportedPropertiesTest.java @@ -114,7 +114,7 @@ public void testPropertiesPropagatedToRuntimeInit() { public void testInsertsOrdered() { var listener = new BatchCountSpyingEventListener(); - QuarkusTransaction.run(() -> { + QuarkusTransaction.requiringNew().run(() -> { em.unwrap(Session.class).addEventListeners(listener); ParentEntity parent1 = new ParentEntity(1); diff --git a/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java b/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java index cb388fe109cb1..0fdcd1e39abea 100644 --- a/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java +++ b/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveProcessor.java @@ -306,7 +306,17 @@ private static ParsedPersistenceXmlDescriptor generateReactivePersistenceUnit( desc.getProperties().setProperty(AvailableSettings.DEFAULT_NULL_ORDERING, persistenceUnitConfig.query.defaultNullOrdering.name().toLowerCase()); + desc.getProperties().setProperty(AvailableSettings.IN_CLAUSE_PARAMETER_PADDING, + String.valueOf(persistenceUnitConfig.query.inClauseParameterPadding)); + // JDBC + persistenceUnitConfig.jdbc.timezone.ifPresent( + timezone -> desc.getProperties().setProperty(AvailableSettings.JDBC_TIME_ZONE, timezone)); + + persistenceUnitConfig.jdbc.statementFetchSize.ifPresent( + fetchSize -> desc.getProperties().setProperty(AvailableSettings.STATEMENT_FETCH_SIZE, + String.valueOf(fetchSize))); + persistenceUnitConfig.jdbc.statementBatchSize.ifPresent( statementBatchSize -> desc.getProperties().setProperty(AvailableSettings.STATEMENT_BATCH_SIZE, String.valueOf(statementBatchSize))); diff --git a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/validatorfactory/ValidatorFactoryFromValidationTest.java b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/validatorfactory/ValidatorFactoryFromValidationTest.java index 0211354d0c6fe..224379c236e97 100644 --- a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/validatorfactory/ValidatorFactoryFromValidationTest.java +++ b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/validatorfactory/ValidatorFactoryFromValidationTest.java @@ -6,6 +6,7 @@ import javax.validation.Validation; import javax.validation.ValidatorFactory; +import org.hibernate.validator.HibernateValidatorFactory; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; @@ -23,9 +24,10 @@ public class ValidatorFactoryFromValidationTest { .create(JavaArchive.class)); @Test - public void testOverrideConstraintValidatorConstraint() { + public void testValidatorFactoryManuallyCreatedIsManagedByQuarkus() { ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); - assertThat(validatorFactoryFromInjection).isSameAs(validatorFactory); + // we need to unwrap here as we have a small wrapper around the manually created one + assertThat(validatorFactoryFromInjection).isSameAs(validatorFactory.unwrap(HibernateValidatorFactory.class)); } } diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcProxyBeanMetaDataClassNormalizer.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcProxyBeanMetaDataClassNormalizer.java index 674b387ad6d37..03d66360d2db5 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcProxyBeanMetaDataClassNormalizer.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcProxyBeanMetaDataClassNormalizer.java @@ -2,12 +2,13 @@ import org.hibernate.validator.metadata.BeanMetaDataClassNormalizer; +import io.quarkus.arc.ClientProxy; import io.quarkus.arc.Subclass; /** * In the case of a proxy generated by Arc, return the parent class. *
- * This can have more than one level of hierarchy, in example: + * This can have more than one level of hierarchy, for example: *
    *
  • When using @{@link io.quarkus.test.junit.mockito.InjectMock} or @{@link io.quarkus.test.junit.mockito.InjectSpy}
  • *
@@ -20,6 +21,9 @@ public Class normalize(Class beanClass) { while (Subclass.class.isAssignableFrom(targetClass)) { targetClass = targetClass.getSuperclass(); } + while (ClientProxy.class.isAssignableFrom(targetClass)) { + targetClass = targetClass.getSuperclass(); + } return targetClass; } diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/CloseAsNoopValidatorFactoryWrapper.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/CloseAsNoopValidatorFactoryWrapper.java new file mode 100644 index 0000000000000..600fb72a6dcd1 --- /dev/null +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/CloseAsNoopValidatorFactoryWrapper.java @@ -0,0 +1,93 @@ +package io.quarkus.hibernate.validator.runtime; + +import java.time.Duration; + +import javax.validation.ClockProvider; +import javax.validation.ConstraintValidatorFactory; +import javax.validation.MessageInterpolator; +import javax.validation.ParameterNameProvider; +import javax.validation.TraversableResolver; +import javax.validation.Validator; + +import org.hibernate.validator.HibernateValidatorContext; +import org.hibernate.validator.HibernateValidatorFactory; +import org.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProvider; +import org.hibernate.validator.spi.properties.GetterPropertySelectionStrategy; +import org.hibernate.validator.spi.scripting.ScriptEvaluatorFactory; + +/** + * Wrapper used to avoid closing the managed ValidatorFactory. + */ +class CloseAsNoopValidatorFactoryWrapper implements HibernateValidatorFactory { + + private final HibernateValidatorFactory validatorFactory; + + CloseAsNoopValidatorFactoryWrapper(HibernateValidatorFactory validatorFactory) { + this.validatorFactory = validatorFactory; + } + + @Override + public void close() { + // do not close the wrapped ValidatorFactory as it is managed by Quarkus + } + + @Override + public Validator getValidator() { + return validatorFactory.getValidator(); + } + + @Override + public MessageInterpolator getMessageInterpolator() { + return validatorFactory.getMessageInterpolator(); + } + + @Override + public TraversableResolver getTraversableResolver() { + return validatorFactory.getTraversableResolver(); + } + + @Override + public ConstraintValidatorFactory getConstraintValidatorFactory() { + return validatorFactory.getConstraintValidatorFactory(); + } + + @Override + public ParameterNameProvider getParameterNameProvider() { + return validatorFactory.getParameterNameProvider(); + } + + @Override + public ClockProvider getClockProvider() { + return validatorFactory.getClockProvider(); + } + + @Override + public T unwrap(Class type) { + return validatorFactory.unwrap(type); + } + + @Override + public ScriptEvaluatorFactory getScriptEvaluatorFactory() { + return validatorFactory.getScriptEvaluatorFactory(); + } + + @Override + public Duration getTemporalValidationTolerance() { + return validatorFactory.getTemporalValidationTolerance(); + } + + @Override + public GetterPropertySelectionStrategy getGetterPropertySelectionStrategy() { + return validatorFactory.getGetterPropertySelectionStrategy(); + } + + @Override + public PropertyNodeNameProvider getPropertyNodeNameProvider() { + return validatorFactory.getPropertyNodeNameProvider(); + } + + @Override + public HibernateValidatorContext usingContext() { + return validatorFactory.usingContext(); + } +} diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java index 385e16d0c5fe9..d6e6b1725e1e8 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java @@ -14,6 +14,7 @@ import javax.validation.ValidatorFactory; import javax.validation.valueextraction.ValueExtractor; +import org.hibernate.validator.HibernateValidatorFactory; import org.hibernate.validator.PredefinedScopeHibernateValidator; import org.hibernate.validator.PredefinedScopeHibernateValidatorConfiguration; import org.hibernate.validator.internal.engine.resolver.JPATraversableResolver; @@ -160,7 +161,7 @@ public void created(BeanContainer container) { } ValidatorFactory validatorFactory = configuration.buildValidatorFactory(); - ValidatorHolder.initialize(validatorFactory); + ValidatorHolder.initialize(validatorFactory.unwrap(HibernateValidatorFactory.class)); // Close the ValidatorFactory on shutdown shutdownContext.addShutdownTask(new Runnable() { diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidationSupport.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidationSupport.java index bfebed445ea47..444defaf83fd9 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidationSupport.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidationSupport.java @@ -3,6 +3,8 @@ import javax.validation.Validation; import javax.validation.ValidatorFactory; +import org.hibernate.validator.HibernateValidatorFactory; + import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; @@ -12,19 +14,18 @@ public final class ValidationSupport { private ValidationSupport() { } - @SuppressWarnings("unused") public static ValidatorFactory buildDefaultValidatorFactory() { ArcContainer container = Arc.container(); if (container == null) { return fallback(); } - InstanceHandle instance = container.instance(ValidatorFactory.class); + InstanceHandle instance = container.instance(HibernateValidatorFactory.class); if (!instance.isAvailable()) { return fallback(); } - return instance.get(); + return new CloseAsNoopValidatorFactoryWrapper(instance.get()); } // the point of having this is to support non-Quarkus tests that could be using Hibernate Validator diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorHolder.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorHolder.java index 5b4d3bb95cfc0..e039659986499 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorHolder.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorHolder.java @@ -1,20 +1,21 @@ package io.quarkus.hibernate.validator.runtime; import javax.validation.Validator; -import javax.validation.ValidatorFactory; + +import org.hibernate.validator.HibernateValidatorFactory; public class ValidatorHolder { - private static ValidatorFactory validatorFactory; + private static HibernateValidatorFactory validatorFactory; private static Validator validator; - static void initialize(ValidatorFactory validatorFactory) { + static void initialize(HibernateValidatorFactory validatorFactory) { ValidatorHolder.validatorFactory = validatorFactory; ValidatorHolder.validator = validatorFactory.getValidator(); } - static ValidatorFactory getValidatorFactory() { + static HibernateValidatorFactory getValidatorFactory() { return validatorFactory; } diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorProvider.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorProvider.java index e4040a04e21f8..06af13698972e 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorProvider.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ValidatorProvider.java @@ -4,7 +4,8 @@ import javax.inject.Named; import javax.inject.Singleton; import javax.validation.Validator; -import javax.validation.ValidatorFactory; + +import org.hibernate.validator.HibernateValidatorFactory; @Singleton public class ValidatorProvider { @@ -12,7 +13,7 @@ public class ValidatorProvider { @Produces @Named("quarkus-hibernate-validator-factory") @Singleton - public ValidatorFactory factory() { + public HibernateValidatorFactory factory() { return ValidatorHolder.getValidatorFactory(); } diff --git a/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java b/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java index b340c93f349e2..065b5e6efcc5b 100644 --- a/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java +++ b/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java @@ -54,6 +54,7 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem; +import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.MethodCreator; @@ -113,18 +114,26 @@ void unremovable(Capabilities capabilities, BuildProducer reflectiveClass, BuildProducer reflectiveHierarchyClass, BuildProducer reflectiveMethod, BuildProducer additionalBeans) { reflectiveClass.produce( - new ReflectiveClassBuildItem(true, false, "com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector", + new ReflectiveClassBuildItem(true, false, "com.fasterxml.jackson.databind.ser.std.SqlDateSerializer", "com.fasterxml.jackson.databind.ser.std.SqlTimeSerializer", "com.fasterxml.jackson.databind.deser.std.DateDeserializers$SqlDateDeserializer", "com.fasterxml.jackson.databind.deser.std.DateDeserializers$TimestampDeserializer", "com.fasterxml.jackson.annotation.SimpleObjectIdResolver")); + if (curateOutcomeBuildItem.getApplicationModel().getDependencies().stream().anyMatch( + x -> x.getGroupId().equals("com.fasterxml.jackson.module") + && x.getArtifactId().equals("jackson-module-jaxb-annotations"))) { + reflectiveClass.produce( + new ReflectiveClassBuildItem(true, false, "com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector")); + } + IndexView index = combinedIndexBuildItem.getIndex(); // TODO: @JsonDeserialize is only supported as a class annotation - we should support the others as well diff --git a/extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbProcessor.java b/extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbProcessor.java index ff38ba44ebf9b..a8b8a8a9f4e81 100644 --- a/extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbProcessor.java +++ b/extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbProcessor.java @@ -266,8 +266,6 @@ void registerClasses( BuildProducer reflectiveClass, BuildProducer resourceBundle) { addReflectiveClass(reflectiveClass, true, false, "com.sun.xml.bind.v2.ContextFactory"); - addReflectiveClass(reflectiveClass, true, false, "com.sun.xml.internal.bind.v2.ContextFactory"); - addReflectiveClass(reflectiveClass, true, false, "com.sun.xml.internal.stream.XMLInputFactoryImpl"); addReflectiveClass(reflectiveClass, true, false, "com.sun.xml.internal.stream.XMLOutputFactoryImpl"); addReflectiveClass(reflectiveClass, true, false, "com.sun.org.apache.xpath.internal.functions.FuncNot"); diff --git a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleNativeImage.java b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleNativeImage.java index c6ad1ad15d2ba..8778e677ca104 100644 --- a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleNativeImage.java +++ b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleNativeImage.java @@ -25,7 +25,14 @@ void reflection(BuildProducer reflectiveClass) { reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, driverName)); // for ldap style jdbc urls. e.g. jdbc:oracle:thin:@ldap://oid:5000/mydb1,cn=OracleContext,dc=myco,dc=com + // + // Note that all JDK provided InitialContextFactory impls from the JDK registered via module descriptors + // available at build time need to be reflectively accessible via ServiceLoader for runtime consistency. + // These are: + // com.sun.jndi.ldap.LdapCtxFactory, com.sun.jndi.dns.DnsContextFactory and com.sun.jndi.rmi.registry.RegistryContextFactory reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, "com.sun.jndi.ldap.LdapCtxFactory")); + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, "com.sun.jndi.dns.DnsContextFactory")); + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, "com.sun.jndi.rmi.registry.RegistryContextFactory")); } @BuildStep diff --git a/extensions/jdbc/jdbc-postgresql/deployment/src/main/java/io/quarkus/jdbc/postgresql/deployment/PostgreSQLJDBCReflections.java b/extensions/jdbc/jdbc-postgresql/deployment/src/main/java/io/quarkus/jdbc/postgresql/deployment/PostgreSQLJDBCReflections.java index daf43576a1516..9c5e85325d0f9 100644 --- a/extensions/jdbc/jdbc-postgresql/deployment/src/main/java/io/quarkus/jdbc/postgresql/deployment/PostgreSQLJDBCReflections.java +++ b/extensions/jdbc/jdbc-postgresql/deployment/src/main/java/io/quarkus/jdbc/postgresql/deployment/PostgreSQLJDBCReflections.java @@ -19,6 +19,9 @@ void build(BuildProducer reflectiveClass) { //We register it for the sake of other users. final String driverName = "org.postgresql.Driver"; reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, driverName)); + + // Needed when quarkus.datasource.jdbc.transactions=xa for the setting of the username and password + reflectiveClass.produce(new ReflectiveClassBuildItem(false, true, false, "org.postgresql.ds.common.BaseDataSource")); } } diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/DevServicesKafkaProcessor.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/DevServicesKafkaProcessor.java index 03b704e56fe5e..88e167dc08b5d 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/DevServicesKafkaProcessor.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/DevServicesKafkaProcessor.java @@ -225,39 +225,53 @@ private RunningDevService startKafka(DockerStatusBuildItem dockerStatusBuildItem // Starting the broker final Supplier defaultKafkaBrokerSupplier = () -> { - if (config.imageName.contains("strimzi")) { - StrimziKafkaContainer container = new StrimziKafkaContainer(config.imageName) - .withBrokerId(1) - .withKraft() - .waitForRunning(); - ConfigureUtil.configureSharedNetwork(container, "kafka"); - if (config.serviceName != null) { - container.withLabel(DevServicesKafkaProcessor.DEV_SERVICE_LABEL, config.serviceName); - } - if (config.fixedExposedPort != 0) { - container.withPort(config.fixedExposedPort); - } - timeout.ifPresent(container::withStartupTimeout); - - container.start(); - return new RunningDevService(Feature.KAFKA_CLIENT.getName(), - container.getContainerId(), - container::close, - KAFKA_BOOTSTRAP_SERVERS, container.getBootstrapServers()); - } else { - RedPandaKafkaContainer container = new RedPandaKafkaContainer( - DockerImageName.parse(config.imageName).asCompatibleSubstituteFor("vectorized/redpanda"), - config.fixedExposedPort, - launchMode.getLaunchMode() == LaunchMode.DEVELOPMENT ? config.serviceName : null, - useSharedNetwork, config.redpanda); - timeout.ifPresent(container::withStartupTimeout); - container.start(); - - return new RunningDevService(Feature.KAFKA_CLIENT.getName(), - container.getContainerId(), - container::close, - KAFKA_BOOTSTRAP_SERVERS, container.getBootstrapServers()); + switch (config.provider) { + case REDPANDA: + RedPandaKafkaContainer redpanda = new RedPandaKafkaContainer( + DockerImageName.parse(config.imageName).asCompatibleSubstituteFor("vectorized/redpanda"), + config.fixedExposedPort, + launchMode.getLaunchMode() == LaunchMode.DEVELOPMENT ? config.serviceName : null, + useSharedNetwork, config.redpanda); + timeout.ifPresent(redpanda::withStartupTimeout); + redpanda.start(); + + return new RunningDevService(Feature.KAFKA_CLIENT.getName(), + redpanda.getContainerId(), + redpanda::close, + KAFKA_BOOTSTRAP_SERVERS, redpanda.getBootstrapServers()); + case STRIMZI: + StrimziKafkaContainer strimzi = new StrimziKafkaContainer(config.imageName) + .withBrokerId(1) + .withKraft() + .waitForRunning(); + ConfigureUtil.configureSharedNetwork(strimzi, "kafka"); + if (config.serviceName != null) { + strimzi.withLabel(DevServicesKafkaProcessor.DEV_SERVICE_LABEL, config.serviceName); + } + if (config.fixedExposedPort != 0) { + strimzi.withPort(config.fixedExposedPort); + } + timeout.ifPresent(strimzi::withStartupTimeout); + + strimzi.start(); + return new RunningDevService(Feature.KAFKA_CLIENT.getName(), + strimzi.getContainerId(), + strimzi::close, + KAFKA_BOOTSTRAP_SERVERS, strimzi.getBootstrapServers()); + case KAFKA_NATIVE: + KafkaNativeContainer kafkaNative = new KafkaNativeContainer(DockerImageName.parse(config.imageName), + config.fixedExposedPort, + launchMode.getLaunchMode() == LaunchMode.DEVELOPMENT ? config.serviceName : null, + useSharedNetwork); + timeout.ifPresent(kafkaNative::withStartupTimeout); + kafkaNative.start(); + + return new RunningDevService(Feature.KAFKA_CLIENT.getName(), + kafkaNative.getContainerId(), + kafkaNative::close, + KAFKA_BOOTSTRAP_SERVERS, kafkaNative.getBootstrapServers()); } + return null; }; return maybeContainerAddress @@ -300,11 +314,15 @@ private static final class KafkaDevServiceCfg { private final String serviceName; private final Map topicPartitions; private final Duration topicPartitionsTimeout; + + private final KafkaDevServicesBuildTimeConfig.Provider provider; + private final RedPandaBuildTimeConfig redpanda; public KafkaDevServiceCfg(KafkaDevServicesBuildTimeConfig config) { this.devServicesEnabled = config.enabled.orElse(true); - this.imageName = config.imageName; + this.provider = config.provider; + this.imageName = config.imageName.orElseGet(provider::getDefaultImageName); this.fixedExposedPort = config.port.orElse(0); this.shared = config.shared; this.serviceName = config.serviceName; @@ -323,13 +341,15 @@ public boolean equals(Object o) { return false; } KafkaDevServiceCfg that = (KafkaDevServiceCfg) o; - return devServicesEnabled == that.devServicesEnabled && Objects.equals(imageName, that.imageName) + return devServicesEnabled == that.devServicesEnabled + && Objects.equals(provider, that.provider) + && Objects.equals(imageName, that.imageName) && Objects.equals(fixedExposedPort, that.fixedExposedPort); } @Override public int hashCode() { - return Objects.hash(devServicesEnabled, imageName, fixedExposedPort); + return Objects.hash(devServicesEnabled, provider, imageName, fixedExposedPort); } } diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaDevServicesBuildTimeConfig.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaDevServicesBuildTimeConfig.java index a0501b7093ac1..36fc407e05d26 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaDevServicesBuildTimeConfig.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaDevServicesBuildTimeConfig.java @@ -28,15 +28,9 @@ public class KafkaDevServicesBuildTimeConfig { public Optional port; /** - * The Kafka container image to use. - *

- * Only Redpanda and Strimzi images are supported. - * Default image is Redpanda. + * Kafka dev service container type. *

- * Note that Strimzi images are launched in Kraft mode. - * In order to use a Strimzi image you need to set a compatible image name such as - * {@code quay.io/strimzi-test-container/test-container:0.100.0-kafka-3.1.0} or - * {@code quay.io/strimzi/kafka:0.27.1-kafka-3.0.0} + * Redpanda, Strimzi and kafka-native container providers are supported. Default is redpanda. *

* For Redpanda: * See https://vectorized.io/docs/quick-start-docker/ and https://hub.docker.com/r/vectorized/redpanda @@ -44,9 +38,37 @@ public class KafkaDevServicesBuildTimeConfig { * For Strimzi: * See https://github.com/strimzi/test-container and https://quay.io/repository/strimzi-test-container/test-container *

+ * For Kafka Native: + * See https://github.com/ozangunalp/kafka-native and https://quay.io/repository/ogunalp/kafka-native + *

+ * Note that Strimzi and Kafka Native images are launched in Kraft mode. */ - @ConfigItem(defaultValue = "docker.io/vectorized/redpanda:v22.1.7") - public String imageName; + @ConfigItem(defaultValue = "redpanda") + public Provider provider = Provider.REDPANDA; + + public enum Provider { + REDPANDA("docker.io/vectorized/redpanda:v22.3.4"), + STRIMZI("quay.io/strimzi-test-container/test-container:latest-kafka-3.2.1"), + KAFKA_NATIVE("quay.io/ogunalp/kafka-native:latest"); + + private final String defaultImageName; + + Provider(String imageName) { + this.defaultImageName = imageName; + } + + public String getDefaultImageName() { + return defaultImageName; + } + } + + /** + * The Kafka container image to use. + *

+ * Dependent on the provider. + */ + @ConfigItem + public Optional imageName; /** * Indicates if the Kafka broker managed by Quarkus Dev Services is shared. diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaNativeContainer.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaNativeContainer.java new file mode 100644 index 0000000000000..b6fff5fd04a9c --- /dev/null +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaNativeContainer.java @@ -0,0 +1,99 @@ +package io.quarkus.kafka.client.deployment; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.utility.DockerImageName; + +import com.github.dockerjava.api.command.InspectContainerResponse; + +import io.quarkus.devservices.common.ConfigureUtil; + +public class KafkaNativeContainer extends GenericContainer { + + private static final String STARTER_SCRIPT = "/work/run.sh"; + + private final Integer fixedExposedPort; + private final boolean useSharedNetwork; + + private String additionalArgs = null; + private int exposedPort = -1; + + private String hostName = null; + + public KafkaNativeContainer(DockerImageName dockerImageName, int fixedExposedPort, String serviceName, + boolean useSharedNetwork) { + super(dockerImageName); + this.fixedExposedPort = fixedExposedPort; + this.useSharedNetwork = useSharedNetwork; + if (serviceName != null) { + withLabel(DevServicesKafkaProcessor.DEV_SERVICE_LABEL, serviceName); + } + String cmd = String.format("while [ ! -f %s ]; do sleep 0.1; done; sleep 0.1; %s", STARTER_SCRIPT, STARTER_SCRIPT); + withCommand("sh", "-c", cmd); + waitingFor(Wait.forLogMessage(".*Kafka broker started.*", 1)); + } + + @Override + protected void containerIsStarting(InspectContainerResponse containerInfo, boolean reused) { + super.containerIsStarting(containerInfo, reused); + // Set exposed port + this.exposedPort = getMappedPort(DevServicesKafkaProcessor.KAFKA_PORT); + // follow output + // Start and configure the advertised address + String cmd = "#!/bin/bash\n"; + cmd += "/work/kafka"; + cmd += " -Dkafka.advertised.listeners=" + getBootstrapServers(); + if (useSharedNetwork) { + cmd += " -Dkafka.listeners=BROKER://:9093,PLAINTEXT://:9092,CONTROLLER://:9094"; + cmd += " -Dkafka.interbroker.listener.name=BROKER"; + cmd += " -Dkafka.controller.listener.names=CONTROLLER"; + cmd += " -Dkafka.listener.security.protocol.map=BROKER:PLAINTEXT,CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT"; + cmd += " -Dkafka.early.start.listeners=BROKER,CONTROLLER,PLAINTEXT"; + } + if (additionalArgs != null) { + cmd += " " + additionalArgs; + } + + //noinspection OctalInteger + copyFileToContainer( + Transferable.of(cmd.getBytes(StandardCharsets.UTF_8), 0777), + STARTER_SCRIPT); + } + + private String getKafkaAdvertisedListeners() { + List addresses = new ArrayList<>(); + if (useSharedNetwork) { + addresses.add(String.format("BROKER://%s:9093", hostName)); + } + // See https://github.com/quarkusio/quarkus/issues/21819 + // Kafka is always exposed to the Docker host network + addresses.add(String.format("PLAINTEXT://%s:%d", getHost(), getExposedKafkaPort())); + return String.join(",", addresses); + } + + public int getExposedKafkaPort() { + return exposedPort; + } + + @Override + protected void configure() { + super.configure(); + + addExposedPort(DevServicesKafkaProcessor.KAFKA_PORT); + hostName = ConfigureUtil.configureSharedNetwork(this, "kafka"); + + if (fixedExposedPort != null) { + addFixedExposedPort(fixedExposedPort, DevServicesKafkaProcessor.KAFKA_PORT); + } + } + + public String getBootstrapServers() { + return getKafkaAdvertisedListeners(); + } + +} diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java index ecabb78aeb726..b246f82e267c5 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java @@ -84,6 +84,7 @@ import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; import io.quarkus.deployment.pkg.NativeConfig; +import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.dev.spi.DevModeType; import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem; import io.quarkus.devconsole.spi.DevConsoleWebjarBuildItem; @@ -215,7 +216,7 @@ void relaxSaslElytron(BuildProducer config @BuildStep public void build( - KafkaBuildTimeConfig config, + KafkaBuildTimeConfig config, CurateOutcomeBuildItem curateOutcomeBuildItem, CombinedIndexBuildItem indexBuildItem, BuildProducer reflectiveClass, BuildProducer serviceProviders, BuildProducer proxies, @@ -273,13 +274,9 @@ public void build( reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, RoundRobinAssignor.class.getName())); reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, StickyAssignor.class.getName())); - // classes needed to perform reflection on DirectByteBuffer - only really needed for Java 8 - reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, "java.nio.DirectByteBuffer")); - reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, "sun.misc.Cleaner")); - handleAvro(reflectiveClass, proxies, serviceProviders, sslNativeSupport, capabilities); handleOpenTracing(reflectiveClass, capabilities); - handleStrimziOAuth(reflectiveClass); + handleStrimziOAuth(curateOutcomeBuildItem, reflectiveClass); if (config.snappyEnabled) { handleSnappy(reflectiveClass, nativeLibs, nativeConfig); } @@ -336,7 +333,8 @@ private void handleOpenTracing(BuildProducer reflectiv "io.opentracing.contrib.kafka.TracingConsumerInterceptor")); } - private void handleStrimziOAuth(BuildProducer reflectiveClass) { + private void handleStrimziOAuth(CurateOutcomeBuildItem curateOutcomeBuildItem, + BuildProducer reflectiveClass) { if (!QuarkusClassLoader.isClassPresentAtRuntime("io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler")) { return; } @@ -344,17 +342,20 @@ private void handleStrimziOAuth(BuildProducer reflecti reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, true, "io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler")); - reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, true, - "org.keycloak.jose.jws.JWSHeader", - "org.keycloak.representations.AccessToken", - "org.keycloak.representations.AccessToken$Access", - "org.keycloak.representations.AccessTokenResponse", - "org.keycloak.representations.IDToken", - "org.keycloak.representations.JsonWebToken", - "org.keycloak.jose.jwk.JSONWebKeySet", - "org.keycloak.jose.jwk.JWK", - "org.keycloak.json.StringOrArrayDeserializer", - "org.keycloak.json.StringListMapDeserializer")); + if (curateOutcomeBuildItem.getApplicationModel().getDependencies().stream().anyMatch( + x -> x.getGroupId().equals("org.keycloak") && x.getArtifactId().equals("keycloak-core"))) { + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, true, + "org.keycloak.jose.jws.JWSHeader", + "org.keycloak.representations.AccessToken", + "org.keycloak.representations.AccessToken$Access", + "org.keycloak.representations.AccessTokenResponse", + "org.keycloak.representations.IDToken", + "org.keycloak.representations.JsonWebToken", + "org.keycloak.jose.jwk.JSONWebKeySet", + "org.keycloak.jose.jwk.JWK", + "org.keycloak.json.StringOrArrayDeserializer", + "org.keycloak.json.StringListMapDeserializer")); + } } private void handleAvro(BuildProducer reflectiveClass, @@ -433,11 +434,25 @@ public void withSasl(CombinedIndexBuildItem index, for (ClassInfo loginModule : index.getIndex().getAllKnownImplementors(LOGIN_MODULE)) { reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, loginModule.name().toString())); } + // Kafka oauth login internally iterates over all ServiceLoader available LoginModule's + registerJDKLoginModules(reflectiveClass); for (ClassInfo authenticateCallbackHandler : index.getIndex().getAllKnownImplementors(AUTHENTICATE_CALLBACK_HANDLER)) { reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, authenticateCallbackHandler.name().toString())); } } + private void registerJDKLoginModules(BuildProducer reflectiveClass) { + // jdk.security.auth module provided LoginModule's + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, "com.sun.security.auth.module.Krb5LoginModule")); + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, "com.sun.security.auth.module.UnixLoginModule")); + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, "com.sun.security.auth.module.JndiLoginModule")); + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, "com.sun.security.auth.module.KeyStoreLoginModule")); + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, "com.sun.security.auth.module.LdapLoginModule")); + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, "com.sun.security.auth.module.NTLoginModule")); + // java.management module provided LoginModule's + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, "com.sun.jmx.remote.security.FileLoginModule")); + } + private static void collectImplementors(Set set, CombinedIndexBuildItem indexBuildItem, Class cls) { collectClassNames(set, indexBuildItem.getIndex().getAllKnownImplementors(DotName.createSimple(cls.getName()))); } diff --git a/extensions/kafka-client/deployment/src/main/resources/dev-static/js/config.js b/extensions/kafka-client/deployment/src/main/resources/dev-static/js/config.js index 130b130828fbe..913a6d4f8fc73 100644 --- a/extensions/kafka-client/deployment/src/main/resources/dev-static/js/config.js +++ b/extensions/kafka-client/deployment/src/main/resources/dev-static/js/config.js @@ -1,2 +1,5 @@ -export const api = '/q/dev/io.quarkus.quarkus-kafka-client/kafka-admin'; +export const api = () => { + let path = window.location.pathname + return path.replace('/kafka-dev-ui', '/kafka-admin') +} export const ui = 'kafka-ui'; \ No newline at end of file diff --git a/extensions/kafka-client/deployment/src/main/resources/dev-static/js/web/web.js b/extensions/kafka-client/deployment/src/main/resources/dev-static/js/web/web.js index 6ba79b5c19720..c981bf9e24a86 100644 --- a/extensions/kafka-client/deployment/src/main/resources/dev-static/js/web/web.js +++ b/extensions/kafka-client/deployment/src/main/resources/dev-static/js/web/web.js @@ -2,7 +2,7 @@ import {api} from "../config.js" export function doPost(data, successCallback, errorCallback) { $.ajax({ - url: api, + url: api(), type: 'POST', data: JSON.stringify(data), contentType: "application/json; charset=utf-8", diff --git a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiHandler.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiHandler.java index 8b7d916de765b..06aea79b5b9f5 100644 --- a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiHandler.java +++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiHandler.java @@ -24,7 +24,8 @@ public void handlePost(RoutingContext event) { endResponse(event, BAD_REQUEST, "Request body is null"); return; } - var body = event.body().asJsonObject(); + var webUtils = kafkaWebUiUtils(); + var body = webUtils.fromJson(event.body().buffer()); if (body == null) { endResponse(event, BAD_REQUEST, "Request JSON body is null"); return; @@ -34,7 +35,6 @@ public void handlePost(RoutingContext event) { var message = "OK"; var error = ""; - var webUtils = kafkaWebUiUtils(); var adminClient = kafkaAdminClient(); boolean res = false; @@ -50,7 +50,7 @@ public void handlePost(RoutingContext event) { res = true; break; case "createTopic": - var topicCreateRq = event.body().asPojo(KafkaCreateTopicRequest.class); + var topicCreateRq = webUtils.fromJson(event.body().buffer(), KafkaCreateTopicRequest.class); res = adminClient.createTopic(topicCreateRq); message = webUtils.toJson(webUtils.getTopics()); break; @@ -64,17 +64,17 @@ public void handlePost(RoutingContext event) { res = true; break; case "topicMessages": - var msgRequest = event.body().asPojo(KafkaMessagesRequest.class); + var msgRequest = webUtils.fromJson(event.body().buffer(), KafkaMessagesRequest.class); message = webUtils.toJson(webUtils.getMessages(msgRequest)); res = true; break; case "getOffset": - var request = event.body().asPojo(KafkaOffsetRequest.class); + var request = webUtils.fromJson(event.body().buffer(), KafkaOffsetRequest.class); message = webUtils.toJson(webUtils.getOffset(request)); res = true; break; case "createMessage": - var rq = event.body().asPojo(KafkaMessageCreateRequest.class); + var rq = webUtils.fromJson(event.body().buffer(), KafkaMessageCreateRequest.class); webUtils.createMessage(rq); message = "{}"; res = true; diff --git a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiUtils.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiUtils.java index 862fdcfeb2d2d..6cca91a20f37e 100644 --- a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiUtils.java +++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/ui/KafkaUiUtils.java @@ -2,7 +2,14 @@ import static io.quarkus.kafka.client.runtime.ui.util.ConsumerFactory.createConsumer; -import java.util.*; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -16,9 +23,14 @@ import org.apache.kafka.common.Node; import org.apache.kafka.common.TopicPartition; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import io.netty.buffer.ByteBufInputStream; import io.quarkus.kafka.client.runtime.KafkaAdminClient; import io.quarkus.kafka.client.runtime.ui.model.Order; import io.quarkus.kafka.client.runtime.ui.model.request.KafkaMessageCreateRequest; @@ -26,6 +38,8 @@ import io.quarkus.kafka.client.runtime.ui.model.request.KafkaOffsetRequest; import io.quarkus.kafka.client.runtime.ui.model.response.*; import io.smallrye.common.annotation.Identifier; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.JsonObject; @Singleton public class KafkaUiUtils { @@ -37,12 +51,15 @@ public class KafkaUiUtils { private final Map config; - public KafkaUiUtils(KafkaAdminClient kafkaAdminClient, KafkaTopicClient kafkaTopicClient, ObjectMapper objectMapper, + public KafkaUiUtils(KafkaAdminClient kafkaAdminClient, KafkaTopicClient kafkaTopicClient, @Identifier("default-kafka-broker") Map config) { this.kafkaAdminClient = kafkaAdminClient; this.kafkaTopicClient = kafkaTopicClient; - this.objectMapper = objectMapper; this.config = config; + this.objectMapper = JsonMapper.builder() + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); } public KafkaInfo getKafkaInfo() throws ExecutionException, InterruptedException { @@ -218,9 +235,22 @@ public String toJson(Object o) { try { res = objectMapper.writeValueAsString(o); } catch (JsonProcessingException ex) { - //FIXME: res = ""; } return res; } + + public JsonObject fromJson(Buffer buffer) { + return new JsonObject(fromJson(buffer, Map.class)); + } + + public T fromJson(Buffer buffer, Class type) { + try { + JsonParser parser = objectMapper.createParser((InputStream) new ByteBufInputStream(buffer.getByteBuf())); + return objectMapper.readValue(parser, type); + } catch (IOException e) { + return null; + } + } + } diff --git a/extensions/keycloak-admin-client-reactive/runtime/pom.xml b/extensions/keycloak-admin-client-reactive/runtime/pom.xml index dc194217932f8..8660b35d01ab7 100644 --- a/extensions/keycloak-admin-client-reactive/runtime/pom.xml +++ b/extensions/keycloak-admin-client-reactive/runtime/pom.xml @@ -11,7 +11,7 @@ quarkus-keycloak-admin-client-reactive Quarkus - Keycloak Admin Client - Reactive - Runtime - Administer a Keycloak Instance + Administer a Keycloak Instance using Reactive diff --git a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java index 5c8c9d4530585..61d7605485442 100644 --- a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java +++ b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java @@ -30,10 +30,11 @@ public Supplier createAdminClient() { final KeycloakAdminClientConfig config = keycloakAdminClientConfigRuntimeValue.getValue(); validate(config); if (config.serverUrl.isEmpty()) { - return new Supplier() { + return new Supplier<>() { @Override public Keycloak get() { - return null; + throw new IllegalStateException( + "'quarkus.keycloak.admin-client.server-url' must be set in order to use the Keycloak admin client as a CDI bean"); } }; } diff --git a/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java b/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java index 5ffe9c6f0df6d..ece1316308464 100644 --- a/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java +++ b/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java @@ -31,10 +31,11 @@ public Supplier createAdminClient() { final KeycloakAdminClientConfig config = keycloakAdminClientConfigRuntimeValue.getValue(); validate(config); if (config.serverUrl.isEmpty()) { - return new Supplier() { + return new Supplier<>() { @Override public Keycloak get() { - return null; + throw new IllegalStateException( + "'quarkus.keycloak.admin-client.server-url' must be set in order to use the Keycloak admin client as a CDI bean"); } }; } diff --git a/extensions/kotlin/runtime/pom.xml b/extensions/kotlin/runtime/pom.xml index b83193e94f16e..a92dd5751b376 100644 --- a/extensions/kotlin/runtime/pom.xml +++ b/extensions/kotlin/runtime/pom.xml @@ -19,20 +19,6 @@ io.quarkus quarkus-extension-maven-plugin - - org.jetbrains.kotlin:kotlin-stdlib-jdk8 - org.jetbrains.kotlin:kotlin-stdlib-jdk7 - org.jetbrains.kotlin:kotlin-reflect - org.jetbrains.kotlin:kotlin-stdlib - org.jetbrains.kotlin:kotlin-stdlib-common - - - org.jetbrains.kotlin:kotlin-stdlib-jdk8 - org.jetbrains.kotlin:kotlin-stdlib-jdk7 - org.jetbrains.kotlin:kotlin-reflect - org.jetbrains.kotlin:kotlin-stdlib - org.jetbrains.kotlin:kotlin-stdlib-common - diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerRecorder.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerRecorder.java index 4ed9d324b4dc1..aaab2f94a3283 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerRecorder.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerRecorder.java @@ -14,7 +14,6 @@ import javax.enterprise.inject.spi.Bean; import javax.enterprise.inject.spi.BeanManager; -import org.graalvm.nativeimage.ImageInfo; import org.jboss.logging.Logger; import io.micrometer.core.instrument.Meter; @@ -37,6 +36,7 @@ import io.quarkus.micrometer.runtime.config.runtime.HttpClientConfig; import io.quarkus.micrometer.runtime.config.runtime.HttpServerConfig; import io.quarkus.micrometer.runtime.config.runtime.VertxConfig; +import io.quarkus.runtime.ImageMode; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.ShutdownContext; @@ -113,7 +113,7 @@ public void configureRegistries(MicrometerConfig config, new JvmMemoryMetrics().bindTo(Metrics.globalRegistry); new JvmThreadMetrics().bindTo(Metrics.globalRegistry); new JVMInfoBinder().bindTo(Metrics.globalRegistry); - if (!ImageInfo.inImageCode()) { + if (ImageMode.current() == ImageMode.JVM) { new JvmGcMetrics().bindTo(Metrics.globalRegistry); } } diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/redis/RedisMetricsBean.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/redis/RedisMetricsBean.java new file mode 100644 index 0000000000000..e01edccb949a1 --- /dev/null +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/redis/RedisMetricsBean.java @@ -0,0 +1,73 @@ +package io.quarkus.micrometer.runtime.binder.redis; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Typed; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import io.quarkus.redis.runtime.client.ObservableRedisMetrics; + +@ApplicationScoped +@Typed(ObservableRedisMetrics.class) +public class RedisMetricsBean implements ObservableRedisMetrics { + + final MeterRegistry registry = Metrics.globalRegistry; + + final Map reportedMetrics = new ConcurrentHashMap<>(); + + @Override + public void report(String name, long durationInNs, boolean succeeded) { + reportedMetrics.computeIfAbsent(name, n -> new RedisMetrics(registry, n)) + .report(name, durationInNs, succeeded); + } + + private class RedisMetrics implements ObservableRedisMetrics { + private final Tags tags; + private final Counter operationCounter; + private final Counter successCounter; + + private final Counter failureCounter; + private final Timer timer; + private String name; + + private RedisMetrics(MeterRegistry registry, String name) { + this.name = name; + this.tags = Tags.of(Tag.of("client-name", name)); + this.operationCounter = Counter.builder("redis.commands.count") + .description("The number of operations (commands or batches) executed") + .tags(tags) + .register(registry); + this.successCounter = Counter.builder("redis.commands.success") + .description("The number of operations (commands or batches) that have been executed successfully") + .tags(tags) + .register(registry); + this.failureCounter = Counter.builder("redis.commands.failure") + .description("The number of operations (commands or batches) that have been failed") + .tags(tags) + .register(registry); + this.timer = Timer.builder("redis.commands.duration") + .description("The duration of the operations (commands of batches") + .tags(tags) + .register(registry); + } + + @Override + public void report(String name, long durationInNs, boolean succeeded) { + operationCounter.increment(); + if (succeeded) { + this.successCounter.increment(); + } else { + this.failureCounter.increment(); + } + timer.record(durationInNs, TimeUnit.NANOSECONDS); + } + } +} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderAdapter.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderAdapter.java index 7e9712c73fdb3..d7fb22080a037 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderAdapter.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderAdapter.java @@ -10,6 +10,7 @@ import io.vertx.core.net.SocketAddress; import io.vertx.core.spi.VertxMetricsFactory; import io.vertx.core.spi.metrics.HttpServerMetrics; +import io.vertx.core.spi.metrics.PoolMetrics; import io.vertx.core.spi.metrics.VertxMetrics; public class VertxMeterBinderAdapter extends MetricsOptions implements VertxMetricsFactory, VertxMetrics { @@ -55,4 +56,10 @@ public MetricsOptions newOptions() { } return null; } + + @Override + public PoolMetrics createPoolMetrics(String poolType, String poolName, int maxPoolSize) { + return new VertxPoolMetrics(Metrics.globalRegistry, poolType, poolName, maxPoolSize); + } + } diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxPoolMetrics.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxPoolMetrics.java new file mode 100644 index 0000000000000..8b3b71535046f --- /dev/null +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxPoolMetrics.java @@ -0,0 +1,152 @@ +package io.quarkus.micrometer.runtime.binder.vertx; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Supplier; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import io.vertx.core.spi.metrics.PoolMetrics; + +/** + * Adaptation of the Vert.x Pool Metrics implementation for Quarkus Micrometer. + */ +public class VertxPoolMetrics implements PoolMetrics { + + private final String poolType; + private final int maxPoolSize; + + private final Timer usage; + private final AtomicReference ratio = new AtomicReference<>(); + private final LongAdder current = new LongAdder(); + private final LongAdder queue = new LongAdder(); + private final Counter completed; + private final Timer queueDelay; + + VertxPoolMetrics(MeterRegistry registry, String poolType, String poolName, int maxPoolSize) { + this.poolType = poolType; + this.maxPoolSize = maxPoolSize; + + Tags tags = Tags.of(Tag.of("pool.type", poolType), Tag.of("pool.name", poolName)); + + queueDelay = Timer.builder(name("queue.delay")) + .description("Time spent in the waiting queue before being processed") + .tags(tags) + .register(registry); + + usage = Timer.builder(name("usage")) + .description("Time spent using resources from the pool") + .tags(tags) + .register(registry); + + Gauge.builder(name("queue.size"), new Supplier() { + @Override + public Number get() { + return queue.doubleValue(); + } + }) + .description("Number of pending elements in the waiting queue") + .tags(tags) + .strongReference(true) + .register(registry); + + Gauge.builder(name("active"), new Supplier() { + @Override + public Number get() { + return current.doubleValue(); + } + }) + .description("The number of resources from the pool currently used") + .tags(tags) + .strongReference(true) + .register(registry); + + if (maxPoolSize > 0) { + Gauge.builder(name("idle"), new Supplier() { + @Override + public Number get() { + return maxPoolSize - current.doubleValue(); + } + }) + .description("The number of resources from the pool currently used") + .tags(tags) + .strongReference(true) + .register(registry); + + Gauge.builder(name("ratio"), ratio::get) + .description("Pool usage ratio") + .tags(tags) + .strongReference(true) + .register(registry); + } + + completed = Counter.builder(name("completed")) + .description("Number of times resources from the pool have been acquired") + .tags(tags) + .register(registry); + + } + + private String name(String suffix) { + return poolType + ".pool." + suffix; + } + + @Override + public EventTiming submitted() { + queue.increment(); + return new EventTiming(queueDelay); + } + + @Override + public void rejected(EventTiming submitted) { + queue.decrement(); + submitted.end(); + } + + @Override + public EventTiming begin(EventTiming submitted) { + queue.decrement(); + submitted.end(); + current.increment(); + computeRatio(current.longValue()); + return new EventTiming(usage); + } + + @Override + public void end(EventTiming timer, boolean succeeded) { + current.decrement(); + computeRatio(current.longValue()); + timer.end(); + + completed.increment(); + } + + @Override + public void close() { + } + + private void computeRatio(long inUse) { + if (maxPoolSize > 0) { + ratio.set((double) inUse / maxPoolSize); + } + } + + public static class EventTiming { + private final long nanoStart; + private final Timer timer; + + private EventTiming(Timer timer) { + this.timer = timer; + this.nanoStart = System.nanoTime(); + } + + public void end() { + timer.record(System.nanoTime() - nanoStart, TimeUnit.NANOSECONDS); + } + } +} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java index 666d8eea8ec08..e1fac0e2d4f69 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java @@ -106,6 +106,8 @@ public static class BinderConfig { public KafkaConfigGroup kafka; + public RedisConfigGroup redis; + public GrpcServerConfigGroup grpcServer; public GrpcClientConfigGroup grpcClient; diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/RedisConfigGroup.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/RedisConfigGroup.java new file mode 100644 index 0000000000000..b79ee7b9c2350 --- /dev/null +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/RedisConfigGroup.java @@ -0,0 +1,35 @@ +package io.quarkus.micrometer.runtime.config; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Build / static runtime config for Redis metrics + */ +@ConfigGroup +public class RedisConfigGroup implements MicrometerConfig.CapabilityEnabled { + /** + * Redis client metrics support. + *

+ * Support for Redis metrics will be enabled if Micrometer support is enabled, + * the Quarkus Redis client extension is on the classpath + * and either this value is true, or this value is unset and + * {@code quarkus.micrometer.binder-enabled-default} is true. + */ + @ConfigItem + public Optional enabled; + + @Override + public Optional getEnabled() { + return enabled; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + + "{enabled=" + enabled + + '}'; + } +} diff --git a/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/quarkus/TransactionRunnerTest.java b/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/quarkus/TransactionRunnerTest.java new file mode 100644 index 0000000000000..39311a6fd6901 --- /dev/null +++ b/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/quarkus/TransactionRunnerTest.java @@ -0,0 +1,285 @@ +package io.quarkus.narayana.quarkus; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import javax.enterprise.context.control.ActivateRequestContext; +import javax.inject.Inject; +import javax.transaction.RollbackException; +import javax.transaction.Status; +import javax.transaction.Synchronization; +import javax.transaction.SystemException; +import javax.transaction.TransactionManager; + +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.narayana.jta.QuarkusTransaction; +import io.quarkus.narayana.jta.QuarkusTransactionException; +import io.quarkus.narayana.jta.TransactionExceptionResult; +import io.quarkus.test.QuarkusUnitTest; + +public class TransactionRunnerTest { + + @Inject + TransactionManager transactionManager; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Test + public void commit() { + var sync = new TestSync(); + QuarkusTransaction.requiringNew().run(() -> register(sync)); + assertEquals(Status.STATUS_COMMITTED, sync.completionStatus); + + assertEquals(Status.STATUS_COMMITTED, + QuarkusTransaction.requiringNew().call(this::register).completionStatus); + } + + @Test + public void rollback() { + var sync1 = new TestSync(); + assertThrows(QuarkusTransactionException.class, + () -> QuarkusTransaction.requiringNew().run(() -> { + register(sync1); + QuarkusTransaction.rollback(); + })); + assertEquals(Status.STATUS_ROLLEDBACK, sync1.completionStatus); + + var sync2 = new TestSync(); + assertThrows(QuarkusTransactionException.class, + () -> QuarkusTransaction.requiringNew().call(() -> { + register(sync2); + QuarkusTransaction.rollback(); + return null; + })); + assertEquals(Status.STATUS_ROLLEDBACK, sync2.completionStatus); + } + + @Test + public void rollbackOnly() { + var sync1 = new TestSync(); + assertThrows(QuarkusTransactionException.class, + () -> QuarkusTransaction.requiringNew().run(() -> { + register(sync1); + QuarkusTransaction.setRollbackOnly(); + })); + assertEquals(Status.STATUS_ROLLEDBACK, sync1.completionStatus); + + var sync2 = new TestSync(); + assertThrows(QuarkusTransactionException.class, + () -> QuarkusTransaction.requiringNew().call(() -> { + register(sync2); + QuarkusTransaction.setRollbackOnly(); + return null; + })); + assertEquals(Status.STATUS_ROLLEDBACK, sync2.completionStatus); + } + + @Test + public void timeout() { + var sync1 = new TestSync(); + assertThrows(QuarkusTransactionException.class, () -> QuarkusTransaction.requiringNew() + .timeout(1) + .run(() -> { + register(sync1); + try { + Thread.sleep(1200); + } catch (InterruptedException e) { + fail("Interrupted unexpectedly"); + } + })); + assertEquals(Status.STATUS_ROLLEDBACK, sync1.completionStatus); + + var sync2 = new TestSync(); + assertThrows(QuarkusTransactionException.class, () -> QuarkusTransaction.requiringNew() + .timeout(1) + .call(() -> { + register(sync2); + try { + Thread.sleep(1200); + } catch (InterruptedException e) { + fail("Interrupted unexpectedly"); + } + return null; + })); + assertEquals(Status.STATUS_ROLLEDBACK, sync2.completionStatus); + } + + @Test + public void exception() { + var sync1 = new TestSync(); + assertThrows(MyRuntimeException.class, () -> QuarkusTransaction.requiringNew() + .run(() -> { + register(sync1); + throw new MyRuntimeException(); + })); + assertEquals(Status.STATUS_ROLLEDBACK, sync1.completionStatus); + + var sync2 = new TestSync(); + assertThrows(MyRuntimeException.class, () -> QuarkusTransaction.requiringNew() + .call(() -> { + register(sync2); + throw new MyRuntimeException(); + })); + assertEquals(Status.STATUS_ROLLEDBACK, sync2.completionStatus); + + var sync3 = new TestSync(); + assertThrows(QuarkusTransactionException.class, () -> QuarkusTransaction.requiringNew() + .call(() -> { + register(sync3); + throw new MyCheckedException(); + })); + assertEquals(Status.STATUS_ROLLEDBACK, sync3.completionStatus); + } + + @Test + public void exceptionHandler() { + var sync1 = new TestSync(); + assertThrows(MyRuntimeException.class, () -> QuarkusTransaction.requiringNew() + .exceptionHandler((e) -> TransactionExceptionResult.COMMIT) + .run(() -> { + register(sync1); + throw new MyRuntimeException(); + })); + assertEquals(Status.STATUS_COMMITTED, sync1.completionStatus); + + var sync2 = new TestSync(); + assertThrows(MyRuntimeException.class, () -> QuarkusTransaction.requiringNew() + .exceptionHandler((e) -> TransactionExceptionResult.COMMIT) + .call(() -> { + register(sync2); + throw new MyRuntimeException(); + })); + assertEquals(Status.STATUS_COMMITTED, sync2.completionStatus); + + var sync3 = new TestSync(); + assertThrows(QuarkusTransactionException.class, () -> QuarkusTransaction.requiringNew() + .exceptionHandler((e) -> TransactionExceptionResult.COMMIT) + .call(() -> { + register(sync3); + throw new MyCheckedException(); + })); + assertEquals(Status.STATUS_COMMITTED, sync3.completionStatus); + } + + @Test + @ActivateRequestContext + public void suspendingExisting() { + QuarkusTransaction.begin(); + assertTrue(QuarkusTransaction.isActive()); + QuarkusTransaction.suspendingExisting() + .run(() -> assertFalse(QuarkusTransaction.isActive())); + assertTrue(QuarkusTransaction.isActive()); + QuarkusTransaction.rollback(); + + assertFalse(QuarkusTransaction.isActive()); + QuarkusTransaction.suspendingExisting() + .run(() -> assertFalse(QuarkusTransaction.isActive())); + assertFalse(QuarkusTransaction.isActive()); + } + + @Test + @ActivateRequestContext + public void disallowingExisting() { + assertFalse(QuarkusTransaction.isActive()); + assertEquals(Status.STATUS_COMMITTED, + QuarkusTransaction.disallowingExisting().call(this::register).completionStatus); + assertFalse(QuarkusTransaction.isActive()); + + QuarkusTransaction.begin(); + assertTrue(QuarkusTransaction.isActive()); + assertThrows(QuarkusTransactionException.class, + () -> QuarkusTransaction.disallowingExisting().call(this::register)); + assertTrue(QuarkusTransaction.isActive()); + QuarkusTransaction.rollback(); + } + + @Test + @ActivateRequestContext + public void requiringNew() throws SystemException { + assertFalse(QuarkusTransaction.isActive()); + assertEquals(Status.STATUS_COMMITTED, + QuarkusTransaction.requiringNew().call(this::register).completionStatus); + assertFalse(QuarkusTransaction.isActive()); + + QuarkusTransaction.begin(); + assertTrue(QuarkusTransaction.isActive()); + var tx = transactionManager.getTransaction(); + assertEquals(Status.STATUS_COMMITTED, QuarkusTransaction.requiringNew().call(() -> { + assertTrue(QuarkusTransaction.isActive()); + assertNotEquals(tx, transactionManager.getTransaction()); + return register(); + }).completionStatus); + assertTrue(QuarkusTransaction.isActive()); + QuarkusTransaction.rollback(); + } + + @Test + @ActivateRequestContext + public void joiningExisting() throws SystemException { + assertFalse(QuarkusTransaction.isActive()); + assertEquals(Status.STATUS_COMMITTED, + QuarkusTransaction.joiningExisting().call(this::register).completionStatus); + assertFalse(QuarkusTransaction.isActive()); + + QuarkusTransaction.begin(); + assertTrue(QuarkusTransaction.isActive()); + var tx = transactionManager.getTransaction(); + QuarkusTransaction.joiningExisting().call(() -> { + assertTrue(QuarkusTransaction.isActive()); + assertEquals(tx, transactionManager.getTransaction()); + return null; + }); + assertTrue(QuarkusTransaction.isActive()); + QuarkusTransaction.rollback(); + } + + void register(TestSync t) { + try { + transactionManager.getTransaction().registerSynchronization(t); + } catch (RollbackException | SystemException e) { + throw new RuntimeException(e); + } + } + + TestSync register() { + TestSync t = new TestSync(); + register(t); + return t; + } + + static class TestSync implements Synchronization { + + int completionStatus = -1; + + @Override + public void beforeCompletion() { + + } + + @Override + public void afterCompletion(int status) { + this.completionStatus = status; + } + } + + static class MyCheckedException extends Exception { + MyCheckedException() { + } + } + + static class MyRuntimeException extends RuntimeException { + MyRuntimeException() { + } + } + +} diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/QuarkusTransaction.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/QuarkusTransaction.java index b1691699aacf7..33eadb815e644 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/QuarkusTransaction.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/QuarkusTransaction.java @@ -1,6 +1,7 @@ package io.quarkus.narayana.jta; import java.util.concurrent.Callable; +import java.util.function.Function; import javax.transaction.Status; import javax.transaction.SystemException; @@ -20,8 +21,9 @@ *

  • No Transaction Leaks: Transactions are tied to the request scope, if the scope is destroyed before the transaction * is committed the transaction is rolled back. Note that this means this can only currently be used when the request scope is * active.
  • - *
  • Per Transaction Timeouts: {@link RunOptions#timeout(int)} can be used to set the new transactions - * timeout, without affecting the per thread default.
  • + *
  • Per Transaction Timeouts: + * {{@link BeginOptions#timeout(int)}/{@link TransactionRunnerOptions#timeout(int)} + * can be used to set the new transactions timeout, without affecting the per thread default.
  • *
  • Lambda Style Transactions: {@link Runnable} and {@link Callable} instances can be run inside the scope of a new * transaction.
  • * @@ -102,12 +104,151 @@ static void setRollbackOnly() { QuarkusTransactionImpl.setRollbackOnly(); } + /** + * Starts the definition of a transaction runner, + * which can then be used to run a task ({@link Runnable}, {@link Callable}, ...), + * with {@link TransactionSemantics#JOIN_EXISTING} semantics: + *
      + *
    • If no transaction is active then a new transaction will be started, and committed when the method ends. + *
    • If an exception is thrown the exception handler registered by + * {@link TransactionRunnerOptions#exceptionHandler(Function)} will be called to + * decide if the TX should be committed or rolled back. + *
    • If an existing transaction is active then the method is run in the context of the existing transaction. If an + * exception is thrown the exception handler will be called, however + * a result of {@link TransactionExceptionResult#ROLLBACK} will result in the TX marked as rollback only, while a result of + * {@link TransactionExceptionResult#COMMIT} will result in no action being taken. + *
    + *

    + * Examples of use: + * + *

    {@code
    +     * QuarkusTransaction.joiningExisting().run(() -> ...);
    +     * int value = QuarkusTransaction.joiningExisting().call(() -> { ...; return 42; });
    +     * }
    + * + * @return An interface that allow various options of a transaction runner to be customized, + * or a {@link Runnable}/{@link Callable} to be executed. + * @see TransactionRunnerOptions + */ + static TransactionRunnerOptions joiningExisting() { + return runner(TransactionSemantics.JOIN_EXISTING); + } + + /** + * Starts the definition of a transaction runner, + * which can then be used to run a task ({@link Runnable}, {@link Callable}, ...), + * with {@link TransactionSemantics#REQUIRE_NEW} semantics: + *
      + *
    • If an existing transaction is already associated with the current thread then the transaction is suspended, + * then a new transaction is started which follows all the normal lifecycle rules, + * and when it's complete the original transaction is resumed. + *
    • Otherwise a new transaction is started, and follows all the normal lifecycle rules. + *
    + *

    + * Examples of use: + * + *

    {@code
    +     * QuarkusTransaction.requiringNew().run(() -> ...);
    +     * int value = QuarkusTransaction.requiringNew().call(() -> { ...; return 42; });
    +     * }
    + * + * @return An interface that allow various options of a transaction runner to be customized, + * or a {@link Runnable}/{@link Callable} to be executed. + * @see TransactionRunnerOptions + */ + static TransactionRunnerOptions requiringNew() { + return runner(TransactionSemantics.REQUIRE_NEW); + } + + /** + * Starts the definition of a transaction runner, + * which can then be used to run a task ({@link Runnable}, {@link Callable}, ...), + * with {@link TransactionSemantics#DISALLOW_EXISTING} semantics: + *
      + *
    • If a transaction is already associated with the current thread a {@link QuarkusTransactionException} will be thrown, + *
    • Otherwise a new transaction is started, and follows all the normal lifecycle rules. + *
    + *

    + * Examples of use: + * + *

    {@code
    +     * QuarkusTransaction.requiringNew().run(() -> ...);
    +     * int value = QuarkusTransaction.requiringNew().call(() -> { ...; return 42; });
    +     * }
    + * + * @return An interface that allow various options of a transaction runner to be customized, + * or a {@link Runnable}/{@link Callable} to be executed. + * @see TransactionRunnerOptions + */ + static TransactionRunnerOptions disallowingExisting() { + return runner(TransactionSemantics.DISALLOW_EXISTING); + } + + /** + * Starts the definition of a transaction runner, + * which can then be used to run a task ({@link Runnable}, {@link Callable}, ...), + * with {@link TransactionSemantics#SUSPEND_EXISTING} semantics: + *
      + *
    • If no transaction is active then these semantics are basically a no-op. + *
    • If a transaction is active then it is suspended, and resumed after the task is run. + *
    • The exception handler will never be consulted when these semantics are in use, specifying both an exception handler + * and + * these semantics are considered an error. + *
    • These semantics allows for code to easily be run outside the scope of a transaction. + *
    + *

    + * Examples of use: + * + *

    {@code
    +     * QuarkusTransaction.requiringNew().run(() -> ...);
    +     * int value = QuarkusTransaction.requiringNew().call(() -> { ...; return 42; });
    +     * }
    + * + * @return An interface that allow various options of a transaction runner to be customized, + * or a {@link Runnable}/{@link Callable} to be executed. + * @see TransactionRunnerOptions + */ + static TransactionRunnerOptions suspendingExisting() { + return runner(TransactionSemantics.SUSPEND_EXISTING); + } + + /** + * Starts the definition of a transaction runner, + * which can then be used to run a task ({@link Runnable}, {@link Callable}, ...), + * following the selected {@link TransactionSemantics}. + *

    + * Examples of use: + * + *

    {@code
    +     * QuarkusTransaction.runner(TransactionSemantics.REQUIRE_NEW).run(() -> ...);
    +     * QuarkusTransaction.runner(TransactionSemantics.JOIN_EXISTING).run(() -> ...);
    +     * QuarkusTransaction.runner(TransactionSemantics.SUSPEND_EXISTING).run(() -> ...);
    +     * QuarkusTransaction.runner(TransactionSemantics.DISALLOW_EXISTING).run(() -> ...);
    +     * int value = QuarkusTransaction.runner(TransactionSemantics.REQUIRE_NEW).call(() -> { ...; return 42; });
    +     * int value = QuarkusTransaction.runner(TransactionSemantics.JOIN_EXISTING).call(() -> { ...; return 42; });
    +     * int value = QuarkusTransaction.runner(TransactionSemantics.SUSPEND_EXISTING).call(() -> { ...; return 42; });
    +     * int value = QuarkusTransaction.runner(TransactionSemantics.DISALLOW_EXISTING).call(() -> { ...; return 42; });
    +     * }
    + * + * @param semantics The selected {@link TransactionSemantics}. + * @return An interface that allow various options of a transaction runner to be customized, + * or a {@link Runnable}/{@link Callable} to be executed. + * @see TransactionRunnerOptions + */ + static TransactionRunnerOptions runner(TransactionSemantics semantics) { + return new TransactionRunnerImpl(semantics); + } + /** * Runs a task in a new transaction with the default timeout. This defaults to {@link Transactional.TxType#REQUIRES_NEW} * semantics, however alternate semantics can be requested using {@link #run(RunOptions, Runnable)}. * * @param task The task to run in a transaction + * @deprecated For the same semantics, use {@link #requiringNew() QuarkusTransaction.requiringNew().run(task)}. + * {@link #joiningExisting()}, {@link #disallowingExisting()}, {@link #suspendingExisting()} + * and {@link #runner(TransactionSemantics)} can also be used for alternate semantics and options. */ + @Deprecated static void run(Runnable task) { run(runOptions(), task); } @@ -118,7 +259,11 @@ static void run(Runnable task) { * * @param options Options that apply to the new transaction * @param task The task to run in a transaction + * @deprecated Use {@link #requiringNew()}, {@link #joiningExisting()}, {@link #disallowingExisting()}, + * {@link #suspendingExisting()} + * or {@link #runner(TransactionSemantics)} instead. */ + @Deprecated static void run(RunOptions options, Runnable task) { call(options, new Callable() { @Override @@ -136,26 +281,40 @@ public Object call() throws Exception { * If the task throws a checked exception it will be wrapped with a {@link QuarkusTransactionException} * * @param task The task to run in a transaction + * @deprecated For the same semantics, use {@link #requiringNew() + * QuarkusTransaction.requiringNew().call(task)}. + * {@link #joiningExisting()}, {@link #disallowingExisting()}, {@link #suspendingExisting()} + * and {@link #runner(TransactionSemantics)} can also be used for alternate semantics and options. */ + @Deprecated static T call(Callable task) { return call(runOptions(), task); } /** * Calls a task in a new transaction with the default timeout. This defaults to {@link Transactional.TxType#REQUIRES_NEW} - * semantics, however alternate semantics can be requested using {@link #call(RunOptions, Callable)}. + * semantics, however alternate semantics can be requested using the {@code options} parameter. *

    * If the task throws a checked exception it will be wrapped with a {@link QuarkusTransactionException} * + * @param options Options that apply to the new transaction * @param task The task to run in a transaction + * @deprecated Use {@link #requiringNew()}, {@link #joiningExisting()}, {@link #disallowingExisting()}, + * {@link #suspendingExisting()} + * or {@link #runner(TransactionSemantics)} instead. */ + @Deprecated static T call(RunOptions options, Callable task) { return QuarkusTransactionImpl.call(options, task); } /** * @return a new RunOptions + * @deprecated Use {@link #requiringNew()}, {@link #joiningExisting()}, {@link #disallowingExisting()}, + * {@link #suspendingExisting()} + * or {@link #runner(TransactionSemantics)} instead. */ + @Deprecated static RunOptions runOptions() { return new RunOptions(); } diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/QuarkusTransactionImpl.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/QuarkusTransactionImpl.java index 4d6a10371bffd..955621392e8b5 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/QuarkusTransactionImpl.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/QuarkusTransactionImpl.java @@ -23,8 +23,8 @@ class QuarkusTransactionImpl { private static TransactionManager cachedTransactionManager; private static UserTransaction cachedUserTransaction; - public static T call(RunOptions options, Callable task) { - switch (options.semantic) { + public static T call(RunOptionsBase options, Callable task) { + switch (options.semantics) { case REQUIRE_NEW: return callRequireNew(options, task); case DISALLOW_EXISTING: @@ -34,10 +34,10 @@ public static T call(RunOptions options, Callable task) { case SUSPEND_EXISTING: return callSuspendExisting(options, task); } - throw new IllegalArgumentException("Unknown semantic"); + throw new IllegalArgumentException("Unknown semantics"); } - private static T callSuspendExisting(RunOptions options, Callable task) { + private static T callSuspendExisting(RunOptionsBase options, Callable task) { if (options.exceptionHandler != null) { throw new IllegalStateException("Cannot specify both an exception handler and SUSPEND_EXISTING"); } @@ -72,7 +72,7 @@ private static T callSuspendExisting(RunOptions options, Callable task) { } } - private static T callJoinExisting(RunOptions options, Callable task) { + private static T callJoinExisting(RunOptionsBase options, Callable task) { if (isTransactionActive()) { return callInTheirTx(options, task); } else { @@ -89,14 +89,14 @@ private static boolean isTransactionActive() { } } - private static T callDisallowExisting(RunOptions options, Callable task) { + private static T callDisallowExisting(RunOptionsBase options, Callable task) { if (isTransactionActive()) { throw new QuarkusTransactionException(new IllegalStateException("Transaction already active")); } return callInOurTx(options, task); } - private static T callRequireNew(RunOptions options, Callable task) { + private static T callRequireNew(RunOptionsBase options, Callable task) { TransactionManager transactionManager = getTransactionManager(); Transaction transaction = null; try { @@ -128,18 +128,18 @@ private static T callRequireNew(RunOptions options, Callable task) { } } - private static T callInOurTx(RunOptions options, Callable task) { + private static T callInOurTx(RunOptionsBase options, Callable task) { begin(options); try { T ret; try { ret = task.call(); } catch (Throwable t) { - RunOptions.ExceptionResult handling = RunOptions.ExceptionResult.ROLLBACK; + TransactionExceptionResult handling = TransactionExceptionResult.ROLLBACK; if (options.exceptionHandler != null) { handling = options.exceptionHandler.apply(t); } - if (handling == RunOptions.ExceptionResult.ROLLBACK) { + if (handling == TransactionExceptionResult.ROLLBACK) { getUserTransaction().rollback(); } else { getUserTransaction().commit(); @@ -166,17 +166,17 @@ private static T callInOurTx(RunOptions options, Callable task) { } } - private static T callInTheirTx(RunOptions options, Callable task) { + private static T callInTheirTx(RunOptionsBase options, Callable task) { try { T ret; try { ret = task.call(); } catch (Throwable t) { - RunOptions.ExceptionResult handling = RunOptions.ExceptionResult.ROLLBACK; + TransactionExceptionResult handling = TransactionExceptionResult.ROLLBACK; if (options.exceptionHandler != null) { handling = options.exceptionHandler.apply(t); } - if (handling == RunOptions.ExceptionResult.ROLLBACK) { + if (handling == TransactionExceptionResult.ROLLBACK) { getUserTransaction().setRollbackOnly(); } if (t instanceof RuntimeException) { @@ -196,7 +196,7 @@ private static T callInTheirTx(RunOptions options, Callable task) { } } - private static void begin(RunOptions options) { + private static void begin(RunOptionsBase options) { int timeout = options != null ? options.timeout : 0; try { if (timeout > 0) { diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/RunOptions.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/RunOptions.java index bb35f06cb7256..49979caac937a 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/RunOptions.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/RunOptions.java @@ -5,12 +5,13 @@ /** * Builder interface to allow a transaction to be customized, including things like timeout and semantics when an existing * transaction is present. + * + * @deprecated Use {@link QuarkusTransaction#requiringNew()}, {@link QuarkusTransaction#joiningExisting()}, + * {@link QuarkusTransaction#disallowingExisting()}, {@link QuarkusTransaction#suspendingExisting()} + * or {@link QuarkusTransaction#runner(TransactionSemantics)} instead. */ -public class RunOptions { - - Semantic semantic = Semantic.REQUIRE_NEW; - int timeout = 0; - Function exceptionHandler; +@Deprecated +public class RunOptions extends RunOptionsBase { /** * Sets the transaction timeout for transactions created by this builder. A value of zero refers to the system default. @@ -20,22 +21,38 @@ public class RunOptions { * @return This builder */ public RunOptions timeout(int seconds) { - if (seconds < 0) { - throw new IllegalArgumentException("seconds cannot be negative"); - } - this.timeout = seconds; + setTimeout(seconds); return this; } /** - * Sets the transaction semantic that is used to determine the action to take if a transaction is already active. + * Sets the transaction semantics that is used to determine the action to take if a transaction is already active. *

    * * @param semantic The semantic * @return This builder */ public RunOptions semantic(Semantic semantic) { - this.semantic = semantic; + if (semantic == null) { + setSemantics(null); + return this; + } + switch (semantic) { + case DISALLOW_EXISTING: + setSemantics(TransactionSemantics.DISALLOW_EXISTING); + break; + case JOIN_EXISTING: + setSemantics(TransactionSemantics.JOIN_EXISTING); + break; + case REQUIRE_NEW: + setSemantics(TransactionSemantics.REQUIRE_NEW); + break; + case SUSPEND_EXISTING: + setSemantics(TransactionSemantics.SUSPEND_EXISTING); + break; + default: + throw new IllegalArgumentException("Unsupported value: " + semantic); + } return this; } @@ -53,7 +70,19 @@ public RunOptions semantic(Semantic semantic) { * @return This builder */ public RunOptions exceptionHandler(Function handler) { - this.exceptionHandler = handler; + setExceptionHandler(handler.andThen(result -> { + if (result == null) { + return null; + } + switch (result) { + case COMMIT: + return TransactionExceptionResult.COMMIT; + case ROLLBACK: + return TransactionExceptionResult.ROLLBACK; + default: + throw new IllegalArgumentException("Unsupported value: " + result); + } + })); return this; } diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/RunOptionsBase.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/RunOptionsBase.java new file mode 100644 index 0000000000000..bf6000250950f --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/RunOptionsBase.java @@ -0,0 +1,33 @@ +package io.quarkus.narayana.jta; + +import java.util.function.Function; + +/** + * An abstract base for both {@link RunOptions} and {@link TransactionRunnerImpl}. + *

    + * Necessary because having {@link RunOptions} extend {@link TransactionRunnerImpl}., + * or the other way around, results in signature conflicts in {@code exceptionHandler(Function)}. + */ +class RunOptionsBase { + TransactionSemantics semantics = TransactionSemantics.REQUIRE_NEW; + int timeout = 0; + Function exceptionHandler; + + RunOptionsBase setTimeout(int seconds) { + if (seconds < 0) { + throw new IllegalArgumentException("seconds cannot be negative"); + } + this.timeout = seconds; + return this; + } + + RunOptionsBase setSemantics(TransactionSemantics semantics) { + this.semantics = semantics; + return this; + } + + RunOptionsBase setExceptionHandler(Function handler) { + this.exceptionHandler = handler; + return this; + } +} diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/TransactionExceptionResult.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/TransactionExceptionResult.java new file mode 100644 index 0000000000000..0b347af184553 --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/TransactionExceptionResult.java @@ -0,0 +1,26 @@ +package io.quarkus.narayana.jta; + +import java.util.function.Function; + +/** + * Enum that can be used to control the decision to rollback or commit based on the type of an exception. + * + * @see QuarkusTransaction#joiningExisting() + * @see QuarkusTransaction#requiringNew() + * @see QuarkusTransaction#disallowingExisting() + * @see QuarkusTransaction#suspendingExisting() + * @see QuarkusTransaction#runner(TransactionSemantics) + * @see TransactionRunnerOptions#exceptionHandler(Function) + */ +public enum TransactionExceptionResult { + + /** + * The transaction should be committed. + */ + COMMIT, + /** + * The transaction should be rolled back. + */ + ROLLBACK + +} diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/TransactionRunner.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/TransactionRunner.java new file mode 100644 index 0000000000000..82a7a6f3e5692 --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/TransactionRunner.java @@ -0,0 +1,37 @@ +package io.quarkus.narayana.jta; + +import java.util.concurrent.Callable; + +/** + * Runs tasks in transactions with pre-defined semantics and options. + * + * @see QuarkusTransaction#joiningExisting() + * @see QuarkusTransaction#requiringNew() + * @see QuarkusTransaction#disallowingExisting() + * @see QuarkusTransaction#suspendingExisting() + * @see QuarkusTransaction#runner(TransactionSemantics) + * @see TransactionSemantics + */ +public interface TransactionRunner { + + /** + * Runs the given runnable, + * starting/suspending transactions as required by the selected {@link TransactionSemantics semantics}. + * + * @param task A task to run with the selected transaction semantics. + */ + void run(Runnable task); + + /** + * Calls the given callable, + * starting/suspending transactions as required by the selected {@link TransactionSemantics semantics}. + *

    + * If the task throws a checked exception it will be wrapped with a {@link QuarkusTransactionException} + * + * @param task A task to run with the selected transaction semantics. + * @return The value returned by {@code task.call()}. + * @throws QuarkusTransactionException If the task throws a checked exception. + */ + T call(Callable task); + +} diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/TransactionRunnerImpl.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/TransactionRunnerImpl.java new file mode 100644 index 0000000000000..23a701a40be6f --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/TransactionRunnerImpl.java @@ -0,0 +1,36 @@ +package io.quarkus.narayana.jta; + +import java.util.concurrent.Callable; +import java.util.function.Function; + +class TransactionRunnerImpl extends RunOptionsBase + implements TransactionRunnerOptions, TransactionRunner { + TransactionRunnerImpl(TransactionSemantics semantics) { + setSemantics(semantics); + } + + @Override + public TransactionRunnerImpl timeout(int seconds) { + setTimeout(seconds); + return this; + } + + @Override + public TransactionRunnerImpl exceptionHandler(Function handler) { + setExceptionHandler(handler); + return this; + } + + @Override + public void run(Runnable task) { + QuarkusTransactionImpl.call(this, () -> { + task.run(); + return null; + }); + } + + @Override + public T call(Callable task) { + return QuarkusTransactionImpl.call(this, task); + } +} diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/TransactionRunnerOptions.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/TransactionRunnerOptions.java new file mode 100644 index 0000000000000..601d1e96d4592 --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/TransactionRunnerOptions.java @@ -0,0 +1,45 @@ +package io.quarkus.narayana.jta; + +import java.util.concurrent.Callable; +import java.util.function.Function; + +/** + * Builder interface to allow various options of a transaction runner to be customized. + *

    + * Note this interface extends {@link TransactionRunner}, + * so it can be used to run a task directly with {@link #run(Runnable)}/{@link #call(Callable)}, + * even if no options need to be customized. + * + * @see QuarkusTransaction#joiningExisting() + * @see QuarkusTransaction#requiringNew() + * @see QuarkusTransaction#disallowingExisting() + * @see QuarkusTransaction#suspendingExisting() + * @see QuarkusTransaction#runner(TransactionSemantics) + */ +public interface TransactionRunnerOptions extends TransactionRunner { + + /** + * Sets the transaction timeout for transactions created by this runner. A value of zero refers to the system default. + * + * @throws IllegalArgumentException If seconds is negative + * @param seconds The timeout in seconds + * @return This builder + */ + TransactionRunnerOptions timeout(int seconds); + + /** + * Provides an exception handler that can make a decision to rollback or commit based on the type of exception. If the + * predicate returns {@link TransactionExceptionResult#ROLLBACK} the transaction is rolled back, + * otherwise it is committed. + *

    + * This exception will still be propagated to the caller, so this method should not log or perform any other actions other + * than determine what should happen to the current transaction. + *

    + * By default, the exception is always rolled back. + * + * @param handler The exception handler + * @return This builder + */ + TransactionRunnerOptions exceptionHandler(Function handler); + +} diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/TransactionSemantics.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/TransactionSemantics.java new file mode 100644 index 0000000000000..bfbba96b0250a --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/TransactionSemantics.java @@ -0,0 +1,68 @@ +package io.quarkus.narayana.jta; + +import java.util.function.Function; + +/** + * Enum that can be used to control the transaction behaviour in the presence or absence of an existing transaction. + * + * @see QuarkusTransaction#joiningExisting() + * @see QuarkusTransaction#requiringNew() + * @see QuarkusTransaction#disallowingExisting() + * @see QuarkusTransaction#suspendingExisting() + * @see QuarkusTransaction#runner(TransactionSemantics) + */ +public enum TransactionSemantics { + + /** + * If a transaction is already associated with the current thread a {@link QuarkusTransactionException} will be thrown, + * otherwise a new transaction is started, and follows all the normal lifecycle rules. + * + * @see QuarkusTransaction#disallowingExisting() + * @see QuarkusTransaction#runner(TransactionSemantics) + */ + DISALLOW_EXISTING, + + /** + * If no transaction is active then a new transaction will be started, and committed when the method ends. + * If an exception is thrown the exception handler registered by + * {@link TransactionRunnerOptions#exceptionHandler(Function)} will be called to + * decide if the TX should be committed or rolled back. + *

    + * If an existing transaction is active then the method is run in the context of the existing transaction. If an + * exception is thrown the exception handler will be called, however + * a result of {@link TransactionExceptionResult#ROLLBACK} will result in the TX marked as rollback only, while a result of + * {@link TransactionExceptionResult#COMMIT} will result in no action being taken. + * + * @see QuarkusTransaction#joiningExisting() + * @see QuarkusTransaction#runner(TransactionSemantics) + */ + JOIN_EXISTING, + + /** + * If an existing transaction is already associated with the current thread then the transaction is suspended, + * then a new transaction is started which follows all the normal lifecycle rules, + * and when it's complete the original transaction is resumed. + *

    + * Otherwise, a new transaction is started, and follows all the normal lifecycle rules. + * + * @see QuarkusTransaction#requiringNew() + * @see QuarkusTransaction#runner(TransactionSemantics) + */ + REQUIRE_NEW, + + /** + * If no transaction is active then these semantics are basically a no-op. + *

    + * If a transaction is active then it is suspended, and resumed after the task is run. + *

    + * The exception handler will never be consulted when these semantics are in use, specifying both an exception handler and + * these semantics are considered an error. + *

    + * These semantics allows for code to easily be run outside the scope of a transaction. + * + * @see QuarkusTransaction#suspendingExisting() + * @see QuarkusTransaction#runner(TransactionSemantics) + */ + SUSPEND_EXISTING + +} diff --git a/extensions/oidc-client-filter/deployment/src/main/java/io/quarkus/oidc/client/filter/deployment/NamedOidcClientFilterBuildItem.java b/extensions/oidc-client-filter/deployment/src/main/java/io/quarkus/oidc/client/filter/deployment/NamedOidcClientFilterBuildItem.java new file mode 100644 index 0000000000000..10ef1343f1a4b --- /dev/null +++ b/extensions/oidc-client-filter/deployment/src/main/java/io/quarkus/oidc/client/filter/deployment/NamedOidcClientFilterBuildItem.java @@ -0,0 +1,17 @@ +package io.quarkus.oidc.client.filter.deployment; + +import java.util.Set; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Contains a list of all Rest clients annotated with @OidcClientFilter("someClientName"). + */ +public final class NamedOidcClientFilterBuildItem extends SimpleBuildItem { + + final Set namedFilterClientClasses; + + NamedOidcClientFilterBuildItem(Set namedFilterClientClasses) { + this.namedFilterClientClasses = Set.copyOf(namedFilterClientClasses); + } +} diff --git a/extensions/oidc-client-filter/deployment/src/main/java/io/quarkus/oidc/client/filter/deployment/OidcClientFilterBuildStep.java b/extensions/oidc-client-filter/deployment/src/main/java/io/quarkus/oidc/client/filter/deployment/OidcClientFilterBuildStep.java index 5d1ce05cdbd40..ddf729ac033ae 100644 --- a/extensions/oidc-client-filter/deployment/src/main/java/io/quarkus/oidc/client/filter/deployment/OidcClientFilterBuildStep.java +++ b/extensions/oidc-client-filter/deployment/src/main/java/io/quarkus/oidc/client/filter/deployment/OidcClientFilterBuildStep.java @@ -1,17 +1,29 @@ package io.quarkus.oidc.client.filter.deployment; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.oidc.client.deployment.OidcClientBuildStep.IsEnabled; +import io.quarkus.oidc.client.deployment.OidcClientFilterDeploymentHelper; import io.quarkus.oidc.client.filter.OidcClientFilter; import io.quarkus.oidc.client.filter.OidcClientRequestFilter; +import io.quarkus.oidc.client.filter.runtime.AbstractOidcClientRequestFilter; import io.quarkus.oidc.client.filter.runtime.OidcClientFilterConfig; import io.quarkus.restclient.deployment.RestClientAnnotationProviderBuildItem; +import io.quarkus.restclient.deployment.RestClientPredicateProviderBuildItem; import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; @BuildSteps(onlyIf = IsEnabled.class) @@ -24,15 +36,92 @@ public class OidcClientFilterBuildStep { @BuildStep void registerProvider(BuildProducer additionalBeans, BuildProducer reflectiveClass, + NamedOidcClientFilterBuildItem namedOidcClientFilterBuildItem, BuildProducer jaxrsProviders, + BuildProducer restPredicateProvider, BuildProducer restAnnotationProvider) { + additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(OidcClientRequestFilter.class)); reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, OidcClientRequestFilter.class)); + final Set namedFilterClientClasses = namedOidcClientFilterBuildItem.namedFilterClientClasses; + + // register default request filter provider against the rest of the clients (client != namedFilterClientClasses) if (config.registerFilter) { - jaxrsProviders.produce(new ResteasyJaxrsProviderBuildItem(OidcClientRequestFilter.class.getName())); + if (namedFilterClientClasses.isEmpty()) { + // register default request filter as global rest client provider + jaxrsProviders.produce(new ResteasyJaxrsProviderBuildItem(OidcClientRequestFilter.class.getName())); + } else { + // register all clients without @OidcClientFilter("clientName") + restPredicateProvider + .produce(new RestClientPredicateProviderBuildItem(OidcClientRequestFilter.class.getName(), + new Predicate() { + // test whether the provider should be added restClientClassInfo + @Override + public boolean test(ClassInfo restClientClassInfo) { + // do not register default request filter as provider against Rest client with named filter + return !namedFilterClientClasses.contains(restClientClassInfo.name().toString()); + } + })); + } } else { - restAnnotationProvider.produce(new RestClientAnnotationProviderBuildItem(OIDC_CLIENT_FILTER, - OidcClientRequestFilter.class)); + if (namedFilterClientClasses.isEmpty()) { + // register default request filter against all the Rest clients annotated with @OidcClientFilter + restAnnotationProvider.produce(new RestClientAnnotationProviderBuildItem(OIDC_CLIENT_FILTER, + OidcClientRequestFilter.class)); + } else { + // register default request filter against Rest client annotated with @OidcClientFilter without named ones + restPredicateProvider + .produce(new RestClientPredicateProviderBuildItem(OidcClientRequestFilter.class.getName(), + new Predicate() { + // test whether the provider should be added restClientClassInfo + @Override + public boolean test(ClassInfo restClientClassInfo) { + // do not register default request filter as provider against Rest client with named filter + return restClientClassInfo.hasAnnotation(OIDC_CLIENT_FILTER) + && !namedFilterClientClasses.contains(restClientClassInfo.name().toString()); + } + })); + } + } + } + + @BuildStep + NamedOidcClientFilterBuildItem registerNamedProviders(BuildProducer reflectiveClass, + CombinedIndexBuildItem indexBuildItem, + BuildProducer restPredicateProvider, + BuildProducer generatedBean) { + + // create and register named request filter for each @OidcClientFilter("clientName") + final var helper = new OidcClientFilterDeploymentHelper<>(AbstractOidcClientRequestFilter.class, generatedBean); + Collection instances = indexBuildItem.getIndex().getAnnotations(OIDC_CLIENT_FILTER); + final Set namedFilterClientClasses = new HashSet<>(); + for (AnnotationInstance instance : instances) { + // get client name from annotation @OidcClientFilter("clientName") + final String clientName = OidcClientFilterDeploymentHelper.getClientName(instance); + // do not create & register named filter for the OidcClient registered through configuration property + // as default request filter got it covered + if (clientName != null && !clientName.equals(config.clientName.orElse(null))) { + + // create named filter class for named OidcClient + // we generate exactly one custom filter for each named client specified through annotation + final var generatedProvider = helper.getOrCreateNamedTokensProducerFor(clientName); + final var targetRestClient = instance.target().asClass().name().toString(); + namedFilterClientClasses.add(targetRestClient); + + // register for reflection + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, true, true, generatedProvider)); + + // register named request filter provider against Rest client + restPredicateProvider.produce(new RestClientPredicateProviderBuildItem(generatedProvider, + new Predicate() { + // test whether the provider should be added restClientClassInfo + @Override + public boolean test(ClassInfo restClientClassInfo) { + return targetRestClient.equals(restClientClassInfo.name().toString()); + } + })); + } } + return new NamedOidcClientFilterBuildItem(namedFilterClientClasses); } } diff --git a/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ConfigPropertyOidcClientResource.java b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ConfigPropertyOidcClientResource.java new file mode 100644 index 0000000000000..659168be30c6e --- /dev/null +++ b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ConfigPropertyOidcClientResource.java @@ -0,0 +1,31 @@ +package io.quarkus.oidc.client.filter; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("/config-property-oidc-client") +public class ConfigPropertyOidcClientResource { + + @Inject + @RestClient + ProtectedResourceServiceConfigPropertyOidcClient protectedResourceServiceConfigPropertyOidcClient; + + @Inject + @RestClient + ProtectedResourceServiceCustomProviderConfigPropOidcClient protectedResourceServiceCustomProviderConfigPropOidcClient; + + @GET + @Path("/annotation/user-name") + public String userName() { + return protectedResourceServiceConfigPropertyOidcClient.getUserName(); + } + + @GET + @Path("/custom-provider/user-name") + public String customProviderConfigPropertyUserName() { + return protectedResourceServiceCustomProviderConfigPropOidcClient.getUserName(); + } +} diff --git a/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/NamedOidcClientFilterDevModeTest.java b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/NamedOidcClientFilterDevModeTest.java new file mode 100644 index 0000000000000..58e38e74b8f92 --- /dev/null +++ b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/NamedOidcClientFilterDevModeTest.java @@ -0,0 +1,52 @@ +package io.quarkus.oidc.client.filter; + +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager; +import io.restassured.RestAssured; + +@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class) +public class NamedOidcClientFilterDevModeTest { + + private static final Class[] testClasses = { + ProtectedResource.class, + ProtectedResourceServiceNamedOidcClient.class, + ProtectedResourceServiceConfigPropertyOidcClient.class, + ProtectedResourceServiceCustomProviderConfigPropOidcClient.class, + NamedOidcClientResource.class, + ConfigPropertyOidcClientResource.class + }; + + @RegisterExtension + static final QuarkusDevModeTest test = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(testClasses) + .addAsResource("application-named-oidc-client-filter.properties", "application.properties")); + + @Test + public void testGerUserConfigPropertyAndAnnotation() { + // OidcClient selected via @OidcClient("clientName") + RestAssured.when().get("/named-oidc-client/user-name") + .then() + .statusCode(200) + .body(equalTo("jdoe")); + + // @OidcClientFilter: OidcClient selected via `quarkus.oidc-client-filter.client-name=config-property` + RestAssured.when().get("/config-property-oidc-client/annotation/user-name") + .then() + .statusCode(200) + .body(equalTo("alice")); + + // @RegisterProvider(OidcClientRequestFilter.class): OidcClient selected via `quarkus.oidc-client-filter.client-name=config-property` + RestAssured.when().get("/config-property-oidc-client/custom-provider/user-name") + .then() + .statusCode(200) + .body(equalTo("alice")); + } + +} diff --git a/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/NamedOidcClientResource.java b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/NamedOidcClientResource.java new file mode 100644 index 0000000000000..5419cddb7598c --- /dev/null +++ b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/NamedOidcClientResource.java @@ -0,0 +1,21 @@ +package io.quarkus.oidc.client.filter; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("/named-oidc-client") +public class NamedOidcClientResource { + + @Inject + @RestClient + ProtectedResourceServiceNamedOidcClient protectedResourceServiceNamedOidcClient; + + @GET + @Path("user-name") + public String userName() { + return protectedResourceServiceNamedOidcClient.getUserName(); + } +} diff --git a/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/OidcClientFilterDevModeTest.java b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/OidcClientFilterDevModeTest.java index 68e1606c09087..97cbbb8d7868c 100644 --- a/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/OidcClientFilterDevModeTest.java +++ b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/OidcClientFilterDevModeTest.java @@ -26,10 +26,12 @@ @QuarkusTestResource(KeycloakTestResourceLifecycleManager.class) public class OidcClientFilterDevModeTest { - private static Class[] testClasses = { + private static final Class[] testClasses = { FrontendResource.class, ProtectedResource.class, - ProtectedResourceService.class + ProtectedResourceService.class, + ProtectedResourceServiceNamedOidcClient.class, + NamedOidcClientResource.class }; @RegisterExtension @@ -60,6 +62,13 @@ public void testGetUserName() { .statusCode(200) .body(equalTo("alice")); checkLog(); + + // here we test that user can optionally select named OidcClient like this @OidcClient("clientName") + // even though 'quarkus.oidc-client-filter.register-filter' is enabled + RestAssured.when().get("/named-oidc-client/user-name") + .then() + .statusCode(200) + .body(equalTo("jdoe")); } private void checkLog() { diff --git a/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ProtectedResourceServiceConfigPropertyOidcClient.java b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ProtectedResourceServiceConfigPropertyOidcClient.java new file mode 100644 index 0000000000000..929f35e34e3d4 --- /dev/null +++ b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ProtectedResourceServiceConfigPropertyOidcClient.java @@ -0,0 +1,15 @@ +package io.quarkus.oidc.client.filter; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@OidcClientFilter +@RegisterRestClient +@Path("/") +public interface ProtectedResourceServiceConfigPropertyOidcClient { + + @GET + String getUserName(); +} diff --git a/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ProtectedResourceServiceCustomProviderConfigPropOidcClient.java b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ProtectedResourceServiceCustomProviderConfigPropOidcClient.java new file mode 100644 index 0000000000000..2d1eeda97ab58 --- /dev/null +++ b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ProtectedResourceServiceCustomProviderConfigPropOidcClient.java @@ -0,0 +1,16 @@ +package io.quarkus.oidc.client.filter; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterProvider(OidcClientRequestFilter.class) +@RegisterRestClient +@Path("/") +public interface ProtectedResourceServiceCustomProviderConfigPropOidcClient { + + @GET + String getUserName(); +} diff --git a/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ProtectedResourceServiceNamedOidcClient.java b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ProtectedResourceServiceNamedOidcClient.java new file mode 100644 index 0000000000000..efb484c0630d8 --- /dev/null +++ b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ProtectedResourceServiceNamedOidcClient.java @@ -0,0 +1,15 @@ +package io.quarkus.oidc.client.filter; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@OidcClientFilter("named") +@RegisterRestClient +@Path("/") +public interface ProtectedResourceServiceNamedOidcClient { + + @GET + String getUserName(); +} diff --git a/extensions/oidc-client-filter/deployment/src/test/resources/application-named-oidc-client-filter.properties b/extensions/oidc-client-filter/deployment/src/test/resources/application-named-oidc-client-filter.properties new file mode 100644 index 0000000000000..617c54e26d925 --- /dev/null +++ b/extensions/oidc-client-filter/deployment/src/test/resources/application-named-oidc-client-filter.properties @@ -0,0 +1,24 @@ +quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.client-id=quarkus-service-app +quarkus.oidc.credentials.secret=secret + +quarkus.oidc-client-filter.client-name=config-property +quarkus.oidc-client.config-property.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.config-property.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.config-property.credentials.client-secret.value=${quarkus.oidc.credentials.secret} +quarkus.oidc-client.config-property.credentials.client-secret.method=POST +quarkus.oidc-client.config-property.grant.type=password +quarkus.oidc-client.config-property.grant-options.password.username=alice +quarkus.oidc-client.config-property.grant-options.password.password=alice + +quarkus.oidc-client.named.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.named.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.named.credentials.client-secret.value=${quarkus.oidc.credentials.secret} +quarkus.oidc-client.named.credentials.client-secret.method=POST +quarkus.oidc-client.named.grant.type=password +quarkus.oidc-client.named.grant-options.password.username=jdoe +quarkus.oidc-client.named.grant-options.password.password=jdoe + +io.quarkus.oidc.client.filter.ProtectedResourceServiceNamedOidcClient/mp-rest/url=http://localhost:8080/protected +io.quarkus.oidc.client.filter.ProtectedResourceServiceConfigPropertyOidcClient/mp-rest/url=http://localhost:8080/protected +io.quarkus.oidc.client.filter.ProtectedResourceServiceCustomProviderConfigPropOidcClient/mp-rest/url=http://localhost:8080/protected \ No newline at end of file diff --git a/extensions/oidc-client-filter/deployment/src/test/resources/application-oidc-client-filter.properties b/extensions/oidc-client-filter/deployment/src/test/resources/application-oidc-client-filter.properties index 830a0ef048dfd..e633ad9ba461c 100644 --- a/extensions/oidc-client-filter/deployment/src/test/resources/application-oidc-client-filter.properties +++ b/extensions/oidc-client-filter/deployment/src/test/resources/application-oidc-client-filter.properties @@ -10,10 +10,19 @@ quarkus.oidc-client.grant.type=password quarkus.oidc-client.grant-options.password.username=alice quarkus.oidc-client.grant-options.password.password=alice +quarkus.oidc-client.named.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.named.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.named.credentials.client-secret.value=${quarkus.oidc.credentials.secret} +quarkus.oidc-client.named.credentials.client-secret.method=POST +quarkus.oidc-client.named.grant.type=password +quarkus.oidc-client.named.grant-options.password.username=jdoe +quarkus.oidc-client.named.grant-options.password.password=jdoe + #quarkus.oidc-client-filter.register-filter=true quarkus.oidc-client.refresh-token-time-skew=5S io.quarkus.oidc.client.filter.ProtectedResourceService/mp-rest/url=http://localhost:8080/protected +io.quarkus.oidc.client.filter.ProtectedResourceServiceNamedOidcClient/mp-rest/url=http://localhost:8080/protected quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".min-level=TRACE quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".level=TRACE quarkus.log.file.enable=true diff --git a/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientRequestFilter.java b/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientRequestFilter.java index 5da589248d305..e1a3fa9708094 100644 --- a/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientRequestFilter.java +++ b/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientRequestFilter.java @@ -1,52 +1,24 @@ package io.quarkus.oidc.client.filter; -import java.io.IOException; import java.util.Optional; import javax.annotation.Priority; import javax.inject.Inject; import javax.inject.Singleton; import javax.ws.rs.Priorities; -import javax.ws.rs.client.ClientRequestContext; -import javax.ws.rs.client.ClientRequestFilter; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; import javax.ws.rs.ext.Provider; -import org.jboss.logging.Logger; - +import io.quarkus.oidc.client.filter.runtime.AbstractOidcClientRequestFilter; import io.quarkus.oidc.client.filter.runtime.OidcClientFilterConfig; -import io.quarkus.oidc.client.runtime.AbstractTokensProducer; -import io.quarkus.oidc.client.runtime.DisabledOidcClientException; -import io.quarkus.oidc.common.runtime.OidcConstants; @Provider @Singleton @Priority(Priorities.AUTHENTICATION) -public class OidcClientRequestFilter extends AbstractTokensProducer implements ClientRequestFilter { - private static final Logger LOG = Logger.getLogger(OidcClientRequestFilter.class); - private static final String BEARER_SCHEME_WITH_SPACE = OidcConstants.BEARER_SCHEME + " "; +public class OidcClientRequestFilter extends AbstractOidcClientRequestFilter { + @Inject OidcClientFilterConfig oidcClientFilterConfig; - @Override - public void filter(ClientRequestContext requestContext) throws IOException { - try { - final String accessToken = getAccessToken(); - requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_SCHEME_WITH_SPACE + accessToken); - } catch (DisabledOidcClientException ex) { - requestContext.abortWith(Response.status(500).build()); - } catch (Exception ex) { - LOG.debugf("Access token is not available, aborting the request with HTTP 401 error: %s", ex.getMessage()); - requestContext.abortWith(Response.status(401).build()); - } - } - - private String getAccessToken() { - // It should be reactive when run with Resteasy Reactive - return awaitTokens().getAccessToken(); - } - protected Optional clientId() { return oidcClientFilterConfig.clientName; } diff --git a/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/runtime/AbstractOidcClientRequestFilter.java b/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/runtime/AbstractOidcClientRequestFilter.java new file mode 100644 index 0000000000000..35de8080216d0 --- /dev/null +++ b/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/runtime/AbstractOidcClientRequestFilter.java @@ -0,0 +1,40 @@ +package io.quarkus.oidc.client.filter.runtime; + +import java.io.IOException; + +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +import org.jboss.logging.Logger; + +import io.quarkus.oidc.client.runtime.AbstractTokensProducer; +import io.quarkus.oidc.client.runtime.DisabledOidcClientException; +import io.quarkus.oidc.common.runtime.OidcConstants; + +public class AbstractOidcClientRequestFilter extends AbstractTokensProducer implements ClientRequestFilter { + private static final Logger LOG = Logger.getLogger(AbstractOidcClientRequestFilter.class); + private static final String BEARER_SCHEME_WITH_SPACE = OidcConstants.BEARER_SCHEME + " "; + + public AbstractOidcClientRequestFilter() { + } + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + try { + final String accessToken = getAccessToken(); + requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_SCHEME_WITH_SPACE + accessToken); + } catch (DisabledOidcClientException ex) { + requestContext.abortWith(Response.status(500).build()); + } catch (Exception ex) { + LOG.debugf("Access token is not available, aborting the request with HTTP 401 error: %s", ex.getMessage()); + requestContext.abortWith(Response.status(401).build()); + } + } + + private String getAccessToken() { + return awaitTokens().getAccessToken(); + } + +} diff --git a/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/runtime/OidcClientFilterConfig.java b/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/runtime/OidcClientFilterConfig.java index f491904fc2169..37ab2a7a03768 100644 --- a/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/runtime/OidcClientFilterConfig.java +++ b/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/runtime/OidcClientFilterConfig.java @@ -16,7 +16,8 @@ public class OidcClientFilterConfig { public boolean registerFilter; /** - * Name of the configured OidcClient. + * Name of the configured OidcClient used by the OidcClientRequestFilter. You can override this configuration for + * individual MP RestClient with the `io.quarkus.oidc.client.filter.OidcClientFilter` annotation. */ @ConfigItem public Optional clientName; diff --git a/extensions/oidc-client-reactive-filter/deployment/pom.xml b/extensions/oidc-client-reactive-filter/deployment/pom.xml index 171436bc7a5c3..b547995beab70 100644 --- a/extensions/oidc-client-reactive-filter/deployment/pom.xml +++ b/extensions/oidc-client-reactive-filter/deployment/pom.xml @@ -26,6 +26,38 @@ io.quarkus quarkus-rest-client-reactive-deployment + + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-test-keycloak-server + test + + + junit + junit + + + + + io.quarkus + quarkus-oidc-deployment + test + + + io.quarkus + quarkus-resteasy-reactive-deployment + test + @@ -56,4 +88,28 @@ + + + test-keycloak + + + test-containers + + + + + + maven-surefire-plugin + + false + + ${keycloak.docker.legacy.image} + false + + + + + + + diff --git a/extensions/oidc-client-reactive-filter/deployment/src/main/java/io/quarkus/oidc/client/reactive/filter/deployment/OidcClientReactiveFilterBuildStep.java b/extensions/oidc-client-reactive-filter/deployment/src/main/java/io/quarkus/oidc/client/reactive/filter/deployment/OidcClientReactiveFilterBuildStep.java index 1b8579b71eada..f3bebe1e72b87 100644 --- a/extensions/oidc-client-reactive-filter/deployment/src/main/java/io/quarkus/oidc/client/reactive/filter/deployment/OidcClientReactiveFilterBuildStep.java +++ b/extensions/oidc-client-reactive-filter/deployment/src/main/java/io/quarkus/oidc/client/reactive/filter/deployment/OidcClientReactiveFilterBuildStep.java @@ -3,12 +3,15 @@ import java.util.Collection; import java.util.List; +import javax.ws.rs.Priorities; + import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.DotName; import org.jboss.jandex.Type; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; @@ -16,8 +19,11 @@ import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.oidc.client.deployment.OidcClientBuildStep.IsEnabled; +import io.quarkus.oidc.client.deployment.OidcClientFilterDeploymentHelper; import io.quarkus.oidc.client.filter.OidcClientFilter; import io.quarkus.oidc.client.reactive.filter.OidcClientRequestReactiveFilter; +import io.quarkus.oidc.client.reactive.filter.runtime.AbstractOidcClientRequestReactiveFilter; +import io.quarkus.oidc.client.reactive.filter.runtime.OidcClientReactiveFilterConfig; import io.quarkus.rest.client.reactive.deployment.DotNames; import io.quarkus.rest.client.reactive.deployment.RegisterProviderAnnotationInstanceBuildItem; @@ -27,20 +33,41 @@ public class OidcClientReactiveFilterBuildStep { private static final DotName OIDC_CLIENT_FILTER = DotName.createSimple(OidcClientFilter.class.getName()); private static final DotName OIDC_CLIENT_REQUEST_REACTIVE_FILTER = DotName .createSimple(OidcClientRequestReactiveFilter.class.getName()); + OidcClientReactiveFilterConfig oidcClientReactiveFilterConfig; // we simply pretend that @OidcClientFilter means @RegisterProvider(OidcClientRequestReactiveFilter.class) @BuildStep - void oidcClientFilterSupport(CombinedIndexBuildItem indexBuildItem, + void oidcClientFilterSupport(CombinedIndexBuildItem indexBuildItem, BuildProducer generatedBean, BuildProducer producer) { + final var helper = new OidcClientFilterDeploymentHelper<>(AbstractOidcClientRequestReactiveFilter.class, generatedBean); + Collection instances = indexBuildItem.getIndex().getAnnotations(OIDC_CLIENT_FILTER); for (AnnotationInstance instance : instances) { + + // get client name from annotation @OidcClientFilter("clientName") + final String clientName = OidcClientFilterDeploymentHelper.getClientName(instance); + final AnnotationValue valueAttr; + if (clientName != null && !clientName.equals(oidcClientReactiveFilterConfig.clientName.orElse(null))) { + // create and use custom filter for named OidcClient + // we generate exactly one custom filter for each named client specified through annotation + valueAttr = createClassValue(helper.getOrCreateFilter(clientName)); + } else { + // use default filter for either default OidcClient or the client configured with config properties + valueAttr = createClassValue(OIDC_CLIENT_REQUEST_REACTIVE_FILTER); + } + + final AnnotationValue priorityAttr = AnnotationValue.createIntegerValue("priority", Priorities.AUTHENTICATION); String targetClass = instance.target().asClass().name().toString(); producer.produce(new RegisterProviderAnnotationInstanceBuildItem(targetClass, AnnotationInstance.create( - DotNames.REGISTER_PROVIDER, instance.target(), List.of(AnnotationValue.createClassValue("value", - Type.create(OIDC_CLIENT_REQUEST_REACTIVE_FILTER, org.jboss.jandex.Type.Kind.CLASS)))))); + DotNames.REGISTER_PROVIDER, instance.target(), List.of(valueAttr, priorityAttr)))); } } + private AnnotationValue createClassValue(DotName filter) { + return AnnotationValue.createClassValue("value", + Type.create(filter, Type.Kind.CLASS)); + } + @BuildStep void registerProvider(BuildProducer additionalBeans, BuildProducer reflectiveClass, diff --git a/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/NamedOidcClientFilterDevModeTest.java b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/NamedOidcClientFilterDevModeTest.java new file mode 100644 index 0000000000000..ef401cbe4d80d --- /dev/null +++ b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/NamedOidcClientFilterDevModeTest.java @@ -0,0 +1,53 @@ +package io.quarkus.oidc.client.reactive.filter; + +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager; +import io.restassured.RestAssured; + +@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class) +public class NamedOidcClientFilterDevModeTest { + + private static final Class[] testClasses = { + ProtectedResource.class, + ProtectedResourceServiceAnnotationOidcClient.class, + ProtectedResourceServiceConfigPropertyOidcClient.class, + ProtectedResourceServiceCustomProviderConfigPropOidcClient.class, + OidcClientResource.class + }; + + @RegisterExtension + static final QuarkusDevModeTest test = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(testClasses) + .addAsResource("application-oidc-client-reactive-filter.properties", "application.properties")); + + @Test + public void testGerUserConfigPropertyAndAnnotation() { + // test OidcClientFilter with OidcClient selected via annotation or config-property + + // OidcClient selected via @OidcClient("clientName") + RestAssured.when().get("/oidc-client/annotation/user-name") + .then() + .statusCode(200) + .body(equalTo("jdoe")); + + // @OidcClientFilter: OidcClient selected via `quarkus.oidc-client-filter.client-name=config-property` + RestAssured.when().get("/oidc-client/config-property/user-name") + .then() + .statusCode(200) + .body(equalTo("alice")); + + // @RegisterProvider(OidcClientRequestReactiveFilter.class): OidcClient selected via `quarkus.oidc-client-filter.client-name=config-property` + RestAssured.when().get("/oidc-client/custom-provider-config-property/user-name") + .then() + .statusCode(200) + .body(equalTo("alice")); + } + +} diff --git a/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/OidcClientResource.java b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/OidcClientResource.java new file mode 100644 index 0000000000000..4e36adab6ae26 --- /dev/null +++ b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/OidcClientResource.java @@ -0,0 +1,41 @@ +package io.quarkus.oidc.client.reactive.filter; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("/oidc-client") +public class OidcClientResource { + + @Inject + @RestClient + ProtectedResourceServiceAnnotationOidcClient protectedResourceServiceAnnotationOidcClient; + + @Inject + @RestClient + ProtectedResourceServiceConfigPropertyOidcClient protectedResourceServiceConfigPropertyOidcClient; + + @Inject + @RestClient + ProtectedResourceServiceCustomProviderConfigPropOidcClient protectedResourceServiceCustomProviderConfigPropOidcClient; + + @GET + @Path("/annotation/user-name") + public String annotationUserName() { + return protectedResourceServiceAnnotationOidcClient.getUserName(); + } + + @GET + @Path("/config-property/user-name") + public String configPropertyUserName() { + return protectedResourceServiceConfigPropertyOidcClient.getUserName(); + } + + @GET + @Path("/custom-provider-config-property/user-name") + public String customProviderConfigPropertyUserName() { + return protectedResourceServiceCustomProviderConfigPropOidcClient.getUserName(); + } +} diff --git a/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResource.java b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResource.java new file mode 100644 index 0000000000000..af91c7906f4bb --- /dev/null +++ b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResource.java @@ -0,0 +1,24 @@ +package io.quarkus.oidc.client.reactive.filter; + +import java.security.Principal; + +import javax.annotation.security.RolesAllowed; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import io.quarkus.security.Authenticated; + +@Path("/protected") +@Authenticated +public class ProtectedResource { + + @Inject + Principal principal; + + @GET + @RolesAllowed("user") + public String principalName() { + return principal.getName(); + } +} diff --git a/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResourceServiceAnnotationOidcClient.java b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResourceServiceAnnotationOidcClient.java new file mode 100644 index 0000000000000..d765aa3b7ebb2 --- /dev/null +++ b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResourceServiceAnnotationOidcClient.java @@ -0,0 +1,17 @@ +package io.quarkus.oidc.client.reactive.filter; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.oidc.client.filter.OidcClientFilter; + +@OidcClientFilter("annotation") +@RegisterRestClient +@Path("/") +public interface ProtectedResourceServiceAnnotationOidcClient { + + @GET + String getUserName(); +} diff --git a/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResourceServiceConfigPropertyOidcClient.java b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResourceServiceConfigPropertyOidcClient.java new file mode 100644 index 0000000000000..98ef7a2ce58c3 --- /dev/null +++ b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResourceServiceConfigPropertyOidcClient.java @@ -0,0 +1,17 @@ +package io.quarkus.oidc.client.reactive.filter; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.oidc.client.filter.OidcClientFilter; + +@OidcClientFilter +@RegisterRestClient +@Path("/") +public interface ProtectedResourceServiceConfigPropertyOidcClient { + + @GET + String getUserName(); +} diff --git a/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResourceServiceCustomProviderConfigPropOidcClient.java b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResourceServiceCustomProviderConfigPropOidcClient.java new file mode 100644 index 0000000000000..d07bcc2c0a26c --- /dev/null +++ b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResourceServiceCustomProviderConfigPropOidcClient.java @@ -0,0 +1,16 @@ +package io.quarkus.oidc.client.reactive.filter; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterProvider(OidcClientRequestReactiveFilter.class) +@RegisterRestClient +@Path("/") +public interface ProtectedResourceServiceCustomProviderConfigPropOidcClient { + + @GET + String getUserName(); +} diff --git a/extensions/oidc-client-reactive-filter/deployment/src/test/resources/application-oidc-client-reactive-filter.properties b/extensions/oidc-client-reactive-filter/deployment/src/test/resources/application-oidc-client-reactive-filter.properties new file mode 100644 index 0000000000000..f894867de0919 --- /dev/null +++ b/extensions/oidc-client-reactive-filter/deployment/src/test/resources/application-oidc-client-reactive-filter.properties @@ -0,0 +1,24 @@ +quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.client-id=quarkus-service-app +quarkus.oidc.credentials.secret=secret + +quarkus.oidc-client-reactive-filter.client-name=config-property +quarkus.oidc-client.config-property.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.config-property.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.config-property.credentials.client-secret.value=${quarkus.oidc.credentials.secret} +quarkus.oidc-client.config-property.credentials.client-secret.method=POST +quarkus.oidc-client.config-property.grant.type=password +quarkus.oidc-client.config-property.grant-options.password.username=alice +quarkus.oidc-client.config-property.grant-options.password.password=alice + +quarkus.oidc-client.annotation.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.annotation.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.annotation.credentials.client-secret.value=${quarkus.oidc.credentials.secret} +quarkus.oidc-client.annotation.credentials.client-secret.method=POST +quarkus.oidc-client.annotation.grant.type=password +quarkus.oidc-client.annotation.grant-options.password.username=jdoe +quarkus.oidc-client.annotation.grant-options.password.password=jdoe + +io.quarkus.oidc.client.reactive.filter.ProtectedResourceServiceAnnotationOidcClient/mp-rest/url=http://localhost:8080/protected +io.quarkus.oidc.client.reactive.filter.ProtectedResourceServiceConfigPropertyOidcClient/mp-rest/url=http://localhost:8080/protected +io.quarkus.oidc.client.reactive.filter.ProtectedResourceServiceCustomProviderConfigPropOidcClient/mp-rest/url=http://localhost:8080/protected diff --git a/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/OidcClientRequestReactiveFilter.java b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/OidcClientRequestReactiveFilter.java index 41e1ed94fc158..ca61a0b88576e 100644 --- a/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/OidcClientRequestReactiveFilter.java +++ b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/OidcClientRequestReactiveFilter.java @@ -1,64 +1,22 @@ package io.quarkus.oidc.client.reactive.filter; import java.util.Optional; -import java.util.function.Consumer; import javax.annotation.Priority; +import javax.inject.Inject; import javax.ws.rs.Priorities; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.jboss.logging.Logger; -import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext; -import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestFilter; - -import io.quarkus.oidc.client.Tokens; -import io.quarkus.oidc.client.runtime.AbstractTokensProducer; -import io.quarkus.oidc.client.runtime.DisabledOidcClientException; -import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.oidc.client.reactive.filter.runtime.AbstractOidcClientRequestReactiveFilter; +import io.quarkus.oidc.client.reactive.filter.runtime.OidcClientReactiveFilterConfig; @Priority(Priorities.AUTHENTICATION) -public class OidcClientRequestReactiveFilter extends AbstractTokensProducer implements ResteasyReactiveClientRequestFilter { - private static final Logger LOG = Logger.getLogger(OidcClientRequestReactiveFilter.class); - private static final String BEARER_SCHEME_WITH_SPACE = OidcConstants.BEARER_SCHEME + " "; - - @ConfigProperty(name = "quarkus.oidc-client-reactive-filter.client-name") - Optional clientName; +public class OidcClientRequestReactiveFilter extends AbstractOidcClientRequestReactiveFilter { - protected void initTokens() { - if (earlyTokenAcquisition) { - LOG.debug("Token acquisition will be delayed until this filter is executed to avoid blocking an IO thread"); - } - } + @Inject + OidcClientReactiveFilterConfig config; @Override - public void filter(ResteasyReactiveClientRequestContext requestContext) { - requestContext.suspend(); - - super.getTokens().subscribe().with(new Consumer<>() { - @Override - public void accept(Tokens tokens) { - requestContext.getHeaders().putSingle(HttpHeaders.AUTHORIZATION, - BEARER_SCHEME_WITH_SPACE + tokens.getAccessToken()); - requestContext.resume(); - } - }, new Consumer<>() { - @Override - public void accept(Throwable t) { - if (t instanceof DisabledOidcClientException) { - LOG.debug("Client is disabled"); - requestContext.abortWith(Response.status(Response.Status.INTERNAL_SERVER_ERROR).build()); - } else { - LOG.debugf("Access token is not available, aborting the request with HTTP 401 error: %s", t.getMessage()); - requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); - } - requestContext.resume(); - } - }); - } - protected Optional clientId() { - return clientName; + return config.clientName; } } diff --git a/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/AbstractOidcClientRequestReactiveFilter.java b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/AbstractOidcClientRequestReactiveFilter.java new file mode 100644 index 0000000000000..641606308915f --- /dev/null +++ b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/AbstractOidcClientRequestReactiveFilter.java @@ -0,0 +1,54 @@ +package io.quarkus.oidc.client.reactive.filter.runtime; + +import java.util.function.Consumer; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext; +import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestFilter; + +import io.quarkus.oidc.client.Tokens; +import io.quarkus.oidc.client.runtime.AbstractTokensProducer; +import io.quarkus.oidc.client.runtime.DisabledOidcClientException; +import io.quarkus.oidc.common.runtime.OidcConstants; + +public class AbstractOidcClientRequestReactiveFilter extends AbstractTokensProducer + implements ResteasyReactiveClientRequestFilter { + private static final Logger LOG = Logger.getLogger(AbstractOidcClientRequestReactiveFilter.class); + private static final String BEARER_SCHEME_WITH_SPACE = OidcConstants.BEARER_SCHEME + " "; + + protected void initTokens() { + if (earlyTokenAcquisition) { + LOG.debug("Token acquisition will be delayed until this filter is executed to avoid blocking an IO thread"); + } + } + + @Override + public void filter(ResteasyReactiveClientRequestContext requestContext) { + requestContext.suspend(); + + super.getTokens().subscribe().with(new Consumer<>() { + @Override + public void accept(Tokens tokens) { + requestContext.getHeaders().putSingle(HttpHeaders.AUTHORIZATION, + BEARER_SCHEME_WITH_SPACE + tokens.getAccessToken()); + requestContext.resume(); + } + }, new Consumer<>() { + @Override + public void accept(Throwable t) { + if (t instanceof DisabledOidcClientException) { + LOG.debug("Client is disabled"); + requestContext.abortWith(Response.status(Response.Status.INTERNAL_SERVER_ERROR).build()); + } else { + LOG.debugf("Access token is not available, aborting the request with HTTP 401 error: %s", t.getMessage()); + requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); + } + requestContext.resume(); + } + }); + } + +} diff --git a/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/OidcClientReactiveFilterConfig.java b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/OidcClientReactiveFilterConfig.java index ed57c266b8287..c745f5dfdf1a3 100644 --- a/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/OidcClientReactiveFilterConfig.java +++ b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/OidcClientReactiveFilterConfig.java @@ -10,7 +10,8 @@ public class OidcClientReactiveFilterConfig { /** - * Name of the configured OidcClient. + * Name of the configured OidcClient used by the OidcClientRequestReactiveFilter. You can override this configuration + * for individual MP RestClients with the `io.quarkus.oidc.client.filter.OidcClientFilter` annotation. */ @ConfigItem public Optional clientName; diff --git a/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java index 839ab11187618..40f9b529f4c4b 100644 --- a/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java +++ b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java @@ -1,5 +1,7 @@ package io.quarkus.oidc.client.deployment; +import static io.quarkus.oidc.client.deployment.OidcClientFilterDeploymentHelper.sanitize; + import java.lang.reflect.Modifier; import java.util.Objects; import java.util.Optional; @@ -201,10 +203,6 @@ private String createNamedTokensProducerFor(ClassOutput classOutput, String targ return generatedName.replace('/', '.'); } - private String sanitize(String oidcClientName) { - return oidcClientName.replaceAll("\\W+", ""); - } - public static class IsEnabled implements BooleanSupplier { OidcClientBuildTimeConfig config; diff --git a/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientFilterDeploymentHelper.java b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientFilterDeploymentHelper.java new file mode 100644 index 0000000000000..4b442c4fa1709 --- /dev/null +++ b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientFilterDeploymentHelper.java @@ -0,0 +1,98 @@ +package io.quarkus.oidc.client.deployment; + +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.DotName; + +import io.quarkus.arc.deployment.GeneratedBeanBuildItem; +import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; +import io.quarkus.arc.processor.DotNames; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.oidc.client.runtime.AbstractTokensProducer; + +/** + * Helps generate Oidc request filter based on {@link AbstractTokensProducer}. + */ +public class OidcClientFilterDeploymentHelper { + + private final Map clientNameToGeneratedClass = new HashMap<>(); + private final Class baseClass; + private final ClassOutput classOutput; + private final String targetPackage; + + public OidcClientFilterDeploymentHelper(Class baseClass, BuildProducer generatedBean) { + this.baseClass = baseClass; + this.classOutput = new GeneratedBeanGizmoAdaptor(generatedBean); + this.targetPackage = DotNames + .internalPackageNameWithTrailingSlash(DotName.createSimple(baseClass.getName())); + } + + /** + * For {@code baseClass} Xyz creates tokens producer class like follows: + * + *

    +     * @Singleton
    +     * @Unremovable
    +     * public class Xyz_oidcClientName extends Xyz {
    +     *
    +     *     @Override
    +     *     protected Optional clientId() {
    +     *         return Optional.of("oidcClientName");
    +     *     }
    +     * }
    +     * 
    + */ + public String getOrCreateNamedTokensProducerFor(String oidcClientName) { + Objects.requireNonNull(oidcClientName); + // do not create class for same client twice + return clientNameToGeneratedClass.computeIfAbsent(oidcClientName, new Function() { + @Override + public String apply(String s) { + final String generatedName = targetPackage + baseClass.getSimpleName() + "_" + sanitize(oidcClientName); + + try (ClassCreator creator = ClassCreator.builder().classOutput(classOutput).className(generatedName) + .superClass(baseClass).build()) { + creator.addAnnotation(DotNames.SINGLETON.toString()); + creator.addAnnotation(DotNames.UNREMOVABLE.toString()); + + try (MethodCreator clientIdMethod = creator.getMethodCreator("clientId", Optional.class)) { + clientIdMethod.setModifiers(Modifier.PROTECTED); + + clientIdMethod.returnValue(clientIdMethod.invokeStaticMethod( + MethodDescriptor.ofMethod(Optional.class, "of", Optional.class, Object.class), + clientIdMethod.load(oidcClientName))); + } + } + + return generatedName.replace('/', '.'); + } + }); + } + + public DotName getOrCreateFilter(String oidcClientName) { + return DotName.createSimple(getOrCreateNamedTokensProducerFor(oidcClientName)); + } + + public static String getClientName(AnnotationInstance annotationInstance) { + final AnnotationValue annotationValue = annotationInstance.value(); + if (annotationValue != null && !annotationValue.asString().isEmpty()) { + return annotationValue.asString(); + } + return null; + } + + public static String sanitize(String oidcClientName) { + return oidcClientName.replaceAll("\\W+", ""); + } +} diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientFilter.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientFilter.java index 65574b10e6f00..6a8f632c16859 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientFilter.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientFilter.java @@ -10,4 +10,10 @@ @Retention(RetentionPolicy.RUNTIME) @Documented public @interface OidcClientFilter { + + /** + * @return name of the OIDC client that should be used to acquire the tokens. + */ + String value() default ""; + } diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/AbstractTokensProducer.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/AbstractTokensProducer.java index 784508e17bb54..f8c942e5d3070 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/AbstractTokensProducer.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/AbstractTokensProducer.java @@ -6,8 +6,6 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; -import org.eclipse.microprofile.config.inject.ConfigProperty; - import io.quarkus.arc.Arc; import io.quarkus.oidc.client.OidcClient; import io.quarkus.oidc.client.OidcClients; @@ -17,18 +15,26 @@ public abstract class AbstractTokensProducer { private OidcClient oidcClient; + protected boolean earlyTokenAcquisition = true; + @Inject - @ConfigProperty(name = "quarkus.oidc-client.early-tokens-acquisition") - public boolean earlyTokenAcquisition; + public OidcClientsConfig oidcClientsConfig; final TokensHelper tokensHelper = new TokensHelper(); @PostConstruct public void init() { - OidcClients oidcClients = Arc.container().instance(OidcClients.class).get(); Optional clientId = Objects.requireNonNull(clientId(), "clientId must not be null"); - oidcClient = clientId.isPresent() ? Objects.requireNonNull(oidcClients.getClient(clientId.get()), "Unknown client") - : oidcClients.getClient(); + OidcClients oidcClients = Arc.container().instance(OidcClients.class).get(); + if (clientId.isPresent()) { + // static named OidcClient + oidcClient = Objects.requireNonNull(oidcClients.getClient(clientId.get()), "Unknown client"); + earlyTokenAcquisition = oidcClientsConfig.namedClients.get(clientId.get()).earlyTokensAcquisition; + } else { + // default OidcClient + earlyTokenAcquisition = oidcClientsConfig.defaultClient.earlyTokensAcquisition; + oidcClient = oidcClients.getClient(); + } initTokens(); } diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java index f3fcf81a68dd6..433dbe70c1b88 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java @@ -14,7 +14,7 @@ public class OidcCommonConfig { * The base URL of the OpenID Connect (OIDC) server, for example, `https://host:port/auth`. * OIDC discovery endpoint will be called by default by appending a '.well-known/openid-configuration' path to this URL. * Note if you work with Keycloak OIDC server, make sure the base URL is in the following format: - * `https://host:port/auth/realms/{realm}` where `{realm}` has to be replaced by the name of the Keycloak realm. + * `https://host:port/realms/{realm}` where `{realm}` has to be replaced by the name of the Keycloak realm. */ @ConfigItem public Optional authServerUrl = Optional.empty(); diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java index c33468cb748c2..3d5f66920fcc6 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java @@ -35,7 +35,7 @@ public class DevServicesConfig { * string. * Set 'quarkus.keycloak.devservices.keycloak-x-image' to override this check. */ - @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:19.0.3") + @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:20.0.1") public String imageName; /** diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java index c7c1674c8f21d..a9e4afbb5bbb5 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java @@ -266,7 +266,7 @@ private Map prepareConfiguration( String clientAuthServerBaseUrl = hostURL != null ? hostURL : internalURL; String clientAuthServerUrl = realmsURL(clientAuthServerBaseUrl, realmName); - boolean createDefaultRealm = realmReps.isEmpty() && capturedDevServicesConfiguration.createRealm; + boolean createDefaultRealm = (realmReps == null || realmReps.isEmpty()) && capturedDevServicesConfiguration.createRealm; String oidcClientId = getOidcClientId(createDefaultRealm); String oidcClientSecret = getOidcClientSecret(createDefaultRealm); @@ -283,9 +283,11 @@ private Map prepareConfiguration( createDefaultRealm(client, adminToken, clientAuthServerBaseUrl, users, oidcClientId, oidcClientSecret, errors); realmNames.add(realmName); } else { - for (RealmRepresentation realmRep : realmReps) { - createRealm(client, adminToken, clientAuthServerBaseUrl, realmRep, errors); - realmNames.add(realmRep.getRealm()); + if (realmReps != null) { + for (RealmRepresentation realmRep : realmReps) { + createRealm(client, adminToken, clientAuthServerBaseUrl, realmRep, errors); + realmNames.add(realmRep.getRealm()); + } } } } finally { diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/OpaqueTokenVerificationWithUserInfoValidationTest.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/OpaqueTokenVerificationWithUserInfoValidationTest.java new file mode 100644 index 0000000000000..b6eaff8715bec --- /dev/null +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/OpaqueTokenVerificationWithUserInfoValidationTest.java @@ -0,0 +1,44 @@ +package io.quarkus.oidc.test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class OpaqueTokenVerificationWithUserInfoValidationTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset("quarkus.oidc.token.verify-access-token-with-user-info=true\n"), + "application.properties")) + .assertException(t -> { + Throwable e = t; + ConfigurationException te = null; + while (e != null) { + if (e instanceof ConfigurationException) { + te = (ConfigurationException) e; + break; + } + e = e.getCause(); + } + assertNotNull(te); + // assert UserInfo is required + assertTrue( + te.getMessage() + .contains("UserInfo is not required but 'verifyAccessTokenWithUserInfo' is enabled"), + te.getMessage()); + }); + + @Test + public void test() { + Assertions.fail(); + } + +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index b8a69106aea5d..15b68c43cf718 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -7,7 +7,6 @@ import java.util.Map; import java.util.Optional; import java.util.OptionalInt; -import java.util.Set; import io.quarkus.oidc.common.runtime.OidcCommonConfig; import io.quarkus.oidc.common.runtime.OidcConstants; @@ -1189,6 +1188,16 @@ public static Token fromAudience(String... audience) { @ConfigItem(defaultValue = "true") public boolean allowOpaqueTokenIntrospection = true; + /** + * Indirectly verify that the opaque (binary) access token is valid by using it to request UserInfo. + * Opaque access token is considered valid if the provider accepted this token and returned a valid UserInfo. + * You should only enable this option if the opaque access tokens have to be accepted but OpenId Connect + * provider does not have a token introspection endpoint. + * This property will have no effect when JWT tokens have to be verified. + */ + @ConfigItem(defaultValue = "false") + public boolean verifyAccessTokenWithUserInfo; + public Optional getIssuer() { return issuer; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index cb5d345f75f97..c0cc270afa591 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -1098,7 +1098,7 @@ private Uni buildLogoutRedirectUriUni(RoutingContext context, TenantConfig @Override public Void apply(Void t) { String logoutUri = buildLogoutRedirectUri(configContext, idToken, context); - LOG.debugf("Logout uri: %s"); + LOG.debugf("Logout uri: %s", logoutUri); throw new AuthenticationRedirectException(logoutUri); } }); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java index 91a9ece130067..1046d54d2c910 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java @@ -99,38 +99,40 @@ private Uni validateAllTokensWithOidcServer(RoutingContext ver TokenAuthenticationRequest request, TenantConfigContext resolvedContext) { - Uni codeAccessTokenUni = verifyCodeFlowAccessTokenUni(vertxContext, request, resolvedContext); + Uni userInfo = resolvedContext.oidcConfig.authentication.isUserInfoRequired().orElse(false) + ? getUserInfoUni(vertxContext, request, resolvedContext) + : NULL_USER_INFO_UNI; - return codeAccessTokenUni.onItemOrFailure().transformToUni( - new BiFunction>() { + return userInfo.onItemOrFailure().transformToUni( + new BiFunction>() { @Override - public Uni apply(TokenVerificationResult codeAccessToken, Throwable t) { + public Uni apply(UserInfo userInfo, Throwable t) { if (t != null) { return Uni.createFrom().failure(new AuthenticationFailedException(t)); } - return validateTokenWithOidcServer(vertxContext, request, resolvedContext, codeAccessToken); + return validateTokenWithOidcServer(vertxContext, request, resolvedContext, userInfo); } }); } private Uni validateTokenWithOidcServer(RoutingContext vertxContext, TokenAuthenticationRequest request, - TenantConfigContext resolvedContext, TokenVerificationResult codeAccessTokenResult) { + TenantConfigContext resolvedContext, UserInfo userInfo) { - if (codeAccessTokenResult != null) { - vertxContext.put(CODE_ACCESS_TOKEN_RESULT, codeAccessTokenResult); - } - - Uni userInfo = resolvedContext.oidcConfig.authentication.isUserInfoRequired().orElse(false) - ? getUserInfoUni(vertxContext, request, resolvedContext) - : NULL_USER_INFO_UNI; + Uni codeAccessTokenUni = verifyCodeFlowAccessTokenUni(vertxContext, request, resolvedContext, + userInfo); - return userInfo.onItemOrFailure().transformToUni( - new BiFunction>() { + return codeAccessTokenUni.onItemOrFailure().transformToUni( + new BiFunction>() { @Override - public Uni apply(UserInfo userInfo, Throwable t) { + public Uni apply(TokenVerificationResult codeAccessToken, Throwable t) { if (t != null) { return Uni.createFrom().failure(new AuthenticationFailedException(t)); } + + if (codeAccessToken != null) { + vertxContext.put(CODE_ACCESS_TOKEN_RESULT, codeAccessToken); + } + return createSecurityIdentityWithOidcServer(vertxContext, request, resolvedContext, userInfo); } }); @@ -148,7 +150,7 @@ private Uni createSecurityIdentityWithOidcServer(RoutingContex tokenUni = verifySelfSignedTokenUni(resolvedContext, request.getToken().getToken()); } } else { - tokenUni = verifyTokenUni(resolvedContext, request.getToken().getToken()); + tokenUni = verifyTokenUni(resolvedContext, request.getToken().getToken(), userInfo); } return tokenUni.onItemOrFailure() @@ -194,28 +196,41 @@ public Uni apply(TokenVerificationResult result, Throwable t) QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(); builder.addCredential(tokenCred); OidcUtils.setSecurityIdentityUserInfo(builder, userInfo); - OidcUtils.setSecurityIdentityIntrospection(builder, result.introspectionResult); OidcUtils.setSecurityIdentityConfigMetadata(builder, resolvedContext); - String principalMember = ""; - if (result.introspectionResult.contains(OidcConstants.INTROSPECTION_TOKEN_USERNAME)) { - principalMember = OidcConstants.INTROSPECTION_TOKEN_USERNAME; - } else if (result.introspectionResult.contains(OidcConstants.INTROSPECTION_TOKEN_SUB)) { - // fallback to "sub", if "username" is not present - principalMember = OidcConstants.INTROSPECTION_TOKEN_SUB; + final String userName; + if (result.introspectionResult == null) { + if (resolvedContext.oidcConfig.token.allowOpaqueTokenIntrospection && + resolvedContext.oidcConfig.token.verifyAccessTokenWithUserInfo) { + userName = ""; + } else { + // we don't expect this to ever happen + LOG.debug("Illegal state - token introspection result is not available."); + return Uni.createFrom().failure(new AuthenticationFailedException()); + } + } else { + OidcUtils.setSecurityIdentityIntrospection(builder, result.introspectionResult); + String principalMember = ""; + if (result.introspectionResult.contains(OidcConstants.INTROSPECTION_TOKEN_USERNAME)) { + principalMember = OidcConstants.INTROSPECTION_TOKEN_USERNAME; + } else if (result.introspectionResult.contains(OidcConstants.INTROSPECTION_TOKEN_SUB)) { + // fallback to "sub", if "username" is not present + principalMember = OidcConstants.INTROSPECTION_TOKEN_SUB; + } + userName = principalMember.isEmpty() ? "" + : result.introspectionResult.getString(principalMember); + if (result.introspectionResult.contains(OidcConstants.TOKEN_SCOPE)) { + for (String role : result.introspectionResult.getString(OidcConstants.TOKEN_SCOPE) + .split(" ")) { + builder.addRole(role.trim()); + } + } } - final String userName = principalMember.isEmpty() ? "" - : result.introspectionResult.getString(principalMember); builder.setPrincipal(new Principal() { @Override public String getName() { return userName; } }); - if (result.introspectionResult.contains(OidcConstants.TOKEN_SCOPE)) { - for (String role : result.introspectionResult.getString(OidcConstants.TOKEN_SCOPE).split(" ")) { - builder.addRole(role.trim()); - } - } if (userInfo != null) { OidcUtils.setSecurityIdentityRoles(builder, resolvedContext.oidcConfig, new JsonObject(userInfo.getJsonObject().toString())); @@ -271,23 +286,34 @@ private static JsonObject getRolesJson(RoutingContext vertxContext, TenantConfig private Uni verifyCodeFlowAccessTokenUni(RoutingContext vertxContext, TokenAuthenticationRequest request, - TenantConfigContext resolvedContext) { + TenantConfigContext resolvedContext, UserInfo userInfo) { if (request.getToken() instanceof IdTokenCredential && (resolvedContext.oidcConfig.authentication.verifyAccessToken || resolvedContext.oidcConfig.roles.source.orElse(null) == Source.accesstoken)) { final String codeAccessToken = (String) vertxContext.get(OidcConstants.ACCESS_TOKEN_VALUE); - return verifyTokenUni(resolvedContext, codeAccessToken); + return verifyTokenUni(resolvedContext, codeAccessToken, userInfo); } else { return NULL_CODE_ACCESS_TOKEN_UNI; } } - private Uni verifyTokenUni(TenantConfigContext resolvedContext, String token) { + private Uni verifyTokenUni(TenantConfigContext resolvedContext, String token, UserInfo userInfo) { if (OidcUtils.isOpaqueToken(token)) { if (!resolvedContext.oidcConfig.token.allowOpaqueTokenIntrospection) { LOG.debug("Token is opaque but the opaque token introspection is not allowed"); throw new AuthenticationFailedException(); } + // verify opaque access token with UserInfo if enabled and introspection URI is absent + if (resolvedContext.oidcConfig.token.verifyAccessTokenWithUserInfo + && resolvedContext.provider.getMetadata().getIntrospectionUri() == null) { + if (userInfo == null) { + return Uni.createFrom().failure( + new AuthenticationFailedException("Opaque access token verification failed as user info is null.")); + } else { + // valid token verification result + return Uni.createFrom().item(new TokenVerificationResult(null, null)); + } + } LOG.debug("Starting the opaque token introspection"); return introspectTokenUni(resolvedContext, token); } else if (resolvedContext.provider.getMetadata().getJsonWebKeySetUri() == null diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java index d0db470486f0d..1d809ccd5cb4b 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java @@ -214,6 +214,23 @@ private Uni createTenantContext(Vertx vertx, OidcTenantConf } } + if (oidcConfig.token.verifyAccessTokenWithUserInfo) { + if (!oidcConfig.authentication.isUserInfoRequired().orElse(false)) { + throw new ConfigurationException( + "UserInfo is not required but 'verifyAccessTokenWithUserInfo' is enabled"); + } + if (!oidcConfig.isDiscoveryEnabled().orElse(true)) { + if (oidcConfig.userInfoPath.isEmpty()) { + throw new ConfigurationException( + "UserInfo path is missing but 'verifyAccessTokenWithUserInfo' is enabled"); + } + if (oidcConfig.introspectionPath.isPresent()) { + throw new ConfigurationException( + "Introspection path is configured and 'verifyAccessTokenWithUserInfo' is enabled, these options are mutually exclusive"); + } + } + } + return createOidcProvider(oidcConfig, tlsConfig, vertx) .onItem().transform(p -> new TenantConfigContext(p, oidcConfig)); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java index 014d4513ce0b4..4e1e2190307a0 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java @@ -50,8 +50,8 @@ private static SecretKey createPkceSecretKey(OidcTenantConfig config) { if (pkceSecret == null) { throw new RuntimeException("Secret key for encrypting PKCE code verifier is missing"); } - if (pkceSecret.length() < 32) { - throw new RuntimeException("Secret key for encrypting PKCE code verifier must be at least 32 characters long"); + if (pkceSecret.length() != 32) { + throw new RuntimeException("Secret key for encrypting PKCE code verifier must be 32 characters long"); } return KeyUtils.createSecretKeyFromSecret(pkceSecret); } @@ -65,8 +65,8 @@ private static SecretKey createTokenEncSecretKey(OidcTenantConfig config) { if (encSecret == null) { throw new RuntimeException("Secret key for encrypting tokens is missing"); } - if (encSecret.length() < 32) { - throw new RuntimeException("Secret key for encrypting tokens must be at least 32 characters long"); + if (encSecret.length() != 32) { + throw new RuntimeException("Secret key for encrypting tokens must be 32 characters long"); } return KeyUtils.createSecretKeyFromSecret(encSecret); } diff --git a/extensions/opentelemetry/deployment/pom.xml b/extensions/opentelemetry/deployment/pom.xml index c4b1382e69a1a..69fc0a0f28833 100644 --- a/extensions/opentelemetry/deployment/pom.xml +++ b/extensions/opentelemetry/deployment/pom.xml @@ -45,6 +45,14 @@ io.quarkus quarkus-grpc-common-deployment + + io.quarkus + quarkus-resteasy-common-spi + + + io.quarkus + quarkus-resteasy-reactive-spi-deployment + @@ -114,20 +122,19 @@ test - - io.opentelemetry - opentelemetry-extension-aws + io.opentelemetry.contrib + opentelemetry-aws-xray-propagator test - io.opentelemetry - opentelemetry-sdk-extension-aws + io.opentelemetry.contrib + opentelemetry-aws-resources test - io.opentelemetry - opentelemetry-sdk-extension-resources + io.opentelemetry.contrib + opentelemetry-aws-xray test diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java index f22257e9c1d7b..9cda1e84141f3 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java @@ -19,14 +19,11 @@ import io.quarkus.arc.deployment.InterceptorBindingRegistrarBuildItem; import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.arc.processor.InterceptorBindingRegistrar; -import io.quarkus.deployment.Capabilities; -import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; -import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; @@ -36,8 +33,7 @@ import io.quarkus.opentelemetry.runtime.QuarkusContextStorage; import io.quarkus.opentelemetry.runtime.config.OpenTelemetryConfig; import io.quarkus.opentelemetry.runtime.tracing.cdi.WithSpanInterceptor; -import io.quarkus.opentelemetry.runtime.tracing.intrumentation.reactivemessaging.ReactiveMessagingTracingDecorator; -import io.quarkus.opentelemetry.runtime.tracing.intrumentation.restclient.OpenTelemetryClientFilter; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.InstrumentationRecorder; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.RuntimeValue; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; @@ -130,31 +126,12 @@ public void transform(TransformationContext context) { })); } - @BuildStep - void registerRestClientClassicProvider( - Capabilities capabilities, - BuildProducer additionalIndexed, - BuildProducer additionalBeans) { - if (capabilities.isPresent(Capability.REST_CLIENT) && capabilities.isMissing(Capability.REST_CLIENT_REACTIVE)) { - additionalIndexed.produce(new AdditionalIndexedClassesBuildItem(OpenTelemetryClientFilter.class.getName())); - additionalBeans.produce(new AdditionalBeanBuildItem(OpenTelemetryClientFilter.class)); - } - } - - @BuildStep - void registerReactiveMessagingMessageDecorator( - Capabilities capabilities, - BuildProducer additionalBeans) { - if (capabilities.isPresent(Capability.SMALLRYE_REACTIVE_MESSAGING)) { - additionalBeans.produce(new AdditionalBeanBuildItem(ReactiveMessagingTracingDecorator.class)); - } - } - @BuildStep @Record(ExecutionTime.STATIC_INIT) void createOpenTelemetry( OpenTelemetryConfig openTelemetryConfig, OpenTelemetryRecorder recorder, + InstrumentationRecorder instrumentationRecorder, Optional tracerProviderBuildItem, LaunchModeBuildItem launchMode) { @@ -166,6 +143,11 @@ void createOpenTelemetry( .orElse(null); recorder.createOpenTelemetry(tracerProvider, openTelemetryConfig); recorder.eagerlyCreateContextStorage(); + + // just checking for live reload would bypass the OpenTelemetryDevModeTest + if (launchMode.getLaunchMode() == LaunchMode.DEVELOPMENT) { + instrumentationRecorder.setTracerDevMode(instrumentationRecorder.createTracers()); + } } @BuildStep diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java index 98e4957b2dd69..647561f9e4e8f 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java @@ -1,18 +1,12 @@ package io.quarkus.opentelemetry.deployment.tracing; -import static io.quarkus.bootstrap.classloading.QuarkusClassLoader.isClassPresentAtRuntime; -import static javax.interceptor.Interceptor.Priority.LIBRARY_AFTER; - import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.function.BooleanSupplier; -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.DotName; @@ -43,10 +37,7 @@ import io.quarkus.opentelemetry.runtime.config.TracerRuntimeConfig; import io.quarkus.opentelemetry.runtime.tracing.TracerRecorder; import io.quarkus.opentelemetry.runtime.tracing.cdi.TracerProducer; -import io.quarkus.opentelemetry.runtime.tracing.intrumentation.grpc.GrpcTracingClientInterceptor; -import io.quarkus.opentelemetry.runtime.tracing.intrumentation.grpc.GrpcTracingServerInterceptor; import io.quarkus.runtime.configuration.ConfigurationException; -import io.quarkus.vertx.core.deployment.VertxOptionsConsumerBuildItem; import io.quarkus.vertx.http.deployment.spi.FrameworkEndpointsBuildItem; import io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem; @@ -58,38 +49,6 @@ public class TracerProcessor { private static final DotName SPAN_EXPORTER = DotName.createSimple(SpanExporter.class.getName()); private static final DotName SPAN_PROCESSOR = DotName.createSimple(SpanProcessor.class.getName()); - static class MetricsExtensionAvailable implements BooleanSupplier { - private static final boolean IS_MICROMETER_EXTENSION_AVAILABLE = isClassPresentAtRuntime( - "io.quarkus.micrometer.runtime.binder.vertx.VertxHttpServerMetrics"); - - @Override - public boolean getAsBoolean() { - Config config = ConfigProvider.getConfig(); - if (IS_MICROMETER_EXTENSION_AVAILABLE) { - if (config.getOptionalValue("quarkus.micrometer.enabled", Boolean.class).orElse(true)) { - Optional httpServerEnabled = config - .getOptionalValue("quarkus.micrometer.binder.http-server.enabled", Boolean.class); - if (httpServerEnabled.isPresent()) { - return httpServerEnabled.get(); - } else { - return config.getOptionalValue("quarkus.micrometer.binder-enabled-default", Boolean.class).orElse(true); - } - } - } - return false; - } - } - - static class GrpcExtensionAvailable implements BooleanSupplier { - private static final boolean IS_GRPC_EXTENSION_AVAILABLE = isClassPresentAtRuntime( - "io.quarkus.grpc.runtime.GrpcServerRecorder"); - - @Override - public boolean getAsBoolean() { - return IS_GRPC_EXTENSION_AVAILABLE; - } - } - @BuildStep UnremovableBeanBuildItem ensureProducersAreRetained( CombinedIndexBuildItem indexBuildItem, @@ -179,24 +138,6 @@ void dropNames( dropStaticResources.produce(new DropStaticResourcesBuildItem(resources)); } - @BuildStep(onlyIf = GrpcExtensionAvailable.class) - void grpcTracers(BuildProducer additionalBeans) { - additionalBeans.produce(new AdditionalBeanBuildItem(GrpcTracingServerInterceptor.class)); - additionalBeans.produce(new AdditionalBeanBuildItem(GrpcTracingClientInterceptor.class)); - } - - @BuildStep - @Record(ExecutionTime.STATIC_INIT) - VertxOptionsConsumerBuildItem vertxTracingOptions(TracerRecorder recorder) { - return new VertxOptionsConsumerBuildItem(recorder.getVertxTracingOptions(), LIBRARY_AFTER); - } - - @BuildStep(onlyIfNot = MetricsExtensionAvailable.class) - @Record(ExecutionTime.STATIC_INIT) - VertxOptionsConsumerBuildItem vertxTracingMetricsOptions(TracerRecorder recorder) { - return new VertxOptionsConsumerBuildItem(recorder.getVertxTracingMetricsOptions(), LIBRARY_AFTER + 1); - } - @BuildStep @Record(ExecutionTime.STATIC_INIT) TracerProviderBuildItem createTracerProvider( diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java new file mode 100644 index 0000000000000..7757673b13866 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java @@ -0,0 +1,153 @@ +package io.quarkus.opentelemetry.deployment.tracing.instrumentation; + +import static io.quarkus.bootstrap.classloading.QuarkusClassLoader.isClassPresentAtRuntime; +import static javax.interceptor.Interceptor.Priority.LIBRARY_AFTER; + +import java.util.Optional; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.opentelemetry.deployment.tracing.TracerEnabled; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.InstrumentationRecorder; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.grpc.GrpcTracingClientInterceptor; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.grpc.GrpcTracingServerInterceptor; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.reactivemessaging.ReactiveMessagingTracingDecorator; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.restclient.OpenTelemetryClientFilter; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy.OpenTelemetryClassicServerFilter; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy.OpenTelemetryReactiveServerFilter; +import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.reactive.spi.ContainerRequestFilterBuildItem; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.vertx.core.deployment.VertxOptionsConsumerBuildItem; +import io.vertx.core.VertxOptions; + +@BuildSteps(onlyIf = TracerEnabled.class) +public class InstrumentationProcessor { + static class MetricsExtensionAvailable implements BooleanSupplier { + private static final boolean IS_MICROMETER_EXTENSION_AVAILABLE = isClassPresentAtRuntime( + "io.quarkus.micrometer.runtime.binder.vertx.VertxHttpServerMetrics"); + + @Override + public boolean getAsBoolean() { + Config config = ConfigProvider.getConfig(); + if (IS_MICROMETER_EXTENSION_AVAILABLE) { + if (config.getOptionalValue("quarkus.micrometer.enabled", Boolean.class).orElse(true)) { + Optional httpServerEnabled = config + .getOptionalValue("quarkus.micrometer.binder.http-server.enabled", Boolean.class); + if (httpServerEnabled.isPresent()) { + return httpServerEnabled.get(); + } else { + return config.getOptionalValue("quarkus.micrometer.binder-enabled-default", Boolean.class).orElse(true); + } + } + } + return false; + } + } + + static class GrpcExtensionAvailable implements BooleanSupplier { + private static final boolean IS_GRPC_EXTENSION_AVAILABLE = isClassPresentAtRuntime( + "io.quarkus.grpc.runtime.GrpcServerRecorder"); + + @Override + public boolean getAsBoolean() { + return IS_GRPC_EXTENSION_AVAILABLE; + } + } + + @BuildStep(onlyIf = GrpcExtensionAvailable.class) + void grpcTracers(BuildProducer additionalBeans) { + additionalBeans.produce(new AdditionalBeanBuildItem(GrpcTracingServerInterceptor.class)); + additionalBeans.produce(new AdditionalBeanBuildItem(GrpcTracingClientInterceptor.class)); + } + + @BuildStep + void registerRestClientClassicProvider( + Capabilities capabilities, + BuildProducer additionalIndexed, + BuildProducer additionalBeans) { + if (capabilities.isPresent(Capability.REST_CLIENT) && capabilities.isMissing(Capability.REST_CLIENT_REACTIVE)) { + additionalIndexed.produce(new AdditionalIndexedClassesBuildItem(OpenTelemetryClientFilter.class.getName())); + additionalBeans.produce(new AdditionalBeanBuildItem(OpenTelemetryClientFilter.class)); + } + } + + @BuildStep + void registerReactiveMessagingMessageDecorator( + Capabilities capabilities, + BuildProducer additionalBeans) { + if (capabilities.isPresent(Capability.SMALLRYE_REACTIVE_MESSAGING)) { + additionalBeans.produce(new AdditionalBeanBuildItem(ReactiveMessagingTracingDecorator.class)); + } + } + + @BuildStep(onlyIfNot = MetricsExtensionAvailable.class) + @Record(ExecutionTime.STATIC_INIT) + VertxOptionsConsumerBuildItem vertxTracingMetricsOptions(InstrumentationRecorder recorder) { + return new VertxOptionsConsumerBuildItem(recorder.getVertxTracingMetricsOptions(), LIBRARY_AFTER + 1); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + VertxOptionsConsumerBuildItem vertxTracingOptions(InstrumentationRecorder recorder, + LaunchModeBuildItem launchMode) { + Consumer vertxTracingOptions; + if (launchMode.getLaunchMode() == LaunchMode.DEVELOPMENT) { + // tracers are set in the OpenTelemetryProcessor + vertxTracingOptions = recorder.getVertxTracingOptionsDevMode(); + } else { + vertxTracingOptions = recorder.getVertxTracingOptionsProd(recorder.createTracers()); + } + return new VertxOptionsConsumerBuildItem( + vertxTracingOptions, + LIBRARY_AFTER); + } + + // RESTEasy and Vert.x web + @BuildStep + void registerResteasyClassicAndOrResteasyReactiveProvider( + Capabilities capabilities, + BuildProducer resteasyJaxrsProviderBuildItemBuildProducer) { + + boolean isResteasyClassicAvailable = capabilities.isPresent(Capability.RESTEASY); + + if (!isResteasyClassicAvailable) { + // if RestEasy is not available then no need to continue + return; + } + + resteasyJaxrsProviderBuildItemBuildProducer + .produce(new ResteasyJaxrsProviderBuildItem(OpenTelemetryClassicServerFilter.class.getName())); + } + + @BuildStep + void registerResteasyReactiveProvider( + Capabilities capabilities, + BuildProducer containerRequestFilterBuildItemBuildProducer) { + + boolean isResteasyReactiveAvailable = capabilities.isPresent(Capability.RESTEASY_REACTIVE); + + if (isResteasyReactiveAvailable) { + // if RestEasy is not available then no need to continue + return; + } + + containerRequestFilterBuildItemBuildProducer + .produce(new ContainerRequestFilterBuildItem.Builder(OpenTelemetryReactiveServerFilter.class.getName()) + .build()); + } + +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDILegacyTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDILegacyTest.java index 1e2f344aa4098..00107904f049f 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDILegacyTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDILegacyTest.java @@ -3,6 +3,7 @@ import static io.opentelemetry.api.trace.SpanKind.INTERNAL; import static io.opentelemetry.api.trace.SpanKind.SERVER; import static io.quarkus.opentelemetry.deployment.common.TestSpanExporter.getSpanByKindAndParentId; +import static io.quarkus.opentelemetry.deployment.common.TestUtil.assertStringAttribute; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -22,7 +23,9 @@ import io.opentelemetry.extension.annotations.WithSpan; import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; +import io.quarkus.opentelemetry.deployment.common.TestUtil; import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; @@ -31,6 +34,7 @@ public class OpenTelemetryHttpCDILegacyTest { static final QuarkusUnitTest TEST = new QuarkusUnitTest() .setArchiveProducer( () -> ShrinkWrap.create(JavaArchive.class) + .addClass(TestUtil.class) .addClass(HelloResource.class) .addClass(HelloBean.class) .addClass(TestSpanExporter.class)); @@ -55,6 +59,10 @@ void telemetry() { SpanData server = getSpanByKindAndParentId(spans, SERVER, "0000000000000000"); assertEquals("/hello", server.getName()); assertEquals(SERVER, server.getKind()); + // verify that OpenTelemetryServerFilter took place + assertStringAttribute(server, SemanticAttributes.CODE_NAMESPACE, + "io.quarkus.opentelemetry.deployment.OpenTelemetryHttpCDILegacyTest$HelloResource"); + assertStringAttribute(server, SemanticAttributes.CODE_FUNCTION, "hello"); SpanData internal = getSpanByKindAndParentId(spans, INTERNAL, server.getSpanId()); assertEquals("HelloBean.hello", internal.getName()); diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDITest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDITest.java index c235f70374751..addd1335ef8c3 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDITest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDITest.java @@ -3,6 +3,7 @@ import static io.opentelemetry.api.trace.SpanKind.INTERNAL; import static io.opentelemetry.api.trace.SpanKind.SERVER; import static io.quarkus.opentelemetry.deployment.common.TestSpanExporter.getSpanByKindAndParentId; +import static io.quarkus.opentelemetry.deployment.common.TestUtil.assertStringAttribute; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -22,7 +23,9 @@ import io.opentelemetry.instrumentation.annotations.WithSpan; import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; +import io.quarkus.opentelemetry.deployment.common.TestUtil; import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; @@ -31,6 +34,7 @@ public class OpenTelemetryHttpCDITest { static final QuarkusUnitTest TEST = new QuarkusUnitTest() .setArchiveProducer( () -> ShrinkWrap.create(JavaArchive.class) + .addClass(TestUtil.class) .addClass(HelloResource.class) .addClass(HelloBean.class) .addClass(TestSpanExporter.class)); @@ -54,6 +58,10 @@ void telemetry() { final SpanData server = getSpanByKindAndParentId(spans, SERVER, "0000000000000000"); assertEquals("/hello", server.getName()); + // verify that OpenTelemetryServerFilter took place + assertStringAttribute(server, SemanticAttributes.CODE_NAMESPACE, + "io.quarkus.opentelemetry.deployment.OpenTelemetryHttpCDITest$HelloResource"); + assertStringAttribute(server, SemanticAttributes.CODE_FUNCTION, "hello"); final SpanData internalFromBean = getSpanByKindAndParentId(spans, INTERNAL, server.getSpanId()); assertEquals("HelloBean.hello", internalFromBean.getName()); diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryIdGeneratorTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryIdGeneratorTest.java index b4aed4c387d3e..7df50931b3551 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryIdGeneratorTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryIdGeneratorTest.java @@ -13,7 +13,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.sdk.extension.aws.trace.AwsXrayIdGenerator; +import io.opentelemetry.contrib.awsxray.AwsXrayIdGenerator; import io.opentelemetry.sdk.trace.IdGenerator; import io.quarkus.opentelemetry.deployment.common.TestUtil; import io.quarkus.test.QuarkusUnitTest; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryPropagatorsTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryPropagatorsTest.java index 58949d12fa1e9..471a31d280260 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryPropagatorsTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryPropagatorsTest.java @@ -11,7 +11,7 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.propagation.TextMapPropagator; -import io.opentelemetry.extension.aws.AwsXrayPropagator; +import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator; import io.quarkus.opentelemetry.deployment.common.TestUtil; import io.quarkus.test.QuarkusUnitTest; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/TestUtil.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/TestUtil.java index 046a03854a138..c2c4911f50cc7 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/TestUtil.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/TestUtil.java @@ -1,15 +1,19 @@ package io.quarkus.opentelemetry.deployment.common; +import static org.junit.jupiter.api.Assertions.assertEquals; + import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.TracerProvider; import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.IdGenerator; import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.samplers.Sampler; import io.quarkus.arc.Unremovable; @@ -56,8 +60,12 @@ public static Sampler getSampler(OpenTelemetry openTelemetry) public static TextMapPropagator[] getTextMapPropagators(OpenTelemetry openTelemetry) throws NoSuchFieldException, IllegalAccessException { TextMapPropagator textMapPropagator = openTelemetry.getPropagators().getTextMapPropagator(); - Field privatePropagatorsField = textMapPropagator.getClass().getDeclaredField("textPropagators"); + Field privatePropagatorsField = textMapPropagator.getClass().getDeclaredField("textMapPropagators"); privatePropagatorsField.setAccessible(true); return (TextMapPropagator[]) privatePropagatorsField.get(textMapPropagator); } + + public static void assertStringAttribute(SpanData spanData, AttributeKey attributeKey, String expectedValue) { + assertEquals(expectedValue, spanData.getAttributes().get(attributeKey), "Attribute Key Named:" + attributeKey.getKey()); + } } diff --git a/extensions/opentelemetry/runtime/pom.xml b/extensions/opentelemetry/runtime/pom.xml index 85dbd8cf0fd0f..beeb587138cad 100644 --- a/extensions/opentelemetry/runtime/pom.xml +++ b/extensions/opentelemetry/runtime/pom.xml @@ -34,6 +34,11 @@ io.smallrye.common smallrye-common-vertx-context + + io.quarkus.resteasy.reactive + resteasy-reactive + provided + org.graalvm.nativeimage svm diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/OpenTelemetryConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/OpenTelemetryConfig.java index 1fe8bfa306b48..73f7c5fafb594 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/OpenTelemetryConfig.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/OpenTelemetryConfig.java @@ -27,6 +27,8 @@ public final class OpenTelemetryConfig { @ConfigItem(defaultValue = "tracecontext,baggage") public List propagators; - /** Build / static runtime config for tracer */ + /** + * Build / static runtime config for tracer + */ public TracerConfig tracer; } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java index 65b786efaa48e..1066280c6baf4 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; import javax.enterprise.inject.Any; import javax.enterprise.inject.Instance; @@ -21,31 +20,12 @@ import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; import io.quarkus.arc.Arc; import io.quarkus.opentelemetry.runtime.config.TracerRuntimeConfig; -import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.OpenTelemetryVertxMetricsFactory; -import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.OpenTelemetryVertxTracingFactory; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; -import io.vertx.core.VertxOptions; -import io.vertx.core.metrics.MetricsOptions; -import io.vertx.core.tracing.TracingOptions; @Recorder public class TracerRecorder { - /* STATIC INIT */ - public Consumer getVertxTracingOptions() { - TracingOptions tracingOptions = new TracingOptions() - .setFactory(new OpenTelemetryVertxTracingFactory()); - return vertxOptions -> vertxOptions.setTracingOptions(tracingOptions); - } - - public Consumer getVertxTracingMetricsOptions() { - MetricsOptions metricsOptions = new MetricsOptions() - .setEnabled(true) - .setFactory(new OpenTelemetryVertxMetricsFactory()); - return vertxOptions -> vertxOptions.setMetricsOptions(metricsOptions); - } - /* STATIC INIT */ public RuntimeValue createTracerProvider( String quarkusVersion, diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java new file mode 100644 index 0000000000000..3fa73c26955b0 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java @@ -0,0 +1,66 @@ +package io.quarkus.opentelemetry.runtime.tracing.intrumentation; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.EventBusInstrumenterVertxTracer; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.HttpInstrumenterVertxTracer; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.InstrumenterVertxTracer; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.OpenTelemetryVertxMetricsFactory; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.OpenTelemetryVertxTracer; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.OpenTelemetryVertxTracingDevModeFactory; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.OpenTelemetryVertxTracingFactory; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.SqlClientInstrumenterVertxTracer; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; +import io.vertx.core.VertxOptions; +import io.vertx.core.metrics.MetricsOptions; +import io.vertx.core.tracing.TracingOptions; + +@Recorder +public class InstrumentationRecorder { + + public static final OpenTelemetryVertxTracingDevModeFactory FACTORY = new OpenTelemetryVertxTracingDevModeFactory(); + + /* RUNTIME INIT */ + public RuntimeValue createTracers() { + OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); + List> instrumenterVertxTracers = new ArrayList<>(); + instrumenterVertxTracers.add(new HttpInstrumenterVertxTracer(openTelemetry)); + instrumenterVertxTracers.add(new EventBusInstrumenterVertxTracer(openTelemetry)); + // TODO - Selectively register this in the recorder if the SQL Client is available. + instrumenterVertxTracers.add(new SqlClientInstrumenterVertxTracer(openTelemetry)); + return new RuntimeValue<>(new OpenTelemetryVertxTracer(instrumenterVertxTracers)); + } + + /* RUNTIME INIT */ + public Consumer getVertxTracingOptionsProd( + RuntimeValue tracer) { + TracingOptions tracingOptions = new TracingOptions() + .setFactory(new OpenTelemetryVertxTracingFactory(tracer.getValue())); + return vertxOptions -> vertxOptions.setTracingOptions(tracingOptions); + } + + /* RUNTIME INIT */ + public Consumer getVertxTracingOptionsDevMode() { + TracingOptions tracingOptions = new TracingOptions() + .setFactory(FACTORY); + return vertxOptions -> vertxOptions.setTracingOptions(tracingOptions); + } + + /* RUNTIME INIT */ + public void setTracerDevMode(RuntimeValue tracer) { + FACTORY.getVertxTracerDelegator().setDelegate(tracer.getValue()); + } + + /* RUNTIME INIT */ + public Consumer getVertxTracingMetricsOptions() { + MetricsOptions metricsOptions = new MetricsOptions() + .setEnabled(true) + .setFactory(new OpenTelemetryVertxMetricsFactory()); + return vertxOptions -> vertxOptions.setMetricsOptions(metricsOptions); + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/OpenTelemetryClassicServerFilter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/OpenTelemetryClassicServerFilter.java new file mode 100644 index 0000000000000..1f09c75ff9533 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/OpenTelemetryClassicServerFilter.java @@ -0,0 +1,31 @@ +package io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy; + +import java.io.IOException; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Context; +import javax.ws.rs.ext.Provider; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; + +/** + * Handles RESTEasy Classic + */ +@Provider +public class OpenTelemetryClassicServerFilter implements ContainerRequestFilter { + + @Context + ResourceInfo resourceInfo; + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + Span localRootSpan = LocalRootSpan.current(); + + localRootSpan.setAttribute(SemanticAttributes.CODE_NAMESPACE, resourceInfo.getResourceClass().getName()); + localRootSpan.setAttribute(SemanticAttributes.CODE_FUNCTION, resourceInfo.getResourceMethod().getName()); + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/OpenTelemetryReactiveServerFilter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/OpenTelemetryReactiveServerFilter.java new file mode 100644 index 0000000000000..e60747a321edc --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/resteasy/OpenTelemetryReactiveServerFilter.java @@ -0,0 +1,32 @@ +package io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy; + +import java.io.IOException; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.Context; +import javax.ws.rs.ext.Provider; + +import org.jboss.resteasy.reactive.server.SimpleResourceInfo; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; + +/** + * Handles RESTEasy Reactive (via Vert.x) + */ +@Provider +public class OpenTelemetryReactiveServerFilter implements ContainerRequestFilter { + + @Context + SimpleResourceInfo resourceInfo; + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + Span localRootSpan = LocalRootSpan.current(); + + localRootSpan.setAttribute(SemanticAttributes.CODE_NAMESPACE, resourceInfo.getResourceClass().getName()); + localRootSpan.setAttribute(SemanticAttributes.CODE_FUNCTION, resourceInfo.getMethodName()); + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/HttpInstrumenterVertxTracer.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/HttpInstrumenterVertxTracer.java index 10abcd55de8f2..f06ded3420f07 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/HttpInstrumenterVertxTracer.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/HttpInstrumenterVertxTracer.java @@ -28,7 +28,6 @@ import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; import io.vertx.core.Context; import io.vertx.core.MultiMap; @@ -44,7 +43,7 @@ import io.vertx.core.spi.observability.HttpResponse; import io.vertx.core.spi.tracing.TagExtractor; -class HttpInstrumenterVertxTracer implements InstrumenterVertxTracer { +public class HttpInstrumenterVertxTracer implements InstrumenterVertxTracer { private final Instrumenter serverInstrumenter; private final Instrumenter clientInstrumenter; @@ -118,8 +117,8 @@ private static Instrumenter getServerInstrumenter(fin return serverBuilder .setSpanStatusExtractor(HttpSpanStatusExtractor.create(serverAttributesExtractor)) - .addAttributesExtractor(HttpServerAttributesExtractor.create(serverAttributesExtractor)) - .addAttributesExtractor(NetServerAttributesExtractor.create(new HttpServerNetAttributesGetter())) + .addAttributesExtractor( + HttpServerAttributesExtractor.create(serverAttributesExtractor, new HttpServerNetAttributesGetter())) .addAttributesExtractor(new AdditionalServerAttributesExtractor()) .addContextCustomizer(HttpRouteHolder.get()) .buildServerInstrumenter(new HttpRequestTextMapGetter()); diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/InstrumenterVertxTracer.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/InstrumenterVertxTracer.java index 6c9408872dcea..c1ec2a629ef98 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/InstrumenterVertxTracer.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/InstrumenterVertxTracer.java @@ -17,7 +17,7 @@ import io.vertx.core.tracing.TracingPolicy; @SuppressWarnings("unchecked") -interface InstrumenterVertxTracer extends VertxTracer { +public interface InstrumenterVertxTracer extends VertxTracer { @Override default SpanOperation receiveRequest( // The Vert.x context passed to use is already duplicated. diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/OpenTelemetryVertxTracingDevModeFactory.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/OpenTelemetryVertxTracingDevModeFactory.java new file mode 100644 index 0000000000000..a26fb8322aa83 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/OpenTelemetryVertxTracingDevModeFactory.java @@ -0,0 +1,84 @@ +package io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx; + +import java.util.function.BiConsumer; + +import io.vertx.core.Context; +import io.vertx.core.spi.VertxTracerFactory; +import io.vertx.core.spi.tracing.SpanKind; +import io.vertx.core.spi.tracing.TagExtractor; +import io.vertx.core.spi.tracing.VertxTracer; +import io.vertx.core.tracing.TracingOptions; +import io.vertx.core.tracing.TracingPolicy; + +public class OpenTelemetryVertxTracingDevModeFactory implements VertxTracerFactory { + private final Delegator vertxTracerDelegator = new Delegator(); + + public OpenTelemetryVertxTracingDevModeFactory() { + } + + public Delegator getVertxTracerDelegator() { + return vertxTracerDelegator; + } + + @Override + public VertxTracer tracer(final TracingOptions options) { + return vertxTracerDelegator; + } + + public static class Delegator implements VertxTracer { + private VertxTracer delegate; + + public VertxTracer getDelegate() { + return delegate; + } + + public Delegator setDelegate(final VertxTracer delegate) { + this.delegate = delegate; + return this; + } + + @Override + public Object receiveRequest( + final Context context, + final SpanKind kind, + final TracingPolicy policy, + final Object request, + final String operation, + final Iterable headers, + final TagExtractor tagExtractor) { + return delegate.receiveRequest(context, kind, policy, request, operation, headers, tagExtractor); + } + + @Override + public void sendResponse( + final Context context, + final Object response, + final Object payload, + final Throwable failure, + final TagExtractor tagExtractor) { + delegate.sendResponse(context, response, payload, failure, tagExtractor); + } + + @Override + public Object sendRequest( + final Context context, + final SpanKind kind, + final TracingPolicy policy, + final Object request, + final String operation, + final BiConsumer headers, + final TagExtractor tagExtractor) { + return delegate.sendRequest(context, kind, policy, request, operation, headers, tagExtractor); + } + + @Override + public void receiveResponse( + final Context context, + final Object response, + final Object payload, + final Throwable failure, + final TagExtractor tagExtractor) { + delegate.receiveResponse(context, response, payload, failure, tagExtractor); + } + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/OpenTelemetryVertxTracingFactory.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/OpenTelemetryVertxTracingFactory.java index 43f2835f61ae2..c90e8a5a3208a 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/OpenTelemetryVertxTracingFactory.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/OpenTelemetryVertxTracingFactory.java @@ -1,23 +1,18 @@ package io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx; -import java.util.ArrayList; -import java.util.List; - -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.OpenTelemetry; import io.vertx.core.spi.VertxTracerFactory; import io.vertx.core.spi.tracing.VertxTracer; import io.vertx.core.tracing.TracingOptions; public class OpenTelemetryVertxTracingFactory implements VertxTracerFactory { + private final OpenTelemetryVertxTracer openTelemetryVertxTracer; + + public OpenTelemetryVertxTracingFactory(OpenTelemetryVertxTracer openTelemetryVertxTracer) { + this.openTelemetryVertxTracer = openTelemetryVertxTracer; + } + @Override public VertxTracer tracer(final TracingOptions options) { - OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); - List> instrumenterVertxTracers = new ArrayList<>(); - instrumenterVertxTracers.add(new HttpInstrumenterVertxTracer(openTelemetry)); - instrumenterVertxTracers.add(new EventBusInstrumenterVertxTracer(openTelemetry)); - // TODO - Selectively register this in the recorder if the SQL Client is available. - instrumenterVertxTracers.add(new SqlClientInstrumenterVertxTracer(openTelemetry)); - return new OpenTelemetryVertxTracer(instrumenterVertxTracers); + return openTelemetryVertxTracer; } } diff --git a/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java b/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java index 70bfb1a803373..1f4dbad0e363f 100644 --- a/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java +++ b/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java @@ -82,7 +82,7 @@ public CommonPanacheQueryImpl project(Class type) { throw new PanacheQueryException("Unable to perform a projection on a named query"); } - String lowerCasedTrimmedQuery = query.trim().toLowerCase(); + String lowerCasedTrimmedQuery = query.trim().replace('\n', ' ').replace('\r', ' ').toLowerCase(); if (lowerCasedTrimmedQuery.startsWith("select new ")) { throw new PanacheQueryException("Unable to perform a projection on a 'select new' query: " + query); } @@ -93,7 +93,7 @@ public CommonPanacheQueryImpl project(Class type) { // New query: SELECT new org.acme.ProjectionClass(e.field1, e.field2) from EntityClass e if (lowerCasedTrimmedQuery.startsWith("select ")) { int endSelect = lowerCasedTrimmedQuery.indexOf(" from "); - String trimmedQuery = query.trim(); + String trimmedQuery = query.trim().replace('\n', ' ').replace('\r', ' '); // 7 is the length of "select " String selectClause = trimmedQuery.substring(7, endSelect).trim(); String from = trimmedQuery.substring(endSelect); diff --git a/extensions/panache/hibernate-orm-panache-kotlin/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/panache/hibernate-orm-panache-kotlin/runtime/src/main/resources/META-INF/quarkus-extension.yaml index c75691278074f..9b9bdc7c48871 100644 --- a/extensions/panache/hibernate-orm-panache-kotlin/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/panache/hibernate-orm-panache-kotlin/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -15,3 +15,7 @@ metadata: config: - "quarkus.datasource." - "quarkus.hibernate-orm." + codestart: + name: "hibernate-orm" + languages: [ "kotlin" ] + artifact: "io.quarkus:quarkus-project-core-extension-codestarts" diff --git a/extensions/panache/hibernate-orm-panache/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/panache/hibernate-orm-panache/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 5894719a99f9c..80b0d0900f65a 100644 --- a/extensions/panache/hibernate-orm-panache/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/panache/hibernate-orm-panache/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -14,3 +14,7 @@ metadata: config: - "quarkus.datasource." - "quarkus.hibernate-orm." + codestart: + name: "hibernate-orm" + languages: ["java"] + artifact: "io.quarkus:quarkus-project-core-extension-codestarts" diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/DataAccessImplementor.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/DataAccessImplementor.java index 6e42b89c706fd..b944fb334f02a 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/DataAccessImplementor.java +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/DataAccessImplementor.java @@ -17,6 +17,25 @@ public interface DataAccessImplementor { */ ResultHandle findById(BytecodeCreator creator, ResultHandle id); + /** + * Find all entities. + * + * @param creator Bytecode creator that should be used for implementation. + * @param page Page instance that should be used in a query. Might be null if pagination is disabled. + * @return Entity list + */ + ResultHandle findAll(BytecodeCreator creator, ResultHandle page); + + /** + * Find all entities. + * + * @param creator Bytecode creator that should be used for implementation. + * @param page Page instance that should be used in a query. Might be null if pagination is disabled. + * @param sort Sort instance that should be used in a query. + * @return Entity list + */ + ResultHandle findAll(BytecodeCreator creator, ResultHandle page, ResultHandle sort); + /** * Find all entities. * diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/EntityDataAccessImplementor.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/EntityDataAccessImplementor.java index 029513c3ed92d..ef1e2375ba6e8 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/EntityDataAccessImplementor.java +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/EntityDataAccessImplementor.java @@ -34,6 +34,29 @@ public ResultHandle findById(BytecodeCreator creator, ResultHandle id) { id); } + /** + * Implements Entity.findAll().page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page) { + ResultHandle query = creator.invokeStaticMethod(ofMethod(entityClassName, "findAll", PanacheQuery.class)); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, + page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", List.class), query); + } + + /** + * Implements Entity.findAll(sort).page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page, ResultHandle sort) { + ResultHandle query = creator.invokeStaticMethod( + ofMethod(entityClassName, "findAll", PanacheQuery.class, Sort.class), sort); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, + page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", List.class), query); + } + /** * Implements Entity.find(query, params).page(page).list() */ diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/RepositoryDataAccessImplementor.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/RepositoryDataAccessImplementor.java index f91f1a776508a..d8ff8b7bf81f3 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/RepositoryDataAccessImplementor.java +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/RepositoryDataAccessImplementor.java @@ -39,6 +39,29 @@ public ResultHandle findById(BytecodeCreator creator, ResultHandle id) { getRepositoryInstance(creator), id); } + /** + * Implements repository.findAll().page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page) { + ResultHandle query = creator.invokeInterfaceMethod( + ofMethod(PanacheRepositoryBase.class, "findAll", PanacheQuery.class), getRepositoryInstance(creator)); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", List.class), query); + } + + /** + * Implements repository.findAll(sort).page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page, ResultHandle sort) { + ResultHandle query = creator.invokeInterfaceMethod( + ofMethod(PanacheRepositoryBase.class, "findAll", PanacheQuery.class, Sort.class), + getRepositoryInstance(creator), sort); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", List.class), query); + } + /** * Implements repository.find(query, params).page(page).list() */ diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/ResourceImplementor.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/ResourceImplementor.java index 0d69dd8bacbbf..c7dbaaa8835ea 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/ResourceImplementor.java +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/ResourceImplementor.java @@ -5,7 +5,9 @@ import java.util.List; import java.util.Map; +import javax.annotation.Priority; import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; import javax.transaction.Transactional; import org.jboss.jandex.ClassInfo; @@ -55,11 +57,16 @@ String implement(ClassOutput classOutput, DataAccessImplementor dataAccessImplem .build(); classCreator.addAnnotation(ApplicationScoped.class); + // The same resource is generated as part of the JaxRsResourceImplementor, so we need to avoid ambiguous resolution + // when injecting the resource in user beans: + classCreator.addAnnotation(Alternative.class); + classCreator.addAnnotation(Priority.class).add("value", Integer.MAX_VALUE); ResourceMethodListenerImplementor listenerImplementor = new ResourceMethodListenerImplementor(classCreator, resourceMethodListeners, false); implementList(classCreator, dataAccessImplementor); + implementListWithQuery(classCreator, dataAccessImplementor); implementListPageCount(classCreator, dataAccessImplementor); implementCount(classCreator, dataAccessImplementor); implementGet(classCreator, dataAccessImplementor); @@ -73,6 +80,20 @@ String implement(ClassOutput classOutput, DataAccessImplementor dataAccessImplem } private void implementList(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor) { + MethodCreator methodCreator = classCreator.getMethodCreator("list", List.class, Page.class, Sort.class); + ResultHandle page = methodCreator.getMethodParam(0); + ResultHandle sort = methodCreator.getMethodParam(1); + ResultHandle columns = methodCreator.invokeVirtualMethod(ofMethod(Sort.class, "getColumns", List.class), sort); + ResultHandle isEmptySort = methodCreator.invokeInterfaceMethod(ofMethod(List.class, "isEmpty", boolean.class), columns); + + BranchResult isEmptySortBranch = methodCreator.ifTrue(isEmptySort); + isEmptySortBranch.trueBranch().returnValue(dataAccessImplementor.findAll(isEmptySortBranch.trueBranch(), page)); + isEmptySortBranch.falseBranch().returnValue(dataAccessImplementor.findAll(isEmptySortBranch.falseBranch(), page, sort)); + + methodCreator.close(); + } + + private void implementListWithQuery(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor) { MethodCreator methodCreator = classCreator.getMethodCreator("list", List.class, Page.class, Sort.class, String.class, Map.class); ResultHandle page = methodCreator.getMethodParam(0); diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/AbstractInjectResourcesMethodTest.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/AbstractInjectResourcesMethodTest.java new file mode 100644 index 0000000000000..b17e82af5ccb3 --- /dev/null +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/AbstractInjectResourcesMethodTest.java @@ -0,0 +1,17 @@ +package io.quarkus.hibernate.orm.rest.data.panache.deployment; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.contains; + +import org.junit.jupiter.api.Test; + +public abstract class AbstractInjectResourcesMethodTest { + + @Test + void shouldGetListOfItems() { + given().accept("application/json") + .when().get("/call/resource/items") + .then().statusCode(200) + .and().body("id", contains(1, 2)); + } +} diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/InjectionResource.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/InjectionResource.java new file mode 100644 index 0000000000000..19f2e6e1e90fb --- /dev/null +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/InjectionResource.java @@ -0,0 +1,26 @@ +package io.quarkus.hibernate.orm.rest.data.panache.deployment.entity; + +import java.util.List; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; + +@Path("/call/resource") +public class InjectionResource { + + @Inject + ItemsResource itemsResource; + + @GET + @Path("/items") + @Produces(MediaType.APPLICATION_JSON) + public List items() { + return itemsResource.list(new Page(5), Sort.by("id")); + } +} diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/PanacheEntityResourceInjectResourcesMethodTest.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/PanacheEntityResourceInjectResourcesMethodTest.java new file mode 100644 index 0000000000000..7a70f6c533636 --- /dev/null +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/PanacheEntityResourceInjectResourcesMethodTest.java @@ -0,0 +1,19 @@ +package io.quarkus.hibernate.orm.rest.data.panache.deployment.entity; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import io.quarkus.hibernate.orm.rest.data.panache.deployment.AbstractInjectResourcesMethodTest; +import io.quarkus.test.QuarkusUnitTest; + +class PanacheEntityResourceInjectResourcesMethodTest extends AbstractInjectResourcesMethodTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(PanacheEntityBase.class, PanacheEntity.class, Collection.class, CollectionsResource.class, + AbstractEntity.class, AbstractItem.class, Item.class, ItemsResource.class, InjectionResource.class) + .addAsResource("application.properties") + .addAsResource("import.sql")); +} diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/DataAccessImplementor.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/DataAccessImplementor.java index 6d88361491536..9d3fec3a0b40a 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/DataAccessImplementor.java +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/DataAccessImplementor.java @@ -17,6 +17,25 @@ public interface DataAccessImplementor { */ ResultHandle findById(BytecodeCreator creator, ResultHandle id); + /** + * Find all entities. + * + * @param creator Bytecode creator that should be used for implementation. + * @param page Page instance that should be used in a query. Might be null if pagination is disabled. + * @return Entity list + */ + ResultHandle findAll(BytecodeCreator creator, ResultHandle page); + + /** + * Find all entities. + * + * @param creator Bytecode creator that should be used for implementation. + * @param page Page instance that should be used in a query. Might be null if pagination is disabled. + * @param sort Sort instance that should be used in a query. + * @return Entity list + */ + ResultHandle findAll(BytecodeCreator creator, ResultHandle page, ResultHandle sort); + /** * Find all entities. * diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/EntityDataAccessImplementor.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/EntityDataAccessImplementor.java index 95c5192b84525..ca0d84fbe5cff 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/EntityDataAccessImplementor.java +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/EntityDataAccessImplementor.java @@ -34,6 +34,29 @@ public ResultHandle findById(BytecodeCreator creator, ResultHandle id) { id); } + /** + * Implements Entity.findAll().page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page) { + ResultHandle query = creator.invokeStaticMethod(ofMethod(entityClassName, "findAll", PanacheQuery.class)); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, + page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", Uni.class), query); + } + + /** + * Implements Entity.findAll(sort).page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page, ResultHandle sort) { + ResultHandle query = creator.invokeStaticMethod( + ofMethod(entityClassName, "findAll", PanacheQuery.class, Sort.class), sort); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, + page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", Uni.class), query); + } + /** * Implements Entity.findAll(query, params).page(page).list() */ diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/RepositoryDataAccessImplementor.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/RepositoryDataAccessImplementor.java index 6382f63a11f9f..603ce94caec84 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/RepositoryDataAccessImplementor.java +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/RepositoryDataAccessImplementor.java @@ -39,6 +39,29 @@ public ResultHandle findById(BytecodeCreator creator, ResultHandle id) { getRepositoryInstance(creator), id); } + /** + * Implements repository.findAll().page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page) { + ResultHandle query = creator.invokeInterfaceMethod( + ofMethod(PanacheRepositoryBase.class, "findAll", PanacheQuery.class), getRepositoryInstance(creator)); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", Uni.class), query); + } + + /** + * Implements repository.findAll(sort).page(page).list() + */ + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page, ResultHandle sort) { + ResultHandle query = creator.invokeInterfaceMethod( + ofMethod(PanacheRepositoryBase.class, "findAll", PanacheQuery.class, Sort.class), + getRepositoryInstance(creator), sort); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", Uni.class), query); + } + /** * Implements repository.find(query, params).page(page).list() */ diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/ResourceImplementor.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/ResourceImplementor.java index 448a741b85413..157a570140815 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/ResourceImplementor.java +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/ResourceImplementor.java @@ -5,7 +5,9 @@ import java.util.List; import java.util.Map; +import javax.annotation.Priority; import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.FieldInfo; @@ -56,13 +58,18 @@ String implement(ClassOutput classOutput, DataAccessImplementor dataAccessImplem .build(); classCreator.addAnnotation(ApplicationScoped.class); + // The same resource is generated as part of the JaxRsResourceImplementor, so we need to avoid ambiguous resolution + // when injecting the resource in user beans: + classCreator.addAnnotation(Alternative.class); + classCreator.addAnnotation(Priority.class).add("value", Integer.MAX_VALUE); ResourceMethodListenerImplementor resourceMethodListenerImplementor = new ResourceMethodListenerImplementor( classCreator, resourceMethodListeners, true); implementList(classCreator, dataAccessImplementor); - implementCount(classCreator, dataAccessImplementor); + implementListWithQuery(classCreator, dataAccessImplementor); implementListPageCount(classCreator, dataAccessImplementor); + implementCount(classCreator, dataAccessImplementor); implementGet(classCreator, dataAccessImplementor); implementAdd(classCreator, dataAccessImplementor, resourceMethodListenerImplementor); implementUpdate(classCreator, dataAccessImplementor, entityType, resourceMethodListenerImplementor); @@ -74,6 +81,20 @@ String implement(ClassOutput classOutput, DataAccessImplementor dataAccessImplem } private void implementList(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor) { + MethodCreator methodCreator = classCreator.getMethodCreator("list", Uni.class, Page.class, Sort.class); + ResultHandle page = methodCreator.getMethodParam(0); + ResultHandle sort = methodCreator.getMethodParam(1); + ResultHandle columns = methodCreator.invokeVirtualMethod(ofMethod(Sort.class, "getColumns", List.class), sort); + ResultHandle isEmptySort = methodCreator.invokeInterfaceMethod(ofMethod(List.class, "isEmpty", boolean.class), columns); + + BranchResult isEmptySortBranch = methodCreator.ifTrue(isEmptySort); + isEmptySortBranch.trueBranch().returnValue(dataAccessImplementor.findAll(isEmptySortBranch.trueBranch(), page)); + isEmptySortBranch.falseBranch().returnValue(dataAccessImplementor.findAll(isEmptySortBranch.falseBranch(), page, sort)); + + methodCreator.close(); + } + + private void implementListWithQuery(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor) { MethodCreator methodCreator = classCreator.getMethodCreator("list", Uni.class, Page.class, Sort.class, String.class, Map.class); ResultHandle page = methodCreator.getMethodParam(0); diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/AbstractInjectResourcesMethodTest.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/AbstractInjectResourcesMethodTest.java new file mode 100644 index 0000000000000..522d322d01d8b --- /dev/null +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/AbstractInjectResourcesMethodTest.java @@ -0,0 +1,17 @@ +package io.quarkus.hibernate.reactive.rest.data.panache.deployment; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.contains; + +import org.junit.jupiter.api.Test; + +public abstract class AbstractInjectResourcesMethodTest { + + @Test + void shouldGetListOfItems() { + given().accept("application/json") + .when().get("/call/resource/items") + .then().statusCode(200) + .and().body("id", contains(1, 2)); + } +} diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/InjectionResource.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/InjectionResource.java new file mode 100644 index 0000000000000..19596484519c3 --- /dev/null +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/InjectionResource.java @@ -0,0 +1,27 @@ +package io.quarkus.hibernate.reactive.rest.data.panache.deployment.entity; + +import java.util.List; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import io.smallrye.mutiny.Uni; + +@Path("/call/resource") +public class InjectionResource { + + @Inject + ItemsResource itemsResource; + + @GET + @Path("/items") + @Produces(MediaType.APPLICATION_JSON) + public Uni> items() { + return itemsResource.list(new Page(5), Sort.by("id")); + } +} diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/PanacheEntityResourceInjectResourcesMethodTest.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/PanacheEntityResourceInjectResourcesMethodTest.java new file mode 100644 index 0000000000000..63615ecadc300 --- /dev/null +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/PanacheEntityResourceInjectResourcesMethodTest.java @@ -0,0 +1,19 @@ +package io.quarkus.hibernate.reactive.rest.data.panache.deployment.entity; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.reactive.panache.PanacheEntity; +import io.quarkus.hibernate.reactive.panache.PanacheEntityBase; +import io.quarkus.hibernate.reactive.rest.data.panache.deployment.AbstractInjectResourcesMethodTest; +import io.quarkus.test.QuarkusUnitTest; + +class PanacheEntityResourceInjectResourcesMethodTest extends AbstractInjectResourcesMethodTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(PanacheEntityBase.class, PanacheEntity.class, Collection.class, CollectionsResource.class, + AbstractEntity.class, AbstractItem.class, Item.class, ItemsResource.class, InjectionResource.class) + .addAsResource("application.properties") + .addAsResource("import.sql")); +} diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/resources/application.properties b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/resources/application.properties index ce8f669b3ab75..c58689900bbea 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/resources/application.properties +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/resources/application.properties @@ -2,6 +2,6 @@ quarkus.datasource.db-kind=postgresql quarkus.datasource.username=hibernate_orm_test quarkus.datasource.password=hibernate_orm_test quarkus.datasource.reactive=true -quarkus.datasource.reactive.url=${postgres.reactive.url} +quarkus.datasource.reactive.url=vertx-reactive:postgresql://localhost:5431/hibernate_orm_test quarkus.hibernate-orm.database.generation=drop-and-create diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/runtime/pom.xml b/extensions/panache/hibernate-reactive-rest-data-panache/runtime/pom.xml index 3f50888b2350f..f4db684d089fc 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/runtime/pom.xml +++ b/extensions/panache/hibernate-reactive-rest-data-panache/runtime/pom.xml @@ -11,7 +11,7 @@ quarkus-hibernate-reactive-rest-data-panache Quarkus - Hibernate Reactive REST data with Panache - Runtime - Generate JAX-RS resources for your Hibernate Panache entities and repositories + Generate JAX-RS resources for your Hibernate Reactive Panache entities and repositories diff --git a/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/reactive/runtime/CommonReactivePanacheQueryImpl.java b/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/reactive/runtime/CommonReactivePanacheQueryImpl.java index 3e80f4f63f3e7..a3bce40fb0142 100644 --- a/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/reactive/runtime/CommonReactivePanacheQueryImpl.java +++ b/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/reactive/runtime/CommonReactivePanacheQueryImpl.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Optional; +import java.util.OptionalInt; import java.util.Set; import org.bson.Document; @@ -33,6 +34,8 @@ public class CommonReactivePanacheQueryImpl { private Collation collation; + private OptionalInt batchSize = OptionalInt.empty(); + public CommonReactivePanacheQueryImpl(ReactiveMongoCollection collection, Bson mongoQuery, Bson sort) { this.collection = collection; this.mongoQuery = mongoQuery; @@ -150,6 +153,11 @@ public CommonReactivePanacheQueryImpl withReadPreference(R return (CommonReactivePanacheQueryImpl) this; } + public CommonReactivePanacheQueryImpl withBatchSize(int batchSize) { + this.batchSize = OptionalInt.of(batchSize); + return (CommonReactivePanacheQueryImpl) this; + } + // Results @SuppressWarnings("unchecked") @@ -229,6 +237,7 @@ private FindOptions buildOptions() { if (this.collation != null) { options.collation(collation); } + batchSize.ifPresent(batchSize -> options.batchSize(batchSize)); return options; } diff --git a/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/runtime/CommonPanacheQueryImpl.java b/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/runtime/CommonPanacheQueryImpl.java index 8ff48b6abd526..6def0096f53ce 100644 --- a/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/runtime/CommonPanacheQueryImpl.java +++ b/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/runtime/CommonPanacheQueryImpl.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.OptionalInt; import java.util.Set; import java.util.stream.Stream; @@ -36,6 +37,8 @@ public class CommonPanacheQueryImpl { private Collation collation; + private OptionalInt batchSize = OptionalInt.empty(); + public CommonPanacheQueryImpl(MongoCollection collection, ClientSession session, Bson mongoQuery, Bson sort) { this.collection = collection; @@ -152,6 +155,11 @@ public CommonPanacheQueryImpl withReadPreference(ReadPrefe return (CommonPanacheQueryImpl) this; } + public CommonPanacheQueryImpl withBatchSize(int batchSize) { + this.batchSize = OptionalInt.of(batchSize); + return (CommonPanacheQueryImpl) this; + } + // Results @SuppressWarnings("unchecked") @@ -178,13 +186,14 @@ public List list() { private List list(Integer limit) { List list = new ArrayList<>(); Bson query = getQuery(); - FindIterable find = clientSession == null ? collection.find(query) : collection.find(clientSession, query); + FindIterable find = clientSession == null ? collection.find(query) : collection.find(clientSession, query); if (this.projections != null) { find.projection(projections); } if (this.collation != null) { find.collation(collation); } + batchSize.ifPresent(batchSize -> find.batchSize(batchSize)); manageOffsets(find, limit); try (MongoCursor cursor = find.sort(sort).iterator()) { diff --git a/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/PanacheQuery.kt b/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/PanacheQuery.kt index 4f6251b0db3f8..5f393492bba62 100644 --- a/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/PanacheQuery.kt +++ b/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/PanacheQuery.kt @@ -141,6 +141,14 @@ interface PanacheQuery { */ fun withReadPreference(readPreference: ReadPreference?): PanacheQuery + /** + * Define the batch size for this query. + * + * @param batchSize the batch size to be used for this query. + * @return this query, modified + */ + fun withBatchSize(batchSize: Int): PanacheQuery + // Results /** * Reads and caches the total number of entities this query operates on. This causes a database diff --git a/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/reactive/ReactivePanacheQuery.kt b/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/reactive/ReactivePanacheQuery.kt index be939bf4cfbc0..e6bb8b8924537 100644 --- a/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/reactive/ReactivePanacheQuery.kt +++ b/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/reactive/ReactivePanacheQuery.kt @@ -144,6 +144,14 @@ interface ReactivePanacheQuery { */ fun withReadPreference(readPreference: ReadPreference): ReactivePanacheQuery + /** + * Define the batch size for this query. + * + * @param batchSize the batch size to be used for this query. + * @return this query, modified + */ + fun withBatchSize(batchSize: Int): ReactivePanacheQuery + /** * Reads and caches the total number of entities this query operates on. This causes a database * query with `SELECT COUNT(*)` and a query equivalent to the current query, minus diff --git a/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/reactive/runtime/ReactivePanacheQueryImpl.kt b/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/reactive/runtime/ReactivePanacheQueryImpl.kt index 1b16783a54dd2..ca886b77c344f 100644 --- a/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/reactive/runtime/ReactivePanacheQueryImpl.kt +++ b/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/reactive/runtime/ReactivePanacheQueryImpl.kt @@ -85,6 +85,11 @@ class ReactivePanacheQueryImpl : ReactivePanacheQuery { return this } + override fun withBatchSize(batchSize: Int): ReactivePanacheQuery { + delegate.withBatchSize(batchSize) + return this + } + override fun count(): Uni { return delegate.count() } diff --git a/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/runtime/PanacheQueryImpl.kt b/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/runtime/PanacheQueryImpl.kt index b0971593bb598..221d186997453 100644 --- a/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/runtime/PanacheQueryImpl.kt +++ b/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/runtime/PanacheQueryImpl.kt @@ -85,6 +85,11 @@ class PanacheQueryImpl : PanacheQuery { return this } + override fun withBatchSize(batchSize: Int): PanacheQuery { + delegate.withBatchSize(batchSize) + return this + } + // Results override fun count(): Long { return delegate.count() diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java index a92ce8f6ed733..288d3a841c945 100644 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java @@ -153,6 +153,14 @@ public interface PanacheQuery { */ public PanacheQuery withReadPreference(ReadPreference readPreference); + /** + * Define the batch size for this query. + * + * @param batchSize the batch size to be used for this query. + * @return this query, modified + */ + public PanacheQuery withBatchSize(int batchSize); + // Results /** diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/ReactivePanacheQuery.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/ReactivePanacheQuery.java index fa1557b02b36a..fd51ab6065205 100644 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/ReactivePanacheQuery.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/ReactivePanacheQuery.java @@ -155,6 +155,13 @@ public interface ReactivePanacheQuery { */ public ReactivePanacheQuery withReadPreference(ReadPreference readPreference); + /** + * Define the batch size for this query. + * + * @param batchSize the batch size to be used for this query. + * @return this query, modified + */ + public ReactivePanacheQuery withBatchSize(int batchSize); // Results /** diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/runtime/ReactivePanacheQueryImpl.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/runtime/ReactivePanacheQueryImpl.java index fc22d253b16f4..c165a6265b952 100644 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/runtime/ReactivePanacheQueryImpl.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/runtime/ReactivePanacheQueryImpl.java @@ -106,6 +106,12 @@ public ReactivePanacheQuery withReadPreference(ReadPrefere return (ReactivePanacheQuery) this; } + @Override + public ReactivePanacheQuery withBatchSize(int batchSize) { + delegate.withBatchSize(batchSize); + return (ReactivePanacheQuery) this; + } + @Override public Uni count() { return delegate.count(); diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java index 7ce5ed0fbdf7c..05012f93f51da 100644 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java @@ -106,6 +106,12 @@ public PanacheQuery withReadPreference(ReadPreference read return (PanacheQuery) this; } + @Override + public PanacheQuery withBatchSize(int batchSize) { + delegate.withBatchSize(batchSize); + return (PanacheQuery) this; + } + // Results @Override diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/JaxRsResourceImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/JaxRsResourceImplementor.java index bbaa04cb04bc3..26662b646e1c2 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/JaxRsResourceImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/JaxRsResourceImplementor.java @@ -4,6 +4,8 @@ import java.util.Arrays; import java.util.List; +import javax.annotation.Priority; +import javax.enterprise.inject.Alternative; import javax.inject.Inject; import javax.ws.rs.Path; @@ -78,6 +80,12 @@ void implement(ClassOutput classOutput, ResourceMetadata resourceMetadata, Resou } ClassCreator classCreator = classCreatorBuilder.build(); + // The same resource is generated as part of the ResourceImplementor, so we need to avoid ambiguous resolution + // when injecting the resource in user beans: + if (resourceMetadata.getResourceInterface() != null) { + classCreator.addAnnotation(Alternative.class); + classCreator.addAnnotation(Priority.class).add("value", Integer.MIN_VALUE); + } implementClassAnnotations(classCreator, resourceMetadata, resourceProperties, capabilities); FieldDescriptor resourceField = implementResourceField(classCreator, resourceMetadata); diff --git a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java index 42087a6d16246..7fd44349f6266 100644 --- a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java +++ b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java @@ -30,6 +30,7 @@ import org.quartz.simpl.CascadingClassLoadHelper; import org.quartz.simpl.SimpleInstanceIdGenerator; import org.quartz.simpl.SimpleThreadPool; +import org.quartz.spi.InstanceIdGenerator; import org.quartz.spi.SchedulerPlugin; import io.quarkus.agroal.spi.JdbcDataSourceBuildItem; @@ -170,6 +171,8 @@ List reflectiveClasses(QuartzBuildTimeConfig config, .add(new ReflectiveClassBuildItem(true, false, QuarkusQuartzConnectionPoolProvider.class.getName())); } + reflectiveClasses + .addAll(getAdditionalConfigurationReflectiveClasses(config.instanceIdGenerators, InstanceIdGenerator.class)); reflectiveClasses.addAll(getAdditionalConfigurationReflectiveClasses(config.triggerListeners, TriggerListener.class)); reflectiveClasses.addAll(getAdditionalConfigurationReflectiveClasses(config.jobListeners, JobListener.class)); reflectiveClasses.addAll(getAdditionalConfigurationReflectiveClasses(config.plugins, SchedulerPlugin.class)); diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ConfigureInstanceIdTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ConfigureInstanceIdTest.java new file mode 100644 index 0000000000000..6e59224833bfc --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ConfigureInstanceIdTest.java @@ -0,0 +1,41 @@ +package io.quarkus.quartz.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.test.QuarkusUnitTest; + +public class ConfigureInstanceIdTest { + + @Inject + Scheduler quartzScheduler; + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Jobs.class) + .addAsResource(new StringAsset( + "quarkus.quartz.instance-id=myInstanceId"), + "application.properties")); + + @Test + public void testSchedulerStarted() throws SchedulerException { + assertEquals("myInstanceId", quartzScheduler.getSchedulerInstanceId()); + } + + static class Jobs { + @Scheduled(every = "1s") + void checkEverySecond() { + // no op + } + } + +} diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java index f6399895ba720..08d89d0a6c13f 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java @@ -68,6 +68,14 @@ public class QuartzBuildTimeConfig { @ConfigItem public Optional selectWithLockSql; + /** + * Instance ID generators. + */ + @ConfigItem + @ConfigDocMapKey("generator-name") + @ConfigDocSection + public Map instanceIdGenerators; + /** * Trigger listeners. */ diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRuntimeConfig.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRuntimeConfig.java index 1c499267b2a42..9a588f7f49f12 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRuntimeConfig.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRuntimeConfig.java @@ -19,6 +19,26 @@ public class QuartzRuntimeConfig { @ConfigItem(defaultValue = "QuarkusQuartzScheduler") public String instanceName; + /** + * The identifier of Quartz instance that must be unique for all schedulers working as if they are the same + * logical Scheduler within a cluster. Use the default value {@code AUTO} or some of the configured + * + * instance ID generators if you wish the identifier to be generated for you. + */ + @ConfigItem(defaultValue = "AUTO") + public String instanceId; + + /** + * The amount of time in milliseconds that a trigger is allowed to be acquired and fired ahead of its scheduled fire time. + */ + @ConfigItem(defaultValue = "0") + public long batchTriggerAcquisitionFireAheadTimeWindow; + + /** + * The maximum number of triggers that a scheduler node is allowed to acquire (for firing) at once. + */ + @ConfigItem(defaultValue = "1") + public int batchTriggerAcquisitionMaxCount; /** * The size of scheduler thread pool. This will initialize the number of worker threads in the pool. */ diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java index 4e53337ea8dd3..098fbe2c5ccef 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java @@ -554,17 +554,18 @@ void destroy() { private Properties getSchedulerConfigurationProperties(QuartzSupport quartzSupport) { Properties props = new Properties(); QuartzBuildTimeConfig buildTimeConfig = quartzSupport.getBuildTimeConfig(); - props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_ID, "AUTO"); + QuartzRuntimeConfig runtimeConfig = quartzSupport.getRuntimeConfig(); + props.put("org.quartz.scheduler.skipUpdateCheck", "true"); - props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, quartzSupport.getRuntimeConfig().instanceName); + props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, runtimeConfig.instanceName); + props.put(StdSchedulerFactory.PROP_SCHED_BATCH_TIME_WINDOW, runtimeConfig.batchTriggerAcquisitionFireAheadTimeWindow); + props.put(StdSchedulerFactory.PROP_SCHED_MAX_BATCH_SIZE, runtimeConfig.batchTriggerAcquisitionMaxCount); props.put(StdSchedulerFactory.PROP_SCHED_WRAP_JOB_IN_USER_TX, "false"); props.put(StdSchedulerFactory.PROP_SCHED_SCHEDULER_THREADS_INHERIT_CONTEXT_CLASS_LOADER_OF_INITIALIZING_THREAD, "true"); props.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, "org.quartz.simpl.SimpleThreadPool"); props.put(StdSchedulerFactory.PROP_SCHED_CLASS_LOAD_HELPER_CLASS, InitThreadContextClassLoadHelper.class.getName()); - props.put(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX + ".threadCount", - "" + quartzSupport.getRuntimeConfig().threadCount); - props.put(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX + ".threadPriority", - "" + quartzSupport.getRuntimeConfig().threadPriority); + props.put(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX + ".threadCount", "" + runtimeConfig.threadCount); + props.put(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX + ".threadPriority", "" + runtimeConfig.threadPriority); props.put(StdSchedulerFactory.PROP_SCHED_RMI_EXPORT, "false"); props.put(StdSchedulerFactory.PROP_SCHED_RMI_PROXY, "false"); props.put(StdSchedulerFactory.PROP_JOB_STORE_CLASS, buildTimeConfig.storeType.clazz); @@ -574,18 +575,18 @@ private Properties getSchedulerConfigurationProperties(QuartzSupport quartzSuppo QuarkusQuartzConnectionPoolProvider.setDataSourceName(dataSource); props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".useProperties", "true"); props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".misfireThreshold", - "" + quartzSupport.getRuntimeConfig().misfireThreshold.toMillis()); + "" + runtimeConfig.misfireThreshold.toMillis()); props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".tablePrefix", buildTimeConfig.tablePrefix); props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".dataSource", dataSource); props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".driverDelegateClass", quartzSupport.getDriverDialect().get()); props.put(StdSchedulerFactory.PROP_DATASOURCE_PREFIX + "." + dataSource + ".connectionProvider.class", QuarkusQuartzConnectionPoolProvider.class.getName()); + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".acquireTriggersWithinLock", "true"); if (buildTimeConfig.clustered) { props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".isClustered", "true"); - props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".acquireTriggersWithinLock", "true"); props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".clusterCheckinInterval", - "" + quartzSupport.getBuildTimeConfig().clusterCheckinInterval); + "" + buildTimeConfig.clusterCheckinInterval); if (buildTimeConfig.selectWithLockSql.isPresent()) { props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".selectWithLockSQL", buildTimeConfig.selectWithLockSql.get()); @@ -596,24 +597,40 @@ private Properties getSchedulerConfigurationProperties(QuartzSupport quartzSuppo props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".nonManagedTXDataSource", dataSource); } } - props.putAll(getAdditionalConfigurationProperties(StdSchedulerFactory.PROP_PLUGIN_PREFIX, buildTimeConfig.plugins)); - props.putAll(getAdditionalConfigurationProperties(StdSchedulerFactory.PROP_JOB_LISTENER_PREFIX, - buildTimeConfig.jobListeners)); - props.putAll(getAdditionalConfigurationProperties(StdSchedulerFactory.PROP_TRIGGER_LISTENER_PREFIX, - buildTimeConfig.triggerListeners)); + QuartzExtensionPointConfig instanceIdGenerator = buildTimeConfig.instanceIdGenerators.get(runtimeConfig.instanceId); + if (runtimeConfig.instanceId.equals(StdSchedulerFactory.AUTO_GENERATE_INSTANCE_ID) || instanceIdGenerator != null) { + props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_ID, StdSchedulerFactory.AUTO_GENERATE_INSTANCE_ID); + } else { + if (runtimeConfig.instanceId.equals(StdSchedulerFactory.SYSTEM_PROPERTY_AS_INSTANCE_ID)) { + LOGGER.warn("Prefer to configure the 'SystemPropertyInstanceIdGenerator' within the instance ID generators, " + + "so the system property name can be changed and the application can be native."); + } + props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_ID, runtimeConfig.instanceId); + } + if (instanceIdGenerator != null) { + putExtensionConfigurationProperties(props, StdSchedulerFactory.PROP_SCHED_INSTANCE_ID_GENERATOR_PREFIX, + instanceIdGenerator); + } + putExtensionConfigurationProperties(props, StdSchedulerFactory.PROP_PLUGIN_PREFIX, buildTimeConfig.plugins); + putExtensionConfigurationProperties(props, StdSchedulerFactory.PROP_JOB_LISTENER_PREFIX, buildTimeConfig.jobListeners); + putExtensionConfigurationProperties(props, StdSchedulerFactory.PROP_TRIGGER_LISTENER_PREFIX, + buildTimeConfig.triggerListeners); return props; } - private Properties getAdditionalConfigurationProperties(String prefix, Map config) { - Properties props = new Properties(); - for (Map.Entry configEntry : config.entrySet()) { - props.put(String.format("%s.%s.class", prefix, configEntry.getKey()), configEntry.getValue().clazz); - for (Map.Entry propsEntry : configEntry.getValue().properties.entrySet()) { - props.put(String.format("%s.%s.%s", prefix, configEntry.getKey(), propsEntry.getKey()), propsEntry.getValue()); - } - } - return props; + private void putExtensionConfigurationProperties(Properties props, String prefix, + Map configs) { + configs.forEach((configKey, config) -> { + putExtensionConfigurationProperties(props, String.format("%s.%s", prefix, configKey), config); + }); + } + + private void putExtensionConfigurationProperties(Properties props, String prefix, QuartzExtensionPointConfig config) { + props.put(String.format("%s.class", prefix), config.clazz); + config.properties.forEach((propName, propValue) -> { + props.put(String.format("%s.%s", prefix, propName), propValue); + }); } /** diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/StoreType.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/StoreType.java index 57e10a02fe6e5..1754f26bcbaa3 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/StoreType.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/StoreType.java @@ -5,8 +5,8 @@ public enum StoreType { JDBC_TX("org.quartz.impl.jdbcjobstore.JobStoreTX", "JobStoreTX"), JDBC_CMT("org.quartz.impl.jdbcjobstore.JobStoreCMT", "JobStoreCMT"); - public String clazz; - public String simpleName; + public final String clazz; + public final String simpleName; StoreType(String clazz, String simpleName) { this.clazz = clazz; diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java index 5c82459d08232..33ca5cd68de84 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java @@ -71,6 +71,7 @@ import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.DescriptorUtils; import io.quarkus.gizmo.FunctionCreator; +import io.quarkus.gizmo.Gizmo; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; @@ -960,8 +961,7 @@ private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreat ResultHandle ret = resolve.newInstance(MethodDescriptor.ofConstructor(CompletableFuture.class)); // First handle dynamic messages, i.e. the "message" virtual method - BytecodeCreator dynamicMessage = resolve.ifTrue(resolve.invokeVirtualMethod(Descriptors.EQUALS, - resolve.load(MESSAGE), name)) + BytecodeCreator dynamicMessage = resolve.ifTrue(Gizmo.equals(resolve, resolve.load(MESSAGE), name)) .trueBranch(); ResultHandle evaluatedMessageKey = dynamicMessage.invokeStaticMethod(Descriptors.EVALUATED_PARAMS_EVALUATE_MESSAGE_KEY, evalContext); @@ -988,8 +988,8 @@ private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreat nameIsNull.invokeVirtualMethod(Descriptors.COMPLETABLE_FUTURE_COMPLETE, whenRet, resultNotFound); nameIsNull.returnValue(null); - BytecodeCreator nameNotFound = success.ifTrue(success.invokeVirtualMethod(Descriptors.EQUALS, - whenComplete.getMethodParam(0), resultNotFound)).trueBranch(); + BytecodeCreator nameNotFound = success.ifTrue(Gizmo.equals(success, whenComplete.getMethodParam(0), resultNotFound)) + .trueBranch(); nameNotFound.invokeVirtualMethod(Descriptors.COMPLETABLE_FUTURE_COMPLETE, whenRet, resultNotFound); nameNotFound.returnValue(null); @@ -1067,8 +1067,7 @@ private void addMessageMethod(MethodCreator resolve, String key, MethodInfo meth ResultHandle ret, String bundleClass) { List methodParams = method.parameterTypes(); - BytecodeCreator matched = resolve.ifTrue(resolve.invokeVirtualMethod(Descriptors.EQUALS, - resolve.load(key), name)) + BytecodeCreator matched = resolve.ifTrue(Gizmo.equals(resolve, resolve.load(key), name)) .trueBranch(); if (method.parameterTypes().isEmpty()) { matched.invokeVirtualMethod(Descriptors.COMPLETABLE_FUTURE_COMPLETE, ret, diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/StringTemplateExtensionsTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/StringTemplateExtensionsTest.java index 26d27f283427a..9833dba9eff2b 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/StringTemplateExtensionsTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/StringTemplateExtensionsTest.java @@ -42,6 +42,10 @@ public void testTemplateExtensions() { engine.parse("{str:fmt(locale,'%tA',now)}") .data("now", LocalDateTime.of(2016, 7, 26, 12, 0), "locale", Locale.GERMAN) .render()); + assertEquals("barbar1", + engine.parse("{foo + 'bar' + 1}") + .data("foo", "bar") + .render()); } } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/StringTemplateExtensions.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/StringTemplateExtensions.java index 6f15b8eed830c..01c4f305d8f26 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/StringTemplateExtensions.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/extensions/StringTemplateExtensions.java @@ -67,4 +67,9 @@ static String fmt(String ignoredPropertyName, Locale locale, String format, Obje return String.format(locale, format, args); } + @TemplateExtension(matchName = "+") + static String plus(String str, Object val) { + return str + val; + } + } diff --git a/extensions/reactive-mysql-client/deployment/pom.xml b/extensions/reactive-mysql-client/deployment/pom.xml index 3ea61350604b3..19c3517b65487 100644 --- a/extensions/reactive-mysql-client/deployment/pom.xml +++ b/extensions/reactive-mysql-client/deployment/pom.xml @@ -60,6 +60,10 @@ io.quarkus quarkus-devservices-mysql + + io.quarkus + quarkus-devservices-mariadb + io.quarkus diff --git a/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java b/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java index b763f424b52d3..2354842e7865c 100644 --- a/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java +++ b/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java @@ -84,8 +84,9 @@ ServiceStartBuildItem build(BuildProducer feature, } @BuildStep - DevServicesDatasourceConfigurationHandlerBuildItem devDbHandler() { - return DevServicesDatasourceConfigurationHandlerBuildItem.reactive(DatabaseKind.MYSQL); + List devDbHandler() { + return List.of(DevServicesDatasourceConfigurationHandlerBuildItem.reactive(DatabaseKind.MYSQL), + DevServicesDatasourceConfigurationHandlerBuildItem.reactive(DatabaseKind.MARIADB)); } @BuildStep diff --git a/extensions/reactive-routes/deployment/src/test/resources/conf/cors-config.properties b/extensions/reactive-routes/deployment/src/test/resources/conf/cors-config.properties index 6cc822b80597c..10c86a915bd04 100644 --- a/extensions/reactive-routes/deployment/src/test/resources/conf/cors-config.properties +++ b/extensions/reactive-routes/deployment/src/test/resources/conf/cors-config.properties @@ -1,3 +1,4 @@ quarkus.http.cors=true +quarkus.http.cors.origins=* # whitespaces added to test that they are not taken into account config is parsed quarkus.http.cors.methods=GET, OPTIONS, POST diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisBuildTimeConfig.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisBuildTimeConfig.java index 1cc335208f82b..a6bac1a32ee56 100644 --- a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisBuildTimeConfig.java +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisBuildTimeConfig.java @@ -10,6 +10,44 @@ @ConfigRoot public class RedisBuildTimeConfig { + + /** + * The default redis client + */ + @ConfigItem(name = ConfigItem.PARENT) + public RedisClientBuildTimeConfig defaultRedisClient; + + /** + * Configures additional (named) Redis clients. + *

    + * Each client has a unique name which must be identified to select the right client. + * For example: + *

    + * + *

    +     * quarkus.redis.client1.hosts = redis://localhost:6379
    +     * quarkus.redis.client2.hosts = redis://localhost:6380
    +     * 
    + *

    + * And then use the {@link io.quarkus.redis.client.RedisClientName} annotation to select the + * {@link io.vertx.mutiny.redis.client.Redis}, + * {@link io.vertx.redis.client.Redis}, {@link io.vertx.mutiny.redis.client.RedisAPI} and + * {@link io.vertx.redis.client.RedisAPI} beans. + *

    + * + *

    +     * {
    +     *     @code
    +     *     @RedisClientName("client1")
    +     *     @Inject
    +     *     RedisAPI redis;
    +     * }
    +     * 
    + */ + @ConfigItem(name = ConfigItem.PARENT) + @ConfigDocMapKey("redis-client-name") + public Map namedRedisClients; + /** * Whether a health check is published in case the smallrye-health extension is present. */ diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisClientBuildTimeConfig.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisClientBuildTimeConfig.java new file mode 100644 index 0000000000000..35881a5a397a3 --- /dev/null +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisClientBuildTimeConfig.java @@ -0,0 +1,42 @@ +package io.quarkus.redis.client.deployment; + +import java.util.List; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConvertWith; +import io.quarkus.runtime.configuration.TrimmedStringConverter; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +@ConfigGroup +public class RedisClientBuildTimeConfig { + + /** + * A list of files allowing to pre-load data into the Redis server. + * The file is formatted as follows: + *
      + *
    • One instruction per line
    • + *
    • Each instruction is a Redis command and its parameter such as {@code HSET foo field value}
    • + *
    • Parameters can be wrapped into double-quotes if they include spaces
    • + *
    • Parameters can be wrapped into single-quote if they include spaces
    • + *
    • Parameters including double-quotes must be wrapped into single-quotes
    • + *
    + */ + @ConfigItem(defaultValueDocumentation = "import.redis in DEV, TEST ; no-file otherwise") + @ConvertWith(TrimmedStringConverter.class) + public Optional> loadScript; + + /** + * When using {@code redisLoadScript}, indicates if the Redis database must be flushed (erased) before importing. + */ + @ConfigItem(defaultValue = "true") + public boolean flushBeforeLoad; + + /** + * When using {@code redisLoadScript}, indicates if the import should only happen if the database is empty (no keys). + */ + @ConfigItem(defaultValue = "true") + public boolean loadOnlyIfEmpty; + +} diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisClientProcessor.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisClientProcessor.java index 3600e898640bd..338fe81be1dfe 100644 --- a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisClientProcessor.java +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisClientProcessor.java @@ -2,14 +2,19 @@ import static io.quarkus.redis.runtime.client.config.RedisConfig.DEFAULT_CLIENT_NAME; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.inject.Default; @@ -30,9 +35,13 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.redis.client.RedisClient; import io.quarkus.redis.client.RedisClientName; @@ -40,6 +49,9 @@ import io.quarkus.redis.client.RedisOptionsCustomizer; import io.quarkus.redis.client.reactive.ReactiveRedisClient; import io.quarkus.redis.runtime.client.RedisClientRecorder; +import io.quarkus.redis.runtime.client.config.RedisConfig; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; import io.quarkus.vertx.deployment.VertxBuildItem; import io.vertx.redis.client.impl.types.BulkType; @@ -103,16 +115,28 @@ UnremovableBeanBuildItem makeHostsProviderAndOptionsCustomizerUnremovable() { @BuildStep @Record(ExecutionTime.RUNTIME_INIT) - public void init(RedisClientRecorder recorder, + public void init( + List clients, + RedisClientRecorder recorder, RedisBuildTimeConfig buildTimeConfig, BeanArchiveIndexBuildItem indexBuildItem, BeanDiscoveryFinishedBuildItem beans, ShutdownContextBuildItem shutdown, BuildProducer syntheticBeans, - VertxBuildItem vertxBuildItem) { + RedisConfig config, + VertxBuildItem vertxBuildItem, + ApplicationArchivesBuildItem applicationArchivesBuildItem, LaunchModeBuildItem launchMode, + BuildProducer nativeImageResources, + BuildProducer hotDeploymentWatchedFiles) { // Collect the used redis clients, the unused clients will not be instantiated. Set names = new HashSet<>(); + + // Add the names from the requested clients. + for (RequestedRedisClientBuildItem client : clients) { + names.add(client.name); + } + IndexView indexView = indexBuildItem.getIndex(); Collection clientAnnotations = indexView.getAnnotations(REDIS_CLIENT_ANNOTATION); for (AnnotationInstance annotation : clientAnnotations) { @@ -156,6 +180,18 @@ public void init(RedisClientRecorder recorder, } recorder.cleanup(shutdown); + + // Handle data import + preloadRedisData(DEFAULT_CLIENT_NAME, buildTimeConfig.defaultRedisClient, applicationArchivesBuildItem, + launchMode.getLaunchMode(), + nativeImageResources, hotDeploymentWatchedFiles, recorder); + + if (buildTimeConfig.namedRedisClients != null) { + for (Map.Entry entry : buildTimeConfig.namedRedisClients.entrySet()) { + preloadRedisData(entry.getKey(), entry.getValue(), applicationArchivesBuildItem, launchMode.getLaunchMode(), + nativeImageResources, hotDeploymentWatchedFiles, recorder); + } + } } static Set configuredClientNames(RedisBuildTimeConfig buildTimeConfig, Config config) { @@ -198,9 +234,75 @@ static SyntheticBeanBuildItem configureAndCreateSyntheticBean(String name, return configurator.done(); } + private void preloadRedisData(String name, RedisClientBuildTimeConfig clientConfig, + ApplicationArchivesBuildItem applicationArchivesBuildItem, + LaunchMode launchMode, BuildProducer nativeImageResources, + BuildProducer hotDeploymentWatchedFiles, RedisClientRecorder recorder) { + List importFiles = getRedisLoadScript(clientConfig, launchMode); + List paths = new ArrayList<>(); + for (String importFile : importFiles) { + Path loadScriptPath; + try { + loadScriptPath = applicationArchivesBuildItem.getRootArchive().getChildPath(importFile); + } catch (RuntimeException e) { + throw new ConfigurationException( + "Unable to interpret path referenced in '" + + RedisConfig.propertyKey(name, "redis-load-script") + "=" + + String.join(",", importFiles) + + "': " + e.getMessage()); + } + + if (loadScriptPath != null && !Files.isDirectory(loadScriptPath)) { + // enlist resource if present + nativeImageResources.produce(new NativeImageResourceBuildItem(importFile)); + } else if (clientConfig != null && clientConfig.loadScript.isPresent()) { + //raise exception if explicit file is not present (i.e. not the default) + throw new ConfigurationException( + "Unable to find file referenced in '" + + RedisConfig.propertyKey(name, "redis-load-script") + "=" + + String.join(", ", clientConfig.loadScript.get()) + + "'. Remove property or add file to your path."); + } + // in dev mode we want to make sure that we watch for changes to file even if it doesn't currently exist + // as a user could still add it after performing the initial configuration + hotDeploymentWatchedFiles.produce(new HotDeploymentWatchedFileBuildItem(importFile)); + + if (loadScriptPath != null) { + paths.add(importFile); + } + } + + if (!paths.isEmpty()) { + if (clientConfig != null) { + recorder.preload(name, paths, clientConfig.flushBeforeLoad, clientConfig.loadOnlyIfEmpty); + } else { + recorder.preload(name, paths, true, true); + } + } + + } + @BuildStep HealthBuildItem addHealthCheck(RedisBuildTimeConfig buildTimeConfig) { return new HealthBuildItem("io.quarkus.redis.runtime.client.health.RedisHealthCheck", buildTimeConfig.healthEnabled); } + + public static final String NO_REDIS_SCRIPT_FILE = "no-file"; + + private static List getRedisLoadScript(RedisClientBuildTimeConfig config, LaunchMode launchMode) { + if (config == null) { + return List.of("import.redis"); + } + var scripts = config.loadScript; + if (scripts.isPresent()) { + return scripts.get().stream() + .filter(s -> !NO_REDIS_SCRIPT_FILE.equalsIgnoreCase(s)) + .collect(Collectors.toList()); + } else if (launchMode == LaunchMode.NORMAL) { + return Collections.emptyList(); + } else { + return List.of("import.redis"); + } + } } diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisDatasourceProcessor.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisDatasourceProcessor.java index 454110ffe7d34..00d9b57673e66 100644 --- a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisDatasourceProcessor.java +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisDatasourceProcessor.java @@ -36,15 +36,10 @@ public class RedisDatasourceProcessor { DotName.createSimple(ReactiveRedisDataSource.class.getName())); @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - public void init(RedisClientRecorder recorder, + public void detectUsage(BuildProducer request, RedisBuildTimeConfig buildTimeConfig, BeanArchiveIndexBuildItem indexBuildItem, - BeanDiscoveryFinishedBuildItem beans, - ShutdownContextBuildItem shutdown, - BuildProducer syntheticBeans, - VertxBuildItem vertxBuildItem) { - + BeanDiscoveryFinishedBuildItem beans) { // Collect the used redis datasource, the unused clients will not be instantiated. Set names = new HashSet<>(); IndexView indexView = indexBuildItem.getIndex(); @@ -65,6 +60,26 @@ public void init(RedisClientRecorder recorder, .findAny() .ifPresent(x -> names.addAll(configuredClientNames(buildTimeConfig, ConfigProvider.getConfig()))); + for (String name : names) { + request.produce(new RequestedRedisClientBuildItem(name)); + } + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public void init(RedisClientRecorder recorder, + List clients, + ShutdownContextBuildItem shutdown, + BuildProducer syntheticBeans, + VertxBuildItem vertxBuildItem) { + + if (clients.isEmpty()) { + return; + } + Set names = new HashSet<>(); + for (RequestedRedisClientBuildItem client : clients) { + names.add(client.name); + } // Inject the creation of the client when the application starts. recorder.initialize(vertxBuildItem.getVertx(), names); diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisMetricsBuildItem.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisMetricsBuildItem.java new file mode 100644 index 0000000000000..7269b154cf237 --- /dev/null +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RedisMetricsBuildItem.java @@ -0,0 +1,18 @@ +package io.quarkus.redis.client.deployment; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.redis.runtime.client.ObservableRedisMetrics; +import io.quarkus.runtime.RuntimeValue; + +public final class RedisMetricsBuildItem extends SimpleBuildItem { + + private final RuntimeValue metrics; + + public RedisMetricsBuildItem(RuntimeValue metrics) { + this.metrics = metrics; + } + + public RuntimeValue get() { + return metrics; + } +} diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RequestedRedisClientBuildItem.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RequestedRedisClientBuildItem.java new file mode 100644 index 0000000000000..342496cd7c8b0 --- /dev/null +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/RequestedRedisClientBuildItem.java @@ -0,0 +1,16 @@ +package io.quarkus.redis.client.deployment; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Request the creation of the Redis client with the given name. + */ +public final class RequestedRedisClientBuildItem extends MultiBuildItem { + + public final String name; + + public RequestedRedisClientBuildItem(String name) { + this.name = name; + } + +} diff --git a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/IncrementResource.java b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/IncrementResource.java index 336538a19261c..29b8dd7701486 100644 --- a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/IncrementResource.java +++ b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/IncrementResource.java @@ -4,6 +4,7 @@ import javax.ws.rs.Path; import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.redis.datasource.keys.KeyCommands; import io.quarkus.redis.datasource.value.ValueCommands; @Path("/inc") @@ -11,9 +12,11 @@ public class IncrementResource { public static final long INCREMENT = 1; private final ValueCommands commands; + private final KeyCommands keys; public IncrementResource(RedisDataSource ds) { commands = ds.value(Integer.class); + keys = ds.key(); } @GET @@ -21,4 +24,10 @@ public int increment() { return (int) commands.incrby("counter-dev-mode", INCREMENT); } + @GET + @Path("/keys") + public int verifyPreloading() { + return keys.keys("*").size(); + } + } diff --git a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/RedisClientDevModeTestCase.java b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/RedisClientDevModeTestCase.java index e0e104ecf5ef5..860194ee8e014 100644 --- a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/RedisClientDevModeTestCase.java +++ b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/RedisClientDevModeTestCase.java @@ -31,11 +31,12 @@ public JavaArchive get() { }); @Test - public void testRedisDevMode() { + public void testSourceFileUpdateInDevMode() { RestAssured.get("/inc") .then() .statusCode(200) .body(Matchers.equalTo("1")); + RestAssured.get("/inc") .then() .statusCode(200) @@ -55,5 +56,9 @@ public String apply(String s) { .statusCode(200) .body(Matchers.equalTo("22")); + RestAssured.get("/inc") + .then() + .statusCode(200) + .body(Matchers.equalTo("32")); } } diff --git a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/RedisClientPreloadDevModeTestCase.java b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/RedisClientPreloadDevModeTestCase.java new file mode 100644 index 0000000000000..c7f92a468c249 --- /dev/null +++ b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/devmode/RedisClientPreloadDevModeTestCase.java @@ -0,0 +1,48 @@ +package io.quarkus.redis.client.deployment.devmode; + +import java.io.File; +import java.util.function.Supplier; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.redis.client.deployment.RedisTestResource; +import io.quarkus.test.QuarkusDevModeTest; +import io.quarkus.test.common.QuarkusTestResource; +import io.restassured.RestAssured; + +@QuarkusTestResource(RedisTestResource.class) +public class RedisClientPreloadDevModeTestCase { + + @RegisterExtension + static QuarkusDevModeTest test = new QuarkusDevModeTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset( + "quarkus.vertx.caching=false\n" + + "quarkus.redis.hosts=${quarkus.redis.tr}\n" + + "quarkus.redis.load-only-if-empty=false\n"), + "application.properties") + .addAsResource(new File("src/test/resources/imports/starwars.redis"), "import.redis") + .addClass(IncrementResource.class); + } + }); + + @Test + public void testImportFileUpdateInDevMode() { + // Verify preloading + int count = Integer.valueOf(RestAssured.get("/inc/keys") + .then().statusCode(200).extract().body().asString()); + + // Change the import file + test.modifyResourceFile("import.redis", s -> s + "\nLPUSH list 1 2 3 4 5"); + RestAssured.get("/inc/keys") + .then().statusCode(200).body(Matchers.equalTo(Integer.toString(count + 1))); + } +} diff --git a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/DefaultFileForDefaultClientPreloadingTest.java b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/DefaultFileForDefaultClientPreloadingTest.java new file mode 100644 index 0000000000000..fa1af0e10f7c3 --- /dev/null +++ b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/DefaultFileForDefaultClientPreloadingTest.java @@ -0,0 +1,48 @@ +package io.quarkus.redis.client.deployment.preloading; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.io.File; + +import javax.inject.Inject; + +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.redis.client.deployment.RedisTestResource; +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; + +@QuarkusTestResource(RedisTestResource.class) +public class DefaultFileForDefaultClientPreloadingTest { + + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new File("src/test/resources/imports/import.redis"), "import.redis")) + .overrideConfigKey("quarkus.redis.hosts", "${quarkus.redis.tr}"); + + @Inject + RedisDataSource ds; + + @Test + void verifyImport() { + var keys = ds.key(); + var values = ds.value(String.class); + var hashes = ds.hash(String.class); + + assertThat(keys.keys("*")).containsExactlyInAnyOrder("foo", "bar", "key1", "key2", "key3", "key4"); + + assertThat(hashes.hgetall("foo")).containsOnly(entry("field1", "abc"), entry("field2", "123")); + assertThat(hashes.hgetall("bar")).containsOnly(entry("field1", "abc def"), entry("field2", "123 456 ")); + + assertThat(values.get("key1")).isEqualTo("A value using \"double-quotes\""); + assertThat(values.get("key2")).isEqualTo("A value using 'single-quotes'"); + assertThat(values.get("key3")).isEqualTo("A value using a single single ' quote"); + assertThat(values.get("key4")).isEqualTo("A value using a single double \" quote"); + } +} diff --git a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/MissingFilePreloadTest.java b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/MissingFilePreloadTest.java new file mode 100644 index 0000000000000..027c28f9ffa07 --- /dev/null +++ b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/MissingFilePreloadTest.java @@ -0,0 +1,40 @@ +package io.quarkus.redis.client.deployment.preloading; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.redis.client.deployment.RedisTestResource; +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; + +@QuarkusTestResource(RedisTestResource.class) +public class MissingFilePreloadTest { + + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset( + "quarkus.redis.hosts=${quarkus.redis.tr}\n" + + "quarkus.redis.load-script=import/my-import.redis,missing.redis"), + "application.properties") + .addAsResource(new File("src/test/resources/imports/import.redis"), "import/my-import.redis")) + .assertException(t -> assertThat(t).hasMessageContaining("Unable to find file referenced")); + + @Inject + RedisDataSource ds; + + @Test + void test() { + // should not run + } +} diff --git a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/MultiClientImportPreloadingTest.java b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/MultiClientImportPreloadingTest.java new file mode 100644 index 0000000000000..951945a866cd4 --- /dev/null +++ b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/MultiClientImportPreloadingTest.java @@ -0,0 +1,69 @@ +package io.quarkus.redis.client.deployment.preloading; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.io.File; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.redis.client.RedisClientName; +import io.quarkus.redis.client.deployment.RedisTestResource; +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; + +@QuarkusTestResource(RedisTestResource.class) +public class MultiClientImportPreloadingTest { + + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset( + "quarkus.redis.hosts=${quarkus.redis.tr}\n" + + "quarkus.redis.load-script=import/my-import.redis\n" + + "quarkus.redis.my-redis.hosts=${quarkus.redis.tr}\n" + + "quarkus.redis.my-redis.load-script=sample.redis\n" + + // Do not erase as it's using the same database + // And load even if not empty + "quarkus.redis.my-redis.flush-before-load=false\n" + + "quarkus.redis.my-redis.load-only-if-empty=false"), + "application.properties") + .addAsResource(new File("src/test/resources/imports/import.redis"), "import/my-import.redis") + .addAsResource(new File("src/test/resources/imports/sample.redis"), "sample.redis") + + ); + + @Inject + RedisDataSource ds; + + @Inject + @RedisClientName("my-redis") + RedisDataSource my; + + @Test + void verifyImport() { + // Both clients are using the same database, by using multiple clients to distinguish. + var values = ds.value(String.class); + var hashes = ds.hash(String.class); + + assertThat(hashes.hgetall("foo")).containsOnly(entry("field1", "abc"), entry("field2", "123")); + assertThat(hashes.hgetall("bar")).containsOnly(entry("field1", "abc def"), entry("field2", "123 456 ")); + + assertThat(values.get("key1")).isEqualTo("A value using \"double-quotes\""); + assertThat(values.get("key2")).isEqualTo("A value using 'single-quotes'"); + assertThat(values.get("key3")).isEqualTo("A value using a single single ' quote"); + assertThat(values.get("key4")).isEqualTo("A value using a single double \" quote"); + + values = my.value(String.class); + assertThat(values.get("key")).isEqualTo("value"); + assertThat(values.get("space:key")).isEqualTo("another value"); + assertThat(values.get("counter")).isEqualTo("1"); + } +} diff --git a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/MultiClientImportPreloadingWithFlushAllTest.java b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/MultiClientImportPreloadingWithFlushAllTest.java new file mode 100644 index 0000000000000..f9e3277d59c38 --- /dev/null +++ b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/MultiClientImportPreloadingWithFlushAllTest.java @@ -0,0 +1,54 @@ +package io.quarkus.redis.client.deployment.preloading; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.redis.client.RedisClientName; +import io.quarkus.redis.client.deployment.RedisTestResource; +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; + +@QuarkusTestResource(RedisTestResource.class) +public class MultiClientImportPreloadingWithFlushAllTest { + + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset( + "quarkus.redis.hosts=${quarkus.redis.tr}\n" + + "quarkus.redis.load-script=import/my-import.redis\n" + + "quarkus.redis.my-redis.hosts=${quarkus.redis.tr}\n" + + "quarkus.redis.my-redis.load-script=sample.redis\n" + + // Erase the data source + // Bad idea (as it will dropped what we loaded so far), but just to test the behavior + "quarkus.redis.my-redis.flush-before-load=true"), + "application.properties") + .addAsResource(new File("src/test/resources/imports/import.redis"), "import/my-import.redis") + .addAsResource(new File("src/test/resources/imports/sample.redis"), "sample.redis") + + ); + + @Inject + RedisDataSource ds; + + @Inject + @RedisClientName("my-redis") + RedisDataSource my; + + @Test + void verifyImport() { + // Others have been removed by the `flushall` command + assertThat(my.key().keys("*")) + .containsOnly("key", "space:key", "counter"); + } +} diff --git a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/MultiClientImportPreloadingWithOnlyIfEmptyTest.java b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/MultiClientImportPreloadingWithOnlyIfEmptyTest.java new file mode 100644 index 0000000000000..4994e82603c45 --- /dev/null +++ b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/MultiClientImportPreloadingWithOnlyIfEmptyTest.java @@ -0,0 +1,54 @@ +package io.quarkus.redis.client.deployment.preloading; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.redis.client.RedisClientName; +import io.quarkus.redis.client.deployment.RedisTestResource; +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; + +@QuarkusTestResource(RedisTestResource.class) +public class MultiClientImportPreloadingWithOnlyIfEmptyTest { + + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset( + "quarkus.redis.hosts=${quarkus.redis.tr}\n" + + "quarkus.redis.load-script=import/my-import.redis\n" + + "quarkus.redis.my-redis.hosts=${quarkus.redis.tr}\n" + + "quarkus.redis.my-redis.load-script=sample.redis\n" + + // Do not erase as it's using the same database + // As the data base is not empty, the second load will be skipped + "quarkus.redis.my-redis.flush-before-load=false\n" + + "quarkus.redis.my-redis.load-only-if-empty=true"), + "application.properties") + .addAsResource(new File("src/test/resources/imports/import.redis"), "import/my-import.redis") + .addAsResource(new File("src/test/resources/imports/sample.redis"), "sample.redis") + + ); + + @Inject + RedisDataSource ds; + + @Inject + @RedisClientName("my-redis") + RedisDataSource my; + + @Test + void verifyImport() { + assertThat(my.key().keys("*")) + .containsOnly("foo", "bar", "key1", "key2", "key3", "key4"); + } +} diff --git a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/MultipleFilesForDefaultClientImportPreloadingTest.java b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/MultipleFilesForDefaultClientImportPreloadingTest.java new file mode 100644 index 0000000000000..0cb0388fafc3b --- /dev/null +++ b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/MultipleFilesForDefaultClientImportPreloadingTest.java @@ -0,0 +1,58 @@ +package io.quarkus.redis.client.deployment.preloading; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.io.File; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.redis.client.deployment.RedisTestResource; +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; + +@QuarkusTestResource(RedisTestResource.class) +public class MultipleFilesForDefaultClientImportPreloadingTest { + + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset( + "quarkus.redis.hosts=${quarkus.redis.tr}\n" + + "quarkus.redis.load-script=import/my-import.redis, sample.redis"), + "application.properties") + .addAsResource(new File("src/test/resources/imports/import.redis"), "import/my-import.redis") + .addAsResource(new File("src/test/resources/imports/sample.redis"), "sample.redis")); + + @Inject + RedisDataSource ds; + + @Test + void verifyImport() { + var keys = ds.key(); + var values = ds.value(String.class); + var hashes = ds.hash(String.class); + + assertThat(keys.keys("*")).containsExactlyInAnyOrder("foo", "bar", "key1", "key2", "key3", + "key4", "space:key", "counter", "key"); + + assertThat(hashes.hgetall("foo")).containsOnly(entry("field1", "abc"), entry("field2", "123")); + assertThat(hashes.hgetall("bar")).containsOnly(entry("field1", "abc def"), entry("field2", "123 456 ")); + + assertThat(values.get("key1")).isEqualTo("A value using \"double-quotes\""); + assertThat(values.get("key2")).isEqualTo("A value using 'single-quotes'"); + assertThat(values.get("key3")).isEqualTo("A value using a single single ' quote"); + assertThat(values.get("key4")).isEqualTo("A value using a single double \" quote"); + + assertThat(values.get("key")).isEqualTo("value"); + assertThat(values.get("space:key")).isEqualTo("another value"); + assertThat(values.get("counter")).isEqualTo("1"); + } +} diff --git a/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/NonDefaultFileForDefaultClientImportPreloadingTest.java b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/NonDefaultFileForDefaultClientImportPreloadingTest.java new file mode 100644 index 0000000000000..50e92df940138 --- /dev/null +++ b/extensions/redis-client/deployment/src/test/java/io/quarkus/redis/client/deployment/preloading/NonDefaultFileForDefaultClientImportPreloadingTest.java @@ -0,0 +1,52 @@ +package io.quarkus.redis.client.deployment.preloading; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.io.File; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.redis.client.deployment.RedisTestResource; +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; + +@QuarkusTestResource(RedisTestResource.class) +public class NonDefaultFileForDefaultClientImportPreloadingTest { + + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset( + "quarkus.redis.hosts=${quarkus.redis.tr}\n" + + "quarkus.redis.load-script=import/my-import.redis"), + "application.properties") + .addAsResource(new File("src/test/resources/imports/import.redis"), "import/my-import.redis")); + + @Inject + RedisDataSource ds; + + @Test + void verifyImport() { + var keys = ds.key(); + var values = ds.value(String.class); + var hashes = ds.hash(String.class); + + assertThat(keys.keys("*")).containsExactlyInAnyOrder("foo", "bar", "key1", "key2", "key3", "key4"); + + assertThat(hashes.hgetall("foo")).containsOnly(entry("field1", "abc"), entry("field2", "123")); + assertThat(hashes.hgetall("bar")).containsOnly(entry("field1", "abc def"), entry("field2", "123 456 ")); + + assertThat(values.get("key1")).isEqualTo("A value using \"double-quotes\""); + assertThat(values.get("key2")).isEqualTo("A value using 'single-quotes'"); + assertThat(values.get("key3")).isEqualTo("A value using a single single ' quote"); + assertThat(values.get("key4")).isEqualTo("A value using a single double \" quote"); + } +} diff --git a/extensions/redis-client/deployment/src/test/resources/imports/import.redis b/extensions/redis-client/deployment/src/test/resources/imports/import.redis new file mode 100644 index 0000000000000..59326a00ffa6f --- /dev/null +++ b/extensions/redis-client/deployment/src/test/resources/imports/import.redis @@ -0,0 +1,13 @@ +# Line starting with # and -- are ignored, as well as empty lines + +-- One command per line: +HSET foo field1 abc field2 123 + +-- Parameters with spaces must be wrapped into single or double quotes +HSET bar field1 "abc def" field2 '123 456 ' + +-- Parameters with double quotes must be wrapped into single quotes and the opposite +SET key1 'A value using "double-quotes"' +SET key2 "A value using 'single-quotes'" +SET key3 "A value using a single single ' quote" +SET key4 'A value using a single double " quote' diff --git a/extensions/redis-client/deployment/src/test/resources/imports/sample.redis b/extensions/redis-client/deployment/src/test/resources/imports/sample.redis new file mode 100644 index 0000000000000..c96451881fcbb --- /dev/null +++ b/extensions/redis-client/deployment/src/test/resources/imports/sample.redis @@ -0,0 +1,6 @@ +-- Run inside a transaction +MULTI +SET key value +SET space:key 'another value' +INCR counter +EXEC \ No newline at end of file diff --git a/extensions/redis-client/deployment/src/test/resources/imports/starwars.redis b/extensions/redis-client/deployment/src/test/resources/imports/starwars.redis new file mode 100644 index 0000000000000..2d165d3515a91 --- /dev/null +++ b/extensions/redis-client/deployment/src/test/resources/imports/starwars.redis @@ -0,0 +1,6 @@ +HSET people:1 firstName "luke" lastName "skywalker" +HSET people:2 firstName "leia" lastName "organa" +HSET people:3 firstName "anakin" lastName "skywalker" +HSET people:4 firstName "c-3po" lastName "" +HSET people:5 firstName "r2-d2" lastName "" +HSET people:6 firstName "owen" lastName "lars" \ No newline at end of file diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/ObservableRedis.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/ObservableRedis.java new file mode 100644 index 0000000000000..46dd791970b82 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/ObservableRedis.java @@ -0,0 +1,168 @@ +package io.quarkus.redis.runtime.client; + +import java.util.List; + +import io.vertx.codegen.annotations.Nullable; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.redis.client.Redis; +import io.vertx.redis.client.RedisConnection; +import io.vertx.redis.client.Request; +import io.vertx.redis.client.Response; + +/** + * An implementation of the {@link Redis} interface that tracks the duration of each operation for observability purpose. + */ +public class ObservableRedis implements Redis { + + private final Redis redis; + private final String name; + private final ObservableRedisMetrics reporter; + + public ObservableRedis(Redis redis, String name, ObservableRedisMetrics reporter) { + this.redis = redis; + this.name = name; + this.reporter = reporter == null ? ObservableRedisMetrics.NOOP : reporter; + } + + public String name() { + return name; + } + + private void report(long time, boolean succeeded) { + reporter.report(name, time, succeeded); + } + + @Override + public Redis connect(Handler> handler) { + this.redis.connect(ar -> { + if (ar.failed()) { + handler.handle(Future.failedFuture(ar.cause())); + } else { + handler.handle(Future.succeededFuture(new ObservableRedisConnection(ar.result()))); + } + }); + return this; + } + + @Override + public Redis send(Request command, Handler> onSend) { + long begin = System.nanoTime(); + redis.send(command, ar -> { + report(System.nanoTime() - begin, ar.succeeded()); + onSend.handle(ar); + }); + return this; + } + + @Override + public Redis batch(List commands, Handler>> onSend) { + long begin = System.nanoTime(); + redis.batch(commands, ar -> { + report(System.nanoTime() - begin, ar.succeeded()); + onSend.handle(ar); + }); + return this; + } + + @Override + public Future connect() { + return redis.connect() + .map(ObservableRedisConnection::new); + } + + @Override + public void close() { + redis.close(); + } + + @Override + public Future send(Request command) { + long begin = System.nanoTime(); + return redis.send(command) + .onComplete(x -> report(System.nanoTime() - begin, x.succeeded())); + } + + @Override + public Future> batch(List commands) { + long begin = System.nanoTime(); + return redis.batch(commands) + .onComplete(x -> report(System.nanoTime() - begin, x.succeeded())); + } + + private class ObservableRedisConnection implements RedisConnection { + + private final RedisConnection delegate; + + private ObservableRedisConnection(RedisConnection delegate) { + this.delegate = delegate; + } + + @Override + public RedisConnection exceptionHandler(Handler handler) { + delegate.exceptionHandler(handler); + return this; + } + + @Override + public RedisConnection handler(@Nullable Handler handler) { + delegate.handler(handler); + return this; + } + + @Override + public RedisConnection pause() { + delegate.pause(); + return this; + } + + @Override + public RedisConnection resume() { + delegate.resume(); + return this; + } + + @Override + public RedisConnection fetch(long amount) { + delegate.fetch(amount); + return this; + } + + @Override + public RedisConnection endHandler(@Nullable Handler endHandler) { + delegate.endHandler(endHandler); + return this; + } + + @Override + public Future send(Request command) { + long begin = System.nanoTime(); + return delegate.send(command) + .onComplete(ar -> { + long end = System.nanoTime(); + report(end - begin, ar.succeeded()); + }); + } + + @Override + public Future> batch(List commands) { + long begin = System.nanoTime(); + return delegate.batch(commands) + .onComplete(ar -> { + long end = System.nanoTime(); + report(end - begin, ar.succeeded()); + }); + } + + @Override + public Future close() { + return delegate.close(); + } + + @Override + public boolean pendingQueueFull() { + return delegate.pendingQueueFull(); + } + } +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/ObservableRedisMetrics.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/ObservableRedisMetrics.java new file mode 100644 index 0000000000000..814b28011a406 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/ObservableRedisMetrics.java @@ -0,0 +1,22 @@ +package io.quarkus.redis.runtime.client; + +public interface ObservableRedisMetrics { + + /** + * Method called by the {@link ObservableRedis} after every operation. + * + * @param name the client name + * @param durationInNs the duration of the operation in ns, it can represent the execution of a single command or + * a batch. + * @param succeeded whether the operation succeeded + */ + void report(String name, long durationInNs, boolean succeeded); + + ObservableRedisMetrics NOOP = new ObservableRedisMetrics() { + @Override + public void report(String name, long durationInNs, boolean succeeded) { + + } + }; + +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/RedisClientRecorder.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/RedisClientRecorder.java index 9d841ee1fcaa0..fcb1abfbc654c 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/RedisClientRecorder.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/RedisClientRecorder.java @@ -4,11 +4,15 @@ import java.time.Duration; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Supplier; +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.spi.CDI; + import io.quarkus.redis.client.RedisClient; import io.quarkus.redis.client.reactive.ReactiveRedisClient; import io.quarkus.redis.datasource.ReactiveRedisDataSource; @@ -22,8 +26,10 @@ import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigurationException; import io.vertx.mutiny.core.Vertx; +import io.vertx.mutiny.redis.client.Command; import io.vertx.mutiny.redis.client.Redis; import io.vertx.mutiny.redis.client.RedisAPI; +import io.vertx.mutiny.redis.client.Request; @Recorder public class RedisClientRecorder { @@ -34,25 +40,25 @@ public class RedisClientRecorder { private static final Map clients = new HashMap<>(); private static final Map dataSources = new HashMap<>(); private Vertx vertx; + private ObservableRedisMetrics metrics; public RedisClientRecorder(RedisConfig rc) { this.config = rc; } public void initialize(RuntimeValue vertx, Set names) { - this.vertx = Vertx.newInstance(vertx.getValue()); - _initialize(this.vertx, names); - } - - private void closeAllClients() { - for (Map.Entry entry : clients.entrySet()) { - entry.getValue().redis.close(); + Instance instance = CDI.current().select(ObservableRedisMetrics.class); + if (instance.isResolvable()) { + this.metrics = instance.get(); + } else { + this.metrics = null; } - clients.clear(); - dataSources.clear(); + + this.vertx = Vertx.newInstance(vertx.getValue()); + _initialize(vertx.getValue(), names); } - public void _initialize(Vertx vertx, Set names) { + public void _initialize(io.vertx.core.Vertx vertx, Set names) { for (String name : names) { // Search if we have an associated config: // - if default -> Default @@ -72,10 +78,11 @@ public ConfigurationException get() { } }); clients.computeIfAbsent(name, - x -> new RedisClientAndApi(VertxRedisClientFactory.create(name, vertx, actualConfig))); + x -> new RedisClientAndApi(name, VertxRedisClientFactory.create(name, vertx, actualConfig), metrics)); } else if (DEFAULT_CLIENT_NAME.equalsIgnoreCase(name) && maybe.isPresent()) { clients.computeIfAbsent(name, - x -> new RedisClientAndApi(VertxRedisClientFactory.create(DEFAULT_CLIENT_NAME, vertx, maybe.get()))); + x -> new RedisClientAndApi(name, + VertxRedisClientFactory.create(DEFAULT_CLIENT_NAME, vertx, maybe.get()), metrics)); } // Do not throw an error. We would need to check if the default redis client is used. } @@ -108,7 +115,7 @@ public Supplier getBareRedisClient(String name) { return new Supplier() { @Override public io.vertx.redis.client.Redis get() { - return clients.get(name).redis.getDelegate(); + return clients.get(name).observable; } }; } @@ -203,13 +210,38 @@ public void run() { }); } + public void preload(String name, List loadScriptPaths, boolean redisFlushBeforeLoad, boolean redisLoadOnlyIfEmpty) { + var tuple = clients.get(name); + if (tuple == null) { + throw new IllegalArgumentException("Unable import data into Redis - cannot find the Redis client " + name + + ", available clients are: " + clients.keySet()); + } + + if (redisFlushBeforeLoad) { + tuple.redis.send(Request.cmd(Command.FLUSHALL)).await().indefinitely(); + } else if (redisLoadOnlyIfEmpty) { + var list = tuple.redis.send(Request.cmd(Command.KEYS).arg("*")).await().indefinitely(); + if (list.size() != 0) { + RedisDataLoader.LOGGER.debugf( + "Skipping the Redis data loading because the database is not empty: %d keys found", list.size()); + return; + } + } + + for (String path : loadScriptPaths) { + RedisDataLoader.load(vertx, tuple.redis, path); + } + } + private static class RedisClientAndApi { private final Redis redis; private final RedisAPI api; + private final ObservableRedis observable; - private RedisClientAndApi(Redis redis) { - this.redis = redis; - this.api = RedisAPI.api(redis); + private RedisClientAndApi(String name, io.vertx.redis.client.Redis redis, ObservableRedisMetrics metrics) { + this.observable = new ObservableRedis(redis, name, metrics); + this.redis = Redis.newInstance(this.observable); + this.api = RedisAPI.api(this.redis); } } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/RedisDataLoader.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/RedisDataLoader.java new file mode 100644 index 0000000000000..5430aad6bf778 --- /dev/null +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/RedisDataLoader.java @@ -0,0 +1,136 @@ +package io.quarkus.redis.runtime.client; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.jboss.logging.Logger; + +import io.quarkus.runtime.configuration.ConfigurationException; +import io.vertx.mutiny.core.Vertx; +import io.vertx.mutiny.core.buffer.Buffer; +import io.vertx.mutiny.redis.client.Command; +import io.vertx.mutiny.redis.client.Redis; +import io.vertx.mutiny.redis.client.Request; + +public class RedisDataLoader { + + static final Logger LOGGER = Logger.getLogger("RedisDataLoader"); + + static void load(Vertx vertx, Redis redis, String path) { + LOGGER.infof("Importing Redis data from %s", path); + + Buffer buffer = vertx.fileSystem().readFileBlocking(path); + if (buffer == null) { + throw new ConfigurationException("Unable to read the " + path + " file"); + } + List batch = read(buffer.toString().lines().collect(Collectors.toList())); + redis.batch(batch).await().atMost(Duration.ofMinutes(1)); + } + + private enum State { + COMMAND, + ARGUMENTS, + PARAM, + PARAM_IN_QUOTES, + PARAM_IN_DOUBLE_QUOTES + } + + private static boolean isComment(String line) { + return line.startsWith("--") || line.startsWith("#"); + } + + private static List read(List lines) { + List requests = new ArrayList<>(); + int lineNumber = 0; + for (int i = 0; i < lines.size(); i++) { + lineNumber++; + var line = lines.get(i); + if (line.trim().length() != 0 && !isComment(line.trim())) { + var req = read(lineNumber, line.trim()); + if (req != null) { + requests.add(req); + } + } + } + return requests; + } + + private static Request read(int lineNumber, String line) { + State state = State.COMMAND; + + StringBuffer current = new StringBuffer(); + Request request = null; + + int pos = 0; + + for (char c : line.toCharArray()) { + pos++; + + if (state == State.COMMAND) { + if (Character.isSpaceChar(c)) { + request = Request.cmd(Command.create(getAndClear(current))); + state = State.ARGUMENTS; + } else { + current.append(c); + } + } else if (state == State.ARGUMENTS) { + if (!Character.isSpaceChar(c)) { + if (c == '\"') { + state = State.PARAM_IN_DOUBLE_QUOTES; + } else if (c == '\'') { + state = State.PARAM_IN_QUOTES; + } else { + state = State.PARAM; + current.append(c); + } + } + } else if (state == State.PARAM) { + if (!Character.isSpaceChar(c)) { + current.append(c); + } else { + request.arg(getAndClear(current)); + state = State.ARGUMENTS; + } + } else if (state == State.PARAM_IN_QUOTES) { + if (c != '\'') { + current.append(c); + } else { + request.arg(getAndClear(current)); + state = State.ARGUMENTS; + } + } else if (state == State.PARAM_IN_DOUBLE_QUOTES) { + if (c != '"') { + current.append(c); + } else { + request.arg(getAndClear(current)); + state = State.ARGUMENTS; + } + } else { + throw new IllegalStateException("Unexpected character at " + lineNumber + ":" + pos + + ", current state is " + state.name()); + } + } + + if (current.length() > 0) { + if (state == State.COMMAND) { + request = Request.cmd(Command.create(getAndClear(current))); + } else if (state == State.PARAM) { + request.arg(getAndClear(current)); + } else { + throw new IllegalStateException("End of line unexpected at " + lineNumber + ":" + pos); + } + } + + return request; + + } + + private static String getAndClear(StringBuffer buffer) { + var content = buffer.toString(); + buffer.setLength(0); + return content; + } + +} diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/VertxRedisClientFactory.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/VertxRedisClientFactory.java index fa61f05dd122b..c9131c9f1809e 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/VertxRedisClientFactory.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/VertxRedisClientFactory.java @@ -22,10 +22,10 @@ import io.quarkus.redis.runtime.client.config.TlsConfig; import io.quarkus.runtime.configuration.ConfigurationException; import io.smallrye.common.annotation.Identifier; +import io.vertx.core.Vertx; import io.vertx.core.net.NetClientOptions; import io.vertx.core.net.ProxyOptions; -import io.vertx.mutiny.core.Vertx; -import io.vertx.mutiny.redis.client.Redis; +import io.vertx.redis.client.Redis; import io.vertx.redis.client.RedisClientType; import io.vertx.redis.client.RedisOptions; @@ -76,7 +76,8 @@ public static Redis create(String name, Vertx vertx, RedisClientConfig config) { options.setPassword(config.password.orElse(null)); config.poolCleanerInterval.ifPresent(d -> options.setPoolCleanerInterval((int) d.toMillis())); options.setPoolRecycleTimeout((int) config.poolRecycleTimeout.toMillis()); - // TODO Pool name? + + options.setPoolName(name); config.role.ifPresent(options::setRole); options.setType(config.clientType); diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisClientConfig.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisClientConfig.java index 77ca63a9de075..7a193cd474a06 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisClientConfig.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisClientConfig.java @@ -25,7 +25,7 @@ public class RedisClientConfig { * * @see Redis scheme on www.iana.org */ - @ConfigItem(defaultValueDocumentation = "redis://localhost:6379", name = RedisConfig.HOSTS_CONFIG_NAME) + @ConfigItem(name = RedisConfig.HOSTS_CONFIG_NAME) public Optional> hosts; /** @@ -178,4 +178,5 @@ public String toString() { ", tls=" + tls + '}'; } + } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisConfig.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisConfig.java index 68cd8ddbce581..a393329ab8fd7 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisConfig.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisConfig.java @@ -53,4 +53,11 @@ public static boolean isDefaultClient(String name) { return DEFAULT_CLIENT_NAME.equalsIgnoreCase(name); } + public static String propertyKey(String name, String radical) { + String prefix = DEFAULT_CLIENT_NAME.equals(name) + ? "quarkus.redis." + : "quarkus.redis.\"" + name + "\"."; + return prefix + radical; + } + } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractGeoCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractGeoCommands.java index 9550a98db6542..04367f7dfb73a 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractGeoCommands.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractGeoCommands.java @@ -12,6 +12,7 @@ import java.util.OptionalDouble; import java.util.OptionalLong; import java.util.Set; +import java.util.regex.Pattern; import io.quarkus.redis.datasource.codecs.Codec; import io.quarkus.redis.datasource.codecs.Codecs; @@ -34,6 +35,8 @@ class AbstractGeoCommands extends AbstractRedisCommands { protected final Codec keyCodec; protected final Codec valueCodec; + private static final Pattern NOISE_REMOVER_PATTERN = Pattern.compile("[^a-zA-Z0-9\\.]"); + AbstractGeoCommands(RedisCommandExecutor redis, Class k, Class v) { super(redis, new Marshaller(k, v)); this.typeOfValue = v; @@ -233,7 +236,7 @@ Double decodeDistance(Response r) { if (r == null) { return null; } - return r.toDouble(); + return parseDouble(r); } List decodeGeoPositions(Response response) { @@ -241,7 +244,7 @@ List decodeGeoPositions(Response response) { if (nested == null) { return null; } else { - return GeoPosition.of(nested.get(0).toDouble(), nested.get(1).toDouble()); + return GeoPosition.of(parseDouble(nested.get(0)), parseDouble(nested.get(1))); } }); } @@ -261,33 +264,33 @@ List> decodeAsListOfGeoValues(Response r, boolean withDistance, bool V member = marshaller.decode(typeOfValue, response.get(0)); if (withCoordinates && withDistance && withHash) { - double dist = response.get(1).toDouble(); + double dist = parseDouble(response.get(1)); long hash = response.get(2).toLong(); - double longitude = response.get(3).get(0).toDouble(); - double latitude = response.get(3).get(1).toDouble(); + double longitude = parseDouble(response.get(3).get(0)); + double latitude = parseDouble(response.get(3).get(1)); list.add(new GeoValue<>(member, OptionalDouble.of(dist), OptionalLong.of(hash), OptionalDouble.of(longitude), OptionalDouble.of(latitude))); } else if (withCoordinates && withDistance) { - double dist = response.get(1).toDouble(); - double longitude = response.get(2).get(0).toDouble(); - double latitude = response.get(2).get(1).toDouble(); + double dist = parseDouble(response.get(1)); + double longitude = parseDouble(response.get(2).get(0)); + double latitude = parseDouble(response.get(2).get(1)); list.add(new GeoValue<>(member, OptionalDouble.of(dist), OptionalLong.empty(), OptionalDouble.of(longitude), OptionalDouble.of(latitude))); } else if (withCoordinates && withHash) { long hash = response.get(1).toLong(); - double longitude = response.get(2).get(0).toDouble(); - double latitude = response.get(2).get(1).toDouble(); + double longitude = parseDouble(response.get(2).get(0)); + double latitude = parseDouble(response.get(2).get(1)); list.add(new GeoValue<>(member, OptionalDouble.empty(), OptionalLong.of(hash), OptionalDouble.of(longitude), OptionalDouble.of(latitude))); } else if (withCoordinates) { // Only coordinates - double longitude = response.get(1).get(0).toDouble(); - double latitude = response.get(1).get(1).toDouble(); + double longitude = parseDouble(response.get(1).get(0)); + double latitude = parseDouble(response.get(1).get(1)); list.add(new GeoValue<>(member, OptionalDouble.empty(), OptionalLong.empty(), OptionalDouble.of(longitude), OptionalDouble.of(latitude))); } else if (withDistance && !withHash) { // Only distance - double dist = response.get(1).toDouble(); + double dist = parseDouble(response.get(1)); list.add(new GeoValue<>(member, OptionalDouble.of(dist), OptionalLong.empty(), OptionalDouble.empty(), OptionalDouble.empty())); } else if (!withDistance) { @@ -297,7 +300,7 @@ List> decodeAsListOfGeoValues(Response r, boolean withDistance, bool OptionalDouble.empty())); } else { // Distance and Hash - double dist = response.get(1).toDouble(); + double dist = parseDouble(response.get(1)); long hash = response.get(2).toLong(); list.add(new GeoValue<>(member, OptionalDouble.of(dist), OptionalLong.of(hash), OptionalDouble.empty(), OptionalDouble.empty())); @@ -305,4 +308,15 @@ List> decodeAsListOfGeoValues(Response r, boolean withDistance, bool } return list; } + + private static double parseDouble(Response response) { + double dist; + try { + dist = response.toDouble(); + } catch (NumberFormatException e) { + String s = NOISE_REMOVER_PATTERN.matcher(response.toString()).replaceAll(""); + dist = Double.parseDouble(s); + } + return dist; + } } diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java index 1d51c132fb2b1..753b343714968 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java @@ -455,7 +455,7 @@ void testAggregation() { .groupBy(new AggregateArgs.GroupBy().addProperty("@day").addProperty("@country").addReduceFunction("count", "num_visits")) .sortBy(new AggregateArgs.SortBy().ascending("@day").descending("@country"))); - assertThat(result.count()).isEqualTo(3); + assertThat(result.count()).isGreaterThanOrEqualTo(3); assertThat(result.documents()).allSatisfy(d -> { assertThat(d.property("day").asInteger()).isPositive(); assertThat(d.property("country").asString()).isNotNull(); diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java index c2ae9b2fc6df0..881a3fd2cef2d 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java @@ -31,6 +31,7 @@ public class RestClientConfig { EMPTY.proxyPassword = Optional.empty(); EMPTY.nonProxyHosts = Optional.empty(); EMPTY.queryParamStyle = Optional.empty(); + EMPTY.verifyHost = Optional.empty(); EMPTY.trustStore = Optional.empty(); EMPTY.trustStorePassword = Optional.empty(); EMPTY.trustStoreType = Optional.empty(); @@ -134,6 +135,12 @@ public class RestClientConfig { @ConfigItem public Optional queryParamStyle; + /** + * Set whether hostname verification is enabled. + */ + @ConfigItem + public Optional verifyHost; + /** * The trust store location. Can point to either a classpath resource or a file. */ @@ -246,6 +253,7 @@ public static RestClientConfig load(String configKey) { instance.proxyPassword = getConfigValue(configKey, "proxy-password", String.class); instance.nonProxyHosts = getConfigValue(configKey, "non-proxy-hosts", String.class); instance.queryParamStyle = getConfigValue(configKey, "query-param-style", QueryParamStyle.class); + instance.verifyHost = getConfigValue(configKey, "verify-host", Boolean.class); instance.trustStore = getConfigValue(configKey, "trust-store", String.class); instance.trustStorePassword = getConfigValue(configKey, "trust-store-password", String.class); instance.trustStoreType = getConfigValue(configKey, "trust-store-type", String.class); @@ -279,6 +287,7 @@ public static RestClientConfig load(Class interfaceClass) { instance.proxyPassword = getConfigValue(interfaceClass, "proxy-password", String.class); instance.nonProxyHosts = getConfigValue(interfaceClass, "non-proxy-hosts", String.class); instance.queryParamStyle = getConfigValue(interfaceClass, "query-param-style", QueryParamStyle.class); + instance.verifyHost = getConfigValue(interfaceClass, "verify-host", Boolean.class); instance.trustStore = getConfigValue(interfaceClass, "trust-store", String.class); instance.trustStorePassword = getConfigValue(interfaceClass, "trust-store-password", String.class); instance.trustStoreType = getConfigValue(interfaceClass, "trust-store-type", String.class); diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java index 02986f7bf1159..d407269ba7fb9 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java @@ -31,6 +31,7 @@ public class RestClientFallbackConfigSourceInterceptor extends FallbackConfigSou CLIENT_PROPERTIES.put("connect-timeout", "connectTimeout"); CLIENT_PROPERTIES.put("read-timeout", "readTimeout"); CLIENT_PROPERTIES.put("hostname-verifier", "hostnameVerifier"); + CLIENT_PROPERTIES.put("verify-host", "verifyHost"); CLIENT_PROPERTIES.put("trust-store", "trustStore"); CLIENT_PROPERTIES.put("trust-store-password", "trustStorePassword"); CLIENT_PROPERTIES.put("trust-store-type", "trustStoreType"); diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java index 99f4c9c117f9e..cd400ada1b8cf 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java @@ -217,6 +217,14 @@ public class RestClientsConfig { @ConfigItem public Optional queryParamStyle; + /** + * Set whether hostname verification is enabled. + * + * Can be overwritten by client-specific settings. + */ + @ConfigItem + public Optional verifyHost; + /** * The trust store location. Can point to either a classpath resource or a file. * diff --git a/extensions/resteasy-classic/rest-client/deployment/src/main/java/io/quarkus/restclient/deployment/RestClientPredicateProviderBuildItem.java b/extensions/resteasy-classic/rest-client/deployment/src/main/java/io/quarkus/restclient/deployment/RestClientPredicateProviderBuildItem.java new file mode 100644 index 0000000000000..93547db897f3e --- /dev/null +++ b/extensions/resteasy-classic/rest-client/deployment/src/main/java/io/quarkus/restclient/deployment/RestClientPredicateProviderBuildItem.java @@ -0,0 +1,36 @@ +package io.quarkus.restclient.deployment; + +import java.util.function.Predicate; + +import org.jboss.jandex.ClassInfo; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Register provider against every Rest client matching predicate. + */ +public final class RestClientPredicateProviderBuildItem extends MultiBuildItem { + + private final String providerClass; + private final Predicate matcher; + + /** + * Register JAX-RS client provider against Rest clients matching {@code matcher} condition. + */ + public RestClientPredicateProviderBuildItem(String providerClass, Predicate matcher) { + this.providerClass = providerClass; + this.matcher = matcher; + } + + public String getProviderClass() { + return providerClass; + } + + /** + * Test whether the {@link #providerClass} should be added to {@code restClientClassInfo} as provider. + */ + boolean appliesTo(ClassInfo restClientClassInfo) { + return matcher.test(restClientClassInfo); + } + +} diff --git a/extensions/resteasy-classic/rest-client/deployment/src/main/java/io/quarkus/restclient/deployment/RestClientProcessor.java b/extensions/resteasy-classic/rest-client/deployment/src/main/java/io/quarkus/restclient/deployment/RestClientProcessor.java index 537ba5c392865..42fc7d921fc62 100644 --- a/extensions/resteasy-classic/rest-client/deployment/src/main/java/io/quarkus/restclient/deployment/RestClientProcessor.java +++ b/extensions/resteasy-classic/rest-client/deployment/src/main/java/io/quarkus/restclient/deployment/RestClientProcessor.java @@ -10,6 +10,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import javax.enterprise.context.SessionScoped; @@ -174,6 +175,23 @@ UnremovableBeanBuildItem makeConfigUnremovable() { return UnremovableBeanBuildItem.beanTypes(RestClientsConfig.class); } + @BuildStep + List transformAnnotationProvider( + List annotationProviders) { + List result = new ArrayList<>(); + for (RestClientAnnotationProviderBuildItem annotationProvider : annotationProviders) { + result.add(new RestClientPredicateProviderBuildItem(annotationProvider.getProviderClass().getName(), + new Predicate() { + @Override + public boolean test(ClassInfo classInfo) { + // register the provider to every Rest client annotated with annotationName + return classInfo.hasAnnotation(annotationProvider.getAnnotationName()); + } + })); + } + return result; + } + @BuildStep void processInterfaces( CombinedIndexBuildItem combinedIndexBuildItem, @@ -181,7 +199,7 @@ void processInterfaces( Capabilities capabilities, Optional metricsCapability, PackageConfig packageConfig, - List restClientAnnotationProviders, + List restClientProviders, BuildProducer proxyDefinition, BuildProducer reflectiveClass, BuildProducer reflectiveHierarchy, @@ -244,28 +262,28 @@ void processInterfaces( configurator.addQualifier(REST_CLIENT); final Optional configKey = getConfigKey(entry.getValue()); final ScopeInfo scope = computeDefaultScope(capabilities, config, entry, configKey); - final List> annotationProviders = checkAnnotationProviders(entry.getValue(), - restClientAnnotationProviders); + final List clientProviders = checkRestClientProviders(entry.getValue(), + restClientProviders); configurator.scope(scope); configurator.creator(m -> { // return new RestClientBase(proxyType, baseUri).create(); ResultHandle interfaceHandle = m.loadClassFromTCCL(restClientName.toString()); ResultHandle baseUriHandle = m.load(getAnnotationParameter(entry.getValue(), "baseUri")); ResultHandle configKeyHandle = configKey.isPresent() ? m.load(configKey.get()) : m.loadNull(); - ResultHandle annotationProvidersHandle; - if (!annotationProviders.isEmpty()) { - annotationProvidersHandle = m.newArray(Class.class, annotationProviders.size()); - for (int i = 0; i < annotationProviders.size(); i++) { - m.writeArrayValue(annotationProvidersHandle, i, m.loadClassFromTCCL(annotationProviders.get(i))); + ResultHandle restClientProvidersHandle; + if (!clientProviders.isEmpty()) { + restClientProvidersHandle = m.newArray(Class.class, clientProviders.size()); + for (int i = 0; i < clientProviders.size(); i++) { + m.writeArrayValue(restClientProvidersHandle, i, m.loadClassFromTCCL(clientProviders.get(i))); } } else { - annotationProvidersHandle = m.loadNull(); + restClientProvidersHandle = m.loadNull(); } ResultHandle baseHandle = m.newInstance( MethodDescriptor.ofConstructor(RestClientBase.class, Class.class, String.class, String.class, Class[].class), - interfaceHandle, baseUriHandle, configKeyHandle, annotationProvidersHandle); + interfaceHandle, baseUriHandle, configKeyHandle, restClientProvidersHandle); ResultHandle ret = m.invokeVirtualMethod( MethodDescriptor.ofMethod(RestClientBase.class, "create", Object.class), baseHandle); m.returnValue(ret); @@ -292,9 +310,9 @@ private boolean isRequired(Capabilities capabilities, && metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER))); } - private static List> checkAnnotationProviders(ClassInfo classInfo, - List restClientAnnotationProviders) { - return restClientAnnotationProviders.stream().filter(p -> (classInfo.classAnnotation(p.getAnnotationName()) != null)) + private static List checkRestClientProviders(ClassInfo classInfo, + List restClientProviders) { + return restClientProviders.stream().filter(p -> p.appliesTo(classInfo)) .map(p -> p.getProviderClass()).collect(Collectors.toList()); } diff --git a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java index 5a8136fd83e28..d96ca2680894c 100644 --- a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java +++ b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java @@ -57,7 +57,6 @@ import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; -import org.graalvm.nativeimage.ImageInfo; import org.jboss.logging.Logger; import org.jboss.resteasy.client.jaxrs.ResteasyClient; import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; @@ -84,6 +83,7 @@ import io.quarkus.restclient.NoopHostnameVerifier; import io.quarkus.resteasy.common.runtime.QuarkusInjectorFactory; +import io.quarkus.runtime.ImageMode; import io.quarkus.runtime.graal.DisabledSSLContext; import io.quarkus.runtime.ssl.SslContextConfiguration; @@ -324,7 +324,7 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi configureTrustAll(resteasyClientBuilder); // we need to push a disabled SSL context when SSL has been disabled // because otherwise Apache HTTP Client will try to initialize one and will fail - if (ImageInfo.inImageRuntimeCode() && !SslContextConfiguration.isSslNativeEnabled()) { + if (ImageMode.current() == ImageMode.NATIVE_RUN && !SslContextConfiguration.isSslNativeEnabled()) { resteasyClientBuilder.sslContext(new DisabledSSLContext()); } diff --git a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/RestClientBase.java b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/RestClientBase.java index 4380c9ef7b333..df5ccb883169e 100644 --- a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/RestClientBase.java +++ b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/RestClientBase.java @@ -24,6 +24,7 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.InstanceHandle; +import io.quarkus.restclient.NoopHostnameVerifier; import io.quarkus.restclient.config.RestClientConfig; import io.quarkus.restclient.config.RestClientsConfig; @@ -35,22 +36,22 @@ public class RestClientBase { private final Class proxyType; private final String baseUriFromAnnotation; - private final Class[] annotationProviders; + private final Class[] clientProviders; private final RestClientsConfig configRoot; private final String configKey; public RestClientBase(Class proxyType, String baseUriFromAnnotation, String configKey, - Class[] annotationProviders) { - this(proxyType, baseUriFromAnnotation, configKey, annotationProviders, + Class[] clientProviders) { + this(proxyType, baseUriFromAnnotation, configKey, clientProviders, RestClientsConfig.getInstance()); } RestClientBase(Class proxyType, String baseUriFromAnnotation, String configKey, - Class[] annotationProviders, RestClientsConfig configRoot) { + Class[] clientProviders, RestClientsConfig configRoot) { this.proxyType = proxyType; this.baseUriFromAnnotation = baseUriFromAnnotation; this.configKey = configKey; - this.annotationProviders = annotationProviders; + this.clientProviders = clientProviders; this.configRoot = configRoot; } @@ -149,6 +150,13 @@ protected void configureSsl(RestClientBuilder builder) { clientConfigByConfigKey().hostnameVerifier, configRoot.hostnameVerifier); if (hostnameVerifier.isPresent()) { registerHostnameVerifier(hostnameVerifier.get(), builder); + } else { + // If `verify-host` is disabled, we configure the client using the `NoopHostnameVerifier` verifier. + Optional verifyHost = oneOf(clientConfigByClassName().verifyHost, clientConfigByConfigKey().verifyHost, + configRoot.verifyHost); + if (verifyHost.isPresent() && !verifyHost.get()) { + registerHostnameVerifier(NoopHostnameVerifier.class.getName(), builder); + } } } @@ -256,8 +264,8 @@ protected void configureProviders(RestClientBuilder builder) { if (providers.isPresent()) { registerProviders(builder, providers.get()); } - if (annotationProviders != null) { - for (Class annotationProvider : annotationProviders) { + if (clientProviders != null) { + for (Class annotationProvider : clientProviders) { builder.register(annotationProvider); } } diff --git a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java index 7a8a444f88f68..a63334d0cbdf1 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java @@ -5,6 +5,12 @@ import java.util.Optional; +import javax.ws.rs.ext.ExceptionMapper; + +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.Type; + import io.quarkus.builder.item.SimpleBuildItem; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -14,19 +20,27 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.ExecutorBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.resteasy.common.deployment.ResteasyInjectionReadyBuildItem; +import io.quarkus.resteasy.runtime.AuthenticationCompletionExceptionMapper; +import io.quarkus.resteasy.runtime.AuthenticationFailedExceptionMapper; +import io.quarkus.resteasy.runtime.AuthenticationRedirectExceptionMapper; import io.quarkus.resteasy.runtime.ResteasyVertxConfig; import io.quarkus.resteasy.runtime.standalone.ResteasyStandaloneRecorder; import io.quarkus.resteasy.server.common.deployment.ResteasyDeploymentBuildItem; +import io.quarkus.security.AuthenticationCompletionException; +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.AuthenticationRedirectException; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; import io.quarkus.vertx.http.deployment.DefaultRouteBuildItem; import io.quarkus.vertx.http.deployment.FilterBuildItem; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.RequireVirtualHttpBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.VertxHttpRecorder; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; @@ -34,6 +48,7 @@ public class ResteasyStandaloneBuildStep { private static final int REST_ROUTE_ORDER_OFFSET = 500; + private static final DotName EXCEPTION_MAPPER = DotName.createSimple(ExceptionMapper.class.getName()); public static final class ResteasyStandaloneBuildItem extends SimpleBuildItem { @@ -75,6 +90,8 @@ public void boot(ShutdownContextBuildItem shutdown, BuildProducer routes, BuildProducer filterBuildItemBuildProducer, CoreVertxBuildItem vertx, + CombinedIndexBuildItem combinedIndexBuildItem, + HttpBuildTimeConfig vertxConfig, ResteasyStandaloneBuildItem standalone, Optional requireVirtual, ExecutorBuildItem executorBuildItem, @@ -90,11 +107,29 @@ public void boot(ShutdownContextBuildItem shutdown, Handler handler = recorder.vertxRequestHandler(vertx.getVertx(), executorBuildItem.getExecutorProxy(), resteasyVertxConfig); + final boolean noCustomAuthCompletionExMapper; + final boolean noCustomAuthFailureExMapper; + final boolean noCustomAuthRedirectExMapper; + if (vertxConfig.auth.proactive) { + noCustomAuthCompletionExMapper = notFoundCustomExMapper(AuthenticationCompletionException.class.getName(), + AuthenticationCompletionExceptionMapper.class.getName(), combinedIndexBuildItem.getIndex()); + noCustomAuthFailureExMapper = notFoundCustomExMapper(AuthenticationFailedException.class.getName(), + AuthenticationFailedExceptionMapper.class.getName(), combinedIndexBuildItem.getIndex()); + noCustomAuthRedirectExMapper = notFoundCustomExMapper(AuthenticationRedirectException.class.getName(), + AuthenticationRedirectExceptionMapper.class.getName(), combinedIndexBuildItem.getIndex()); + } else { + // with disabled proactive auth we need to handle exceptions anyway as default auth failure handler did not + noCustomAuthCompletionExMapper = false; + noCustomAuthFailureExMapper = false; + noCustomAuthRedirectExMapper = false; + } // failure handler for auth failures that occurred before the handler defined right above started processing the request + // we add the failure handler right before QuarkusErrorHandler + // so that user can define failure handlers that precede exception mappers final Handler failureHandler = recorder.vertxFailureHandler(vertx.getVertx(), - executorBuildItem.getExecutorProxy(), resteasyVertxConfig); - filterBuildItemBuildProducer.produce(new FilterBuildItem(failureHandler, - VertxHttpRecorder.AFTER_DEFAULT_ROUTE_ORDER_MARK + REST_ROUTE_ORDER_OFFSET, true)); + executorBuildItem.getExecutorProxy(), resteasyVertxConfig, noCustomAuthCompletionExMapper, + noCustomAuthFailureExMapper, noCustomAuthRedirectExMapper, vertxConfig.auth.proactive); + filterBuildItemBuildProducer.produce(FilterBuildItem.ofAuthenticationFailureHandler(failureHandler)); // Exact match for resources matched to the root path routes.produce( @@ -116,6 +151,24 @@ public void boot(ShutdownContextBuildItem shutdown, recorder.start(shutdown, requireVirtual.isPresent()); } + private static boolean notFoundCustomExMapper(String exSignatureStr, String exMapperSignatureStr, IndexView index) { + for (var implementor : index.getAllKnownImplementors(EXCEPTION_MAPPER)) { + if (exMapperSignatureStr.equals(implementor.name().toString())) { + continue; + } + for (Type interfaceType : implementor.interfaceTypes()) { + if (EXCEPTION_MAPPER.equals(interfaceType.name())) { + final String mapperExSignature = interfaceType.asParameterizedType().arguments().get(0).name().toString(); + if (exSignatureStr.equals(mapperExSignature)) { + return false; + } + break; + } + } + } + return true; + } + @BuildStep @Record(value = ExecutionTime.STATIC_INIT) public FilterBuildItem addDefaultAuthFailureHandler(ResteasyStandaloneRecorder recorder) { diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationFailedExceptionHeaderTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationFailedExceptionHeaderTest.java new file mode 100644 index 0000000000000..740494074d9d6 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationFailedExceptionHeaderTest.java @@ -0,0 +1,100 @@ +package io.quarkus.resteasy.test.security; + +import static io.vertx.core.http.HttpHeaders.LOCATION; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.BaseAuthenticationRequest; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.restassured.RestAssured; +import io.restassured.http.Header; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +public class AuthenticationFailedExceptionHeaderTest { + + private static final String APP_PROPS = "" + + "quarkus.http.auth.permission.default.paths=/*\n" + + "quarkus.http.auth.permission.default.policy=authenticated"; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); + + @Test + public void testHeaders() { + // case-insensitive test that there is only one location header + // there has been duplicate location when both default auth failure handler and auth ex mapper send challenge + var response = RestAssured + .given() + .redirects() + .follow(false) + .when() + .get("/secured-route"); + response.then().statusCode(302); + assertEquals(1, response.headers().asList().stream().map(Header::getName).map(String::toLowerCase) + .filter(LOCATION.toString()::equals).count()); + } + + @Path("/hello") + public static class HelloResource { + @GET + public String hello() { + return "hello"; + } + } + + @ApplicationScoped + public static class FailingAuthenticator implements HttpAuthenticationMechanism { + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + return Uni.createFrom().failure(new AuthenticationFailedException()); + } + + @Override + public Set> getCredentialTypes() { + return Set.of(BaseAuthenticationRequest.class); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().item(new ChallengeData(302, LOCATION, "http://localhost:8080/")); + } + + } + + @ApplicationScoped + public static class BasicIdentityProvider implements IdentityProvider { + + @Override + public Class getRequestType() { + return BaseAuthenticationRequest.class; + } + + @Override + public Uni authenticate( + BaseAuthenticationRequest simpleAuthenticationRequest, + AuthenticationRequestContext authenticationRequestContext) { + return Uni.createFrom().nothing(); + } + } +} diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationRedirectExceptionHeaderTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationRedirectExceptionHeaderTest.java new file mode 100644 index 0000000000000..b46efe9d16aef --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationRedirectExceptionHeaderTest.java @@ -0,0 +1,109 @@ +package io.quarkus.resteasy.test.security; + +import static io.vertx.core.http.HttpHeaders.CACHE_CONTROL; +import static io.vertx.core.http.HttpHeaders.LOCATION; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationRedirectException; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.BaseAuthenticationRequest; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.restassured.RestAssured; +import io.restassured.http.Header; +import io.restassured.response.Response; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +public class AuthenticationRedirectExceptionHeaderTest { + + private static final String APP_PROPS = "" + + "quarkus.http.auth.permission.default.paths=/*\n" + + "quarkus.http.auth.permission.default.policy=authenticated"; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); + + @Test + public void testHeaders() { + // case-insensitive test that Pragma, cache-control and location headers are only present once + // there were duplicate headers when both default auth failure handler and auth ex mapper set headers + var response = RestAssured + .given() + .redirects() + .follow(false) + .when() + .get("/secured-route"); + response.then().statusCode(302); + assertEquals(1, getHeaderCount(response, LOCATION.toString())); + assertEquals(1, getHeaderCount(response, CACHE_CONTROL.toString())); + assertEquals(1, getHeaderCount(response, "Pragma")); + } + + private static int getHeaderCount(Response response, String headerName) { + headerName = headerName.toLowerCase(); + return (int) response.headers().asList().stream().map(Header::getName).map(String::toLowerCase) + .filter(headerName::equals).count(); + } + + @Path("/hello") + public static class HelloResource { + @GET + public String hello() { + return "hello"; + } + } + + @ApplicationScoped + public static class RedirectingAuthenticator implements HttpAuthenticationMechanism { + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + return Uni.createFrom().failure(new AuthenticationRedirectException(302, "https://quarkus.io/")); + } + + @Override + public Set> getCredentialTypes() { + return Set.of(BaseAuthenticationRequest.class); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().item(new ChallengeData(302, "header-name", "header-value")); + } + + } + + @ApplicationScoped + public static class BasicIdentityProvider implements IdentityProvider { + + @Override + public Class getRequestType() { + return BaseAuthenticationRequest.class; + } + + @Override + public Uni authenticate( + BaseAuthenticationRequest simpleAuthenticationRequest, + AuthenticationRequestContext authenticationRequestContext) { + return Uni.createFrom().nothing(); + } + } +} diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionHandlerTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionHandlerTest.java index ac93652557433..44dbffff92614 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionHandlerTest.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionHandlerTest.java @@ -61,9 +61,6 @@ public void testAuthCompletionExMapper() { .body(Matchers.equalTo(AUTHENTICATION_COMPLETION_EX)); } - /** - * Use failure handler as when proactive security is enabled, JAX-RS exception mappers won't do. - */ @ApplicationScoped public static final class CustomAuthCompletionExceptionHandler { diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionMapperTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionMapperTest.java new file mode 100644 index 0000000000000..f5372c7166fb1 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionMapperTest.java @@ -0,0 +1,86 @@ +package io.quarkus.resteasy.test.security; + +import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; + +import java.util.function.Supplier; + +import javax.annotation.Priority; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Priorities; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationCompletionException; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.filter.cookie.CookieFilter; + +public class ProactiveAuthCompletionExceptionMapperTest { + + private static final String AUTHENTICATION_COMPLETION_EX = "AuthenticationCompletionException"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestIdentityController.class) + .addAsResource(new StringAsset("quarkus.http.auth.form.enabled=true\n"), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("a d m i n", "a d m i n", "a d m i n"); + } + + @Test + public void testAuthCompletionExMapper() { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + RestAssured + .given() + .filter(new CookieFilter()) + .redirects().follow(false) + .when() + .formParam("j_username", "a d m i n") + .formParam("j_password", "a d m i n") + .cookie("quarkus-redirect-location", "https://quarkus.io/guides") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(401) + .body(Matchers.equalTo(AUTHENTICATION_COMPLETION_EX)); + } + + @Path("/hello") + public static class HelloResource { + + @GET + public String hello() { + return "Hello"; + } + + } + + @Priority(Priorities.USER) + @Provider + public static class CustomAuthCompletionExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(AuthenticationCompletionException e) { + return Response.status(UNAUTHORIZED).entity(AUTHENTICATION_COMPLETION_EX).build(); + } + } +} diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthHttpPolicyCustomForbiddenExHandlerTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthHttpPolicyCustomForbiddenExHandlerTest.java new file mode 100644 index 0000000000000..34f90cd676b0e --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthHttpPolicyCustomForbiddenExHandlerTest.java @@ -0,0 +1,92 @@ +package io.quarkus.resteasy.test.security; + +import static io.quarkus.resteasy.test.security.ProactiveAuthHttpPolicyCustomForbiddenExHandlerTest.CustomForbiddenFailureHandler.CUSTOM_FORBIDDEN_EXCEPTION_HANDLER; +import static javax.ws.rs.core.Response.Status.FORBIDDEN; +import static org.hamcrest.Matchers.equalTo; + +import java.util.function.Supplier; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.vertx.core.Handler; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; + +public class ProactiveAuthHttpPolicyCustomForbiddenExHandlerTest { + + private static final String PROPERTIES = "quarkus.http.auth.basic=true\n" + + "quarkus.http.auth.policy.user-policy.roles-allowed=user\n" + + "quarkus.http.auth.permission.roles.paths=/secured\n" + + "quarkus.http.auth.permission.roles.policy=user-policy"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestIdentityController.class) + .addAsResource(new StringAsset(PROPERTIES), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("a d m i n", "a d m i n", "a d m i n"); + } + + @Test + public void testDeniedAccessAdminResource() { + RestAssured.given() + .auth().basic("a d m i n", "a d m i n") + .when().get("/secured") + .then() + .statusCode(403) + .body(equalTo(CUSTOM_FORBIDDEN_EXCEPTION_HANDLER)); + } + + @Path("/secured") + public static class SecuredResource { + + @GET + public String get() { + throw new IllegalStateException(); + } + + } + + @ApplicationScoped + public static final class CustomForbiddenFailureHandler { + + public static final String CUSTOM_FORBIDDEN_EXCEPTION_HANDLER = CustomForbiddenFailureHandler.class.getName(); + + public void init(@Observes Router router) { + router.route().failureHandler(new Handler() { + @Override + public void handle(RoutingContext event) { + if (event.failure() instanceof ForbiddenException) { + event.response().setStatusCode(FORBIDDEN.getStatusCode()).end(CUSTOM_FORBIDDEN_EXCEPTION_HANDLER); + } else { + event.next(); + } + } + }); + } + + } + +} diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/resources/cors-config.properties b/extensions/resteasy-classic/resteasy/deployment/src/test/resources/cors-config.properties index aa8d0000a7867..3f6f798ab006f 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/test/resources/cors-config.properties +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/resources/cors-config.properties @@ -1 +1,2 @@ -quarkus.http.cors=true \ No newline at end of file +quarkus.http.cors=true +quarkus.http.cors.origins=* diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java index 253be4d73b8ab..9a290ac4a2c11 100644 --- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java @@ -78,7 +78,9 @@ public Handler vertxRequestHandler(Supplier vertx, Execut return null; } - public Handler vertxFailureHandler(Supplier vertx, Executor executor, ResteasyVertxConfig config) { + public Handler vertxFailureHandler(Supplier vertx, Executor executor, ResteasyVertxConfig config, + boolean noCustomAuthCompletionExMapper, boolean noCustomAuthFailureExMapper, boolean noCustomAuthRedirectExMapper, + boolean proactive) { if (deployment == null) { return null; } else { @@ -90,6 +92,40 @@ public Handler vertxFailureHandler(Supplier vertx, Execut @Override public void handle(RoutingContext request) { + + // special handling when proactive auth is enabled as then we know default auth failure handler already run + if (proactive && request.get(QuarkusHttpUser.AUTH_FAILURE_HANDLER) instanceof DefaultAuthFailureHandler) { + // we want to prevent repeated handling of exceptions if user don't want to handle exception himself + // we do not pass exception to abort handlers if proactive auth is enabled and user did not + // provide custom ex. mapper; we replace default auth failure handler as soon as we can, so that + // we can handle Quarkus Security Exceptions ourselves + if (request.failure() instanceof AuthenticationFailedException) { + if (noCustomAuthFailureExMapper) { + request.next(); + } else { + // allow response customization + super.handle(request); + } + return; + } else if (request.failure() instanceof AuthenticationCompletionException) { + if (noCustomAuthCompletionExMapper) { + request.next(); + } else { + // allow response customization + super.handle(request); + } + return; + } else if (request.failure() instanceof AuthenticationRedirectException) { + if (noCustomAuthRedirectExMapper) { + request.next(); + } else { + // allow response customization + super.handle(request); + } + return; + } + } + if (request.failure() instanceof AuthenticationFailedException || request.failure() instanceof AuthenticationCompletionException || request.failure() instanceof AuthenticationRedirectException 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 39cfc80d38003..86edc4adff49d 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 @@ -1161,8 +1161,31 @@ private void handleSubResourceMethod(List ownerContext.constructor.writeInstanceField(forMethodTargetDesc, ownerContext.constructor.getThis(), constructorTarget); - ResultHandle subInstance = ownerMethod.newInstance(subConstructorDescriptor, - ownerMethod.readInstanceField(forMethodTargetDesc, ownerMethod.getThis())); + Supplier methodParamAnnotationsField = ownerContext.getLazyJavaMethodParamAnnotationsField( + methodIndex); + Supplier methodGenericParametersField = ownerContext.getLazyJavaMethodGenericParametersField( + methodIndex); + + AssignableResultHandle client = createRestClientField(name, ownerContext.classCreator, ownerMethod); + AssignableResultHandle webTarget = ownerMethod.createVariable(WebTarget.class); + ownerMethod.assign(webTarget, ownerMethod.readInstanceField(forMethodTargetDesc, ownerMethod.getThis())); + // Setup Path param from current method + for (int i = 0; i < method.getParameters().length; i++) { + MethodParameter param = method.getParameters()[i]; + if (param.parameterType == ParameterType.PATH) { + ResultHandle paramValue = ownerMethod.getMethodParam(i); + // methodTarget = methodTarget.resolveTemplate(paramname, paramvalue); + addPathParam(ownerMethod, webTarget, param.name, paramValue, + param.type, + client, + ownerMethod.readStaticField(methodGenericParametersField.get()), + ownerMethod.readStaticField(methodParamAnnotationsField.get()), + i); + } + } + + // Continue creating the subresource instance with the web target updated + ResultHandle subInstance = ownerMethod.newInstance(subConstructorDescriptor, webTarget); List subParamFields = new ArrayList<>(); @@ -1179,23 +1202,24 @@ private void handleSubResourceMethod(List ownerParameter.paramIndex)); } - FieldDescriptor clientField = createRestClientField(name, ownerContext.classCreator, ownerMethod, - subContext.classCreator, subInstance); - - Supplier methodParamAnnotationsField = ownerContext.getLazyJavaMethodParamAnnotationsField( - methodIndex); - Supplier methodGenericParametersField = ownerContext.getLazyJavaMethodGenericParametersField( - methodIndex); - // method parameters are rewritten to sub client fields (directly, public fields): + FieldDescriptor clientField = subContext.classCreator.getFieldCreator("client", RestClientBase.class) + .setModifiers(Modifier.PUBLIC) + .getFieldDescriptor(); + ownerMethod.writeInstanceField(clientField, subInstance, client); + // method parameters (except path parameters) are rewritten to sub client fields (directly, public fields): for (int i = 0; i < method.getParameters().length; i++) { - FieldDescriptor paramField = subContext.classCreator.getFieldCreator("param" + i, - method.getParameters()[i].type) - .setModifiers(Modifier.PUBLIC) - .getFieldDescriptor(); - ownerMethod.writeInstanceField(paramField, subInstance, ownerMethod.getMethodParam(i)); - subParamFields.add(new SubResourceParameter(method.getParameters()[i], method.getParameters()[i].type, - jandexMethod.parameterType(i), paramField, methodParamAnnotationsField, methodGenericParametersField, - i)); + MethodParameter param = method.getParameters()[i]; + if (param.parameterType != ParameterType.PATH) { + FieldDescriptor paramField = subContext.classCreator.getFieldCreator("param" + i, param.type) + .setModifiers(Modifier.PUBLIC) + .getFieldDescriptor(); + ownerMethod.writeInstanceField(paramField, subInstance, ownerMethod.getMethodParam(i)); + subParamFields.add(new SubResourceParameter(method.getParameters()[i], param.type, + jandexMethod.parameterType(i), paramField, methodParamAnnotationsField, + methodGenericParametersField, + i)); + } + } int subMethodIndex = 0; @@ -1555,22 +1579,19 @@ private void appendPath(MethodCreator constructor, String pathPart, AssignableRe * Create the `client` field into the `c` class that represents a RestClientBase instance. * The RestClientBase instance is coming from either a root client or a sub client (clients generated from root clients). */ - private FieldDescriptor createRestClientField(String name, ClassCreator c, MethodCreator methodCreator, ClassCreator sub, - ResultHandle subInstance) { - FieldDescriptor clientField = sub.getFieldCreator("client", RestClientBase.class) - .setModifiers(Modifier.PUBLIC) - .getFieldDescriptor(); + private AssignableResultHandle createRestClientField(String name, ClassCreator c, MethodCreator methodCreator) { + AssignableResultHandle client = methodCreator.createVariable(RestClientBase.class); if (c.getSuperClass().contains(RestClientBase.class.getSimpleName())) { // We're in a root client, so we can set the client field with: sub.client = (RestClientBase) this - methodCreator.writeInstanceField(clientField, subInstance, methodCreator.getThis()); + methodCreator.assign(client, methodCreator.getThis()); } else { FieldDescriptor subClientField = FieldDescriptor.of(name, "client", RestClientBase.class); - // We're in a sub sub resource, so we need to get the client from the field: subSub.client = sub.client - methodCreator.writeInstanceField(clientField, subInstance, - methodCreator.readInstanceField(subClientField, methodCreator.getThis())); + // We're in a sub-sub resource, so we need to get the client from the field: subSub.client = sub.client + methodCreator.assign(client, methodCreator.readInstanceField(subClientField, methodCreator.getThis())); } - return clientField; + + return client; } private void handleMultipartField(String formParamName, String partType, String partFilename, diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java index 0c3c5b8ad89e5..fa92296e337b1 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java @@ -187,7 +187,7 @@ protected void registerInterceptors( if (filterItem instanceof ContainerRequestFilterBuildItem) { ContainerRequestFilterBuildItem crfbi = (ContainerRequestFilterBuildItem) filterItem; interceptor.setNonBlockingRequired(crfbi.isNonBlockingRequired()); - interceptor.setReadBody(crfbi.isReadBody()); + interceptor.setWithFormRead(crfbi.isWithFormRead()); MethodInfo filterSourceMethod = crfbi.getFilterSourceMethod(); if (filterSourceMethod != null) { interceptor.metadata = Map.of(FILTER_SOURCE_METHOD_METADATA_KEY, filterSourceMethod); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ContainerRequestFilterBuildItem.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ContainerRequestFilterBuildItem.java index 6ae524f8145e2..7f7a1faed4c92 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ContainerRequestFilterBuildItem.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ContainerRequestFilterBuildItem.java @@ -6,7 +6,7 @@ public final class ContainerRequestFilterBuildItem extends AbstractInterceptorBu private final boolean preMatching; private final boolean nonBlockingRequired; - private final boolean readBody; + private final boolean withFormRead; private final MethodInfo filterSourceMethod; @@ -14,7 +14,7 @@ protected ContainerRequestFilterBuildItem(Builder builder) { super(builder); this.preMatching = builder.preMatching; this.nonBlockingRequired = builder.nonBlockingRequired; - this.readBody = builder.readBody; + this.withFormRead = builder.withFormRead; this.filterSourceMethod = builder.filterSourceMethod; } @@ -22,7 +22,7 @@ public ContainerRequestFilterBuildItem(String className) { super(className); this.preMatching = false; this.nonBlockingRequired = false; - this.readBody = false; + this.withFormRead = false; this.filterSourceMethod = null; } @@ -34,8 +34,8 @@ public boolean isNonBlockingRequired() { return nonBlockingRequired; } - public boolean isReadBody() { - return readBody; + public boolean isWithFormRead() { + return withFormRead; } public MethodInfo getFilterSourceMethod() { @@ -45,7 +45,7 @@ public MethodInfo getFilterSourceMethod() { public static final class Builder extends AbstractInterceptorBuildItem.Builder { boolean preMatching = false; boolean nonBlockingRequired = false; - boolean readBody = false; + boolean withFormRead = false; MethodInfo filterSourceMethod = null; @@ -63,8 +63,8 @@ public Builder setNonBlockingRequired(boolean nonBlockingRequired) { return this; } - public Builder setReadBody(boolean readBody) { - this.readBody = readBody; + public Builder setWithFormRead(boolean withFormRead) { + this.withFormRead = withFormRead; return this; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ExceptionMapperBuildItem.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ExceptionMapperBuildItem.java index 727213f77799f..1479091055640 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ExceptionMapperBuildItem.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ExceptionMapperBuildItem.java @@ -1,5 +1,7 @@ package io.quarkus.resteasy.reactive.spi; +import org.jboss.jandex.ClassInfo; + import io.quarkus.builder.item.MultiBuildItem; public final class ExceptionMapperBuildItem extends MultiBuildItem implements CheckBean { @@ -8,12 +10,14 @@ public final class ExceptionMapperBuildItem extends MultiBuildItem implements Ch private final Integer priority; private final String handledExceptionName; private final boolean registerAsBean; + private final ClassInfo declaringClass; public ExceptionMapperBuildItem(String className, String handledExceptionName, Integer priority, boolean registerAsBean) { this.className = className; this.priority = priority; this.handledExceptionName = handledExceptionName; this.registerAsBean = registerAsBean; + this.declaringClass = null; } private ExceptionMapperBuildItem(Builder builder) { @@ -21,6 +25,7 @@ private ExceptionMapperBuildItem(Builder builder) { this.handledExceptionName = builder.handledExceptionName; this.priority = builder.priority; this.registerAsBean = builder.registerAsBean; + this.declaringClass = builder.declaringClass; } public String getClassName() { @@ -40,6 +45,10 @@ public boolean isRegisterAsBean() { return registerAsBean; } + public ClassInfo getDeclaringClass() { + return declaringClass; + } + public static class Builder { private final String className; private final String handledExceptionName; @@ -47,6 +56,12 @@ public static class Builder { private Integer priority; private boolean registerAsBean = true; + /** + * Used to track the class that resulted in the registration of the exception mapper. + * This is only set for exception mappers created from {@code @ServerExceptionMapper} + */ + private ClassInfo declaringClass; + public Builder(String className, String handledExceptionName) { this.className = className; this.handledExceptionName = handledExceptionName; @@ -62,6 +77,11 @@ public Builder setRegisterAsBean(boolean registerAsBean) { return this; } + public Builder setDeclaringClass(ClassInfo declaringClass) { + this.declaringClass = declaringClass; + return this; + } + public ExceptionMapperBuildItem build() { return new ExceptionMapperBuildItem(this); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MessageBodyReaderTests.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MessageBodyReaderTests.java new file mode 100644 index 0000000000000..eaa5ec0f8bb30 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MessageBodyReaderTests.java @@ -0,0 +1,274 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.container.CompletionCallback; +import javax.ws.rs.container.ConnectionCallback; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.common.providers.serialisers.AbstractJsonMessageBodyReader; +import org.jboss.resteasy.reactive.server.jackson.JacksonBasicMessageBodyReader; +import org.jboss.resteasy.reactive.server.spi.ContentType; +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; +import org.jboss.resteasy.reactive.server.spi.ServerHttpResponse; +import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonIdentityReference; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.fasterxml.jackson.core.exc.StreamReadException; +import com.fasterxml.jackson.databind.DatabindException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; +import com.fasterxml.jackson.databind.exc.ValueInstantiationException; + +import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.ServerJacksonMessageBodyReader; + +@SuppressWarnings("unchecked") +class MessageBodyReaderTests { + + static class CommonReaderTests { + private final AbstractJsonMessageBodyReader reader; + + public CommonReaderTests(AbstractJsonMessageBodyReader reader) { + this.reader = reader; + } + + void deserializeMissingToken() throws IOException { + var stream = new ByteArrayInputStream("{\"model\": \"model\", \"cost\": 2".getBytes(StandardCharsets.UTF_8)); + Object widget = new Widget("", 1d); + reader.readFrom((Class) widget.getClass(), null, null, null, null, stream); + } + + void deserializeMissingRequiredProperty() throws IOException { + // missing non-nullable property + var stream = new ByteArrayInputStream("{\"cost\": 2}".getBytes(StandardCharsets.UTF_8)); + Object widget = new Widget("", 1d); + reader.readFrom((Class) widget.getClass(), null, null, null, null, stream); + } + + void deserializeMissingReferenceProperty() throws IOException { + var json = "{\n" + + " \"id\" : 1,\n" + + " \"name\" : \"Learn HTML\",\n" + + " \"owner\" : 1\n" + // unresolved reference to student + "}"; + + var stream = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); + Object book = new Book(1, null, null); + reader.readFrom((Class) book.getClass(), null, null, null, null, stream); + } + + void deserializeClassWithInvalidDefinition() throws IOException { + var json = "{\n" + + " \"arg\" : \"Learn HTML\"" + + "}"; + + var stream = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); + Object invalid = new InvalidDefinition(null); + reader.readFrom((Class) invalid.getClass(), null, null, null, null, stream); + } + } + + @Nested + @DisplayName("JacksonMessageBodyReader") + class JacksonMessageBodyReaderTests { + private final CommonReaderTests tests = new CommonReaderTests(new JacksonBasicMessageBodyReader(new ObjectMapper())); + + @Test + void shouldThrowStreamReadException() { + assertThrows(StreamReadException.class, tests::deserializeMissingToken); + } + + @Test + void shouldThrowValueInstantiationException() { + assertThrows(ValueInstantiationException.class, tests::deserializeMissingRequiredProperty); + } + + @Test + void shouldThrowDatabindException() { + assertThrows(DatabindException.class, tests::deserializeMissingReferenceProperty); + } + + @Test + void shouldThrowInvalidDefinitionException() { + assertThrows(InvalidDefinitionException.class, tests::deserializeClassWithInvalidDefinition); + } + } + + @Nested + @DisplayName("ServerJacksonMessageBodyReader") + class ServerJacksonMessageBodyReaderTests { + private final CommonReaderTests tests = new CommonReaderTests(new ServerJacksonMessageBodyReader(new ObjectMapper())); + + @Test + void shouldThrowWebExceptionWithStreamReadExceptionCause() { + var e = assertThrows(WebApplicationException.class, tests::deserializeMissingToken); + assertThat(StreamReadException.class).isAssignableFrom(e.getCause().getClass()); + } + + @Test + void shouldThrowWebExceptionWithValueInstantiationExceptionCause() { + var e = assertThrows(WebApplicationException.class, tests::deserializeMissingRequiredProperty); + assertThat(ValueInstantiationException.class).isAssignableFrom(e.getCause().getClass()); + } + + @Test + void shouldThrowWebExceptionWithDatabindExceptionCause() { + var e = assertThrows(WebApplicationException.class, tests::deserializeMissingReferenceProperty); + assertThat(DatabindException.class).isAssignableFrom(e.getCause().getClass()); + } + + @Test + void shouldThrowInvalidDefinitionException() { + assertThrows(InvalidDefinitionException.class, tests::deserializeClassWithInvalidDefinition); + } + + @Test + void shouldThrowWebExceptionWithValueInstantiationExceptionCauseUsingServerRequestContext() throws IOException { + var reader = new ServerJacksonMessageBodyReader(new ObjectMapper()); + // missing non-nullable property + var stream = new ByteArrayInputStream("{\"cost\": 2}".getBytes(StandardCharsets.UTF_8)); + var context = new MockServerRequestContext(stream); + Object widget = new Widget("", 1d); + + try { + reader.readFrom((Class) widget.getClass(), null, MediaType.APPLICATION_JSON_TYPE, context); + } catch (WebApplicationException e) { + assertThat(ValueInstantiationException.class).isAssignableFrom(e.getCause().getClass()); + } + } + } + + static class InvalidDefinition { + // Note: Multiple constructors marked as JsonCreators should throw InvalidDefinitionException + + private final Object arg; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public InvalidDefinition(Object arg) { + this.arg = arg; + } + } + + static class Widget { + + public final String model; + public final double cost; + + @JsonCreator + public Widget( + @JsonProperty("model") String model, + @JsonProperty("cost") double cost) { + this.model = Objects.requireNonNull(model, "'model' must be supplied"); + this.cost = cost; + } + } + + @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") + static class Student { + public int id; + public int rollNo; + public String name; + public List books; + + Student(int id, int rollNo, String name) { + this.id = id; + this.rollNo = rollNo; + this.name = name; + this.books = new ArrayList<>(); + } + } + + @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") + static class Book { + @JsonProperty("id") + public int id; + @JsonProperty("name") + public String name; + + Book() { + // do nothing ... for Jackson + } + + Book(int id, String name, Student owner) { + this.id = id; + this.name = name; + this.owner = owner; + } + + @JsonIdentityReference(alwaysAsId = true) + @JsonProperty("owner") + public Student owner; + } + + private static class MockServerRequestContext implements ServerRequestContext { + private final InputStream stream; + + public MockServerRequestContext(InputStream stream) { + this.stream = stream; + } + + @Override + public void registerCompletionCallback(CompletionCallback callback) { + + } + + @Override + public void registerConnectionCallback(ConnectionCallback callback) { + + } + + @Override + public ServerHttpResponse serverResponse() { + return null; + } + + @Override + public InputStream getInputStream() { + return stream; + } + + @Override + public ContentType getResponseContentType() { + return null; + } + + @Override + public MediaType getResponseMediaType() { + return null; + } + + @Override + public OutputStream getOrCreateOutputStream() { + return null; + } + + @Override + public ResteasyReactiveResourceInfo getResteasyReactiveResourceInfo() { + return null; + } + + @Override + public void abortWith(Response response) { + + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java index 579d8f1428c0b..d76704b01e1d5 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java @@ -14,6 +14,7 @@ import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; import javax.ws.rs.container.AsyncResponse; import javax.ws.rs.container.Suspended; import javax.ws.rs.core.MediaType; @@ -23,7 +24,6 @@ import org.jboss.resteasy.reactive.server.ServerExceptionMapper; import com.fasterxml.jackson.annotation.JsonView; -import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.json.JsonWriteFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; @@ -42,8 +42,9 @@ public class SimpleJsonResource extends SuperClass { @ServerExceptionMapper - public Response handleParseException(JsonParseException jpe) { - return Response.status(Response.Status.BAD_REQUEST).entity(jpe.getMessage()).build(); + public Response handleParseException(WebApplicationException e) { + var cause = e.getCause() == null ? e : e.getCause(); + return Response.status(Response.Status.BAD_REQUEST).entity(cause.getMessage()).build(); } @GET diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/ServerJacksonMessageBodyReader.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/ServerJacksonMessageBodyReader.java index 99be966aa88bd..755eb1997c913 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/ServerJacksonMessageBodyReader.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/ServerJacksonMessageBodyReader.java @@ -17,8 +17,11 @@ import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyReader; import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; +import com.fasterxml.jackson.core.exc.StreamReadException; +import com.fasterxml.jackson.databind.DatabindException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import com.fasterxml.jackson.databind.exc.MismatchedInputException; public class ServerJacksonMessageBodyReader extends JacksonBasicMessageBodyReader implements ServerMessageBodyReader { @@ -33,7 +36,27 @@ public Object readFrom(Class type, Type genericType, Annotation[] annota MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { try { return doReadFrom(type, genericType, entityStream); - } catch (MismatchedInputException e) { + } catch (MismatchedInputException | InvalidDefinitionException e) { + /* + * To extract additional details when running in dev mode or test mode, Quarkus previously offered the + * DefaultMismatchedInputException(Mapper). That mapper provides additional details about bad input, + * beyond Jackson's default, when running in Dev or Test mode. To preserve that behavior, we rethrow + * MismatchedInputExceptions we encounter. + * + * An InvalidDefinitionException is thrown when there is a problem with the way a type is + * set up/annotated for consumption by the Jackson API. We don't wrap it in a WebApplicationException + * (as a Server Error), since unhandled exceptions will end up as a 500 anyway. In addition, this + * allows built-in features like the NativeInvalidDefinitionExceptionMapper to be registered and + * communicate potential Jackson integration issues, and potential solutions for resolving them. + */ + throw e; + } catch (StreamReadException | DatabindException e) { + /* + * As JSON is evaluated, it can be invalid due to one of two reasons: + * 1) Malformed JSON. Un-parsable JSON results in a StreamReadException + * 2) Valid JSON that violates some binding constraint, i.e., a required property, mismatched data types, etc. + * Violations of these types are captured via a DatabindException. + */ throw new WebApplicationException(e, Response.Status.BAD_REQUEST); } } @@ -51,7 +74,7 @@ public boolean isReadable(Class type, Type genericType, ResteasyReactiveResou @Override public Object readFrom(Class type, Type genericType, MediaType mediaType, ServerRequestContext context) throws WebApplicationException, IOException { - return doReadFrom(type, genericType, context.getInputStream()); + return readFrom(type, genericType, null, mediaType, null, context.getInputStream()); } private Object doReadFrom(Class type, Type genericType, InputStream entityStream) throws IOException { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb-common/deployment/src/main/java/io/quarkus/resteasy/reactive/jsonb/common/deployment/ResteasyReactiveJsonbCommonProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb-common/deployment/src/main/java/io/quarkus/resteasy/reactive/jsonb/common/deployment/ResteasyReactiveJsonbCommonProcessor.java index 1ca16e922bd1f..1c5c4a70741da 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb-common/deployment/src/main/java/io/quarkus/resteasy/reactive/jsonb/common/deployment/ResteasyReactiveJsonbCommonProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb-common/deployment/src/main/java/io/quarkus/resteasy/reactive/jsonb/common/deployment/ResteasyReactiveJsonbCommonProcessor.java @@ -3,6 +3,7 @@ import java.util.Collections; import java.util.List; +import javax.ws.rs.RuntimeType; import javax.ws.rs.core.MediaType; import org.jboss.resteasy.reactive.common.util.RestMediaType; @@ -29,33 +30,37 @@ public class ResteasyReactiveJsonbCommonProcessor { VertxJson.JsonArrayDeserializer.class.getName()); @BuildStep - void additionalProviders(BuildProducer additionalBean, - BuildProducer additionalReaders, - BuildProducer additionalWriters) { + public void registerVertxJsonSupport( + BuildProducer serializers, + BuildProducer deserializers) { + serializers.produce(new JsonbSerializerBuildItem(VERTX_SERIALIZERS)); + deserializers.produce(new JsonbDeserializerBuildItem(VERTX_DESERIALIZERS)); + } + + @BuildStep + public void beans(BuildProducer additionalBean) { // make these beans to they can get instantiated with the Quarkus CDI configured Jsonb object additionalBean.produce(AdditionalBeanBuildItem.builder() .addBeanClass(JsonbMessageBodyReader.class.getName()) .addBeanClass(JsonbMessageBodyWriter.class.getName()) .setUnremovable().build()); + } + + public static void additionalProviders(BuildProducer additionalReaders, + BuildProducer additionalWriters, RuntimeType runtimeType) { additionalReaders.produce( new MessageBodyReaderBuildItem.Builder(JsonbMessageBodyReader.class.getName(), Object.class.getName()) .setMediaTypeStrings(Collections.singletonList(MediaType.APPLICATION_JSON)) .setBuiltin(true) + .setRuntimeType(runtimeType) .build()); additionalWriters.produce( new MessageBodyWriterBuildItem.Builder(JsonbMessageBodyWriter.class.getName(), Object.class.getName()) .setMediaTypeStrings(List.of(MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_NDJSON, RestMediaType.APPLICATION_STREAM_JSON)) .setBuiltin(true) + .setRuntimeType(runtimeType) .build()); } - - @BuildStep - public void registerVertxJsonSupport( - BuildProducer serializers, - BuildProducer deserializers) { - serializers.produce(new JsonbSerializerBuildItem(VERTX_SERIALIZERS)); - deserializers.produce(new JsonbDeserializerBuildItem(VERTX_DESERIALIZERS)); - } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/main/java/io/quarkus/resteasy/reactive/jsonb/deployment/ResteasyReactiveJsonbProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/main/java/io/quarkus/resteasy/reactive/jsonb/deployment/ResteasyReactiveJsonbProcessor.java index a674d8d89bdfa..64046a86364c5 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/main/java/io/quarkus/resteasy/reactive/jsonb/deployment/ResteasyReactiveJsonbProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/main/java/io/quarkus/resteasy/reactive/jsonb/deployment/ResteasyReactiveJsonbProcessor.java @@ -1,10 +1,15 @@ package io.quarkus.resteasy.reactive.jsonb.deployment; +import javax.ws.rs.RuntimeType; + import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.resteasy.reactive.common.deployment.ServerDefaultProducesHandlerBuildItem; +import io.quarkus.resteasy.reactive.jsonb.common.deployment.ResteasyReactiveJsonbCommonProcessor; +import io.quarkus.resteasy.reactive.spi.MessageBodyReaderBuildItem; +import io.quarkus.resteasy.reactive.spi.MessageBodyWriterBuildItem; public class ResteasyReactiveJsonbProcessor { @@ -17,4 +22,11 @@ void feature(BuildProducer feature) { ServerDefaultProducesHandlerBuildItem jsonDefault() { return ServerDefaultProducesHandlerBuildItem.json(); } + + @BuildStep + void additionalProviders(BuildProducer additionalReaders, + BuildProducer additionalWriters) { + ResteasyReactiveJsonbCommonProcessor.additionalProviders(additionalReaders, additionalWriters, + RuntimeType.SERVER); + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseTestCase.java index 8930a47aa6622..8d36e04781214 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseTestCase.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseTestCase.java @@ -14,6 +14,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import javax.json.bind.JsonbBuilder; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.WebTarget; @@ -24,6 +25,7 @@ import org.apache.http.HttpStatus; import org.jboss.resteasy.reactive.client.impl.MultiInvoker; import org.jboss.resteasy.reactive.common.util.RestMediaType; +import org.jboss.resteasy.reactive.server.jsonb.JsonbMessageBodyReader; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -135,7 +137,9 @@ public void testStreamJsonMultiFromMulti() { } private void testJsonMulti(String path) { - Client client = ClientBuilder.newBuilder().build(); + Client client = ClientBuilder.newBuilder() + .register(new JsonbMessageBodyReader(JsonbBuilder.create())) // we need this because registering Jsonb for the server part does not affect the client + .build(); WebTarget target = client.target(uri.toString() + path); Multi multi = target.request().rx(MultiInvoker.class).get(Message.class); List list = multi.collect().asList().await().atMost(Duration.ofSeconds(30)); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomResourceProducersGenerator.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomResourceProducersGenerator.java index a0b95bbb21214..600dc62fc03da 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomResourceProducersGenerator.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomResourceProducersGenerator.java @@ -136,8 +136,9 @@ public static void generate(Map resourcesThatNeedCustomProd ResultHandle quarkusRestContextHandle = m.invokeVirtualMethod(getContextMethodCreator.getMethodDescriptor(), m.getThis()); ResultHandle extractorHandle = m.newInstance( - MethodDescriptor.ofConstructor(QueryParamExtractor.class, String.class, boolean.class, boolean.class), - m.getMethodParam(0), m.load(true), m.load(false)); + MethodDescriptor.ofConstructor(QueryParamExtractor.class, String.class, boolean.class, boolean.class, + String.class), + m.getMethodParam(0), m.load(true), m.load(false), m.loadNull()); ResultHandle resultHandle = m.invokeVirtualMethod(MethodDescriptor.ofMethod(QueryParamExtractor.class, "extractParameter", Object.class, ResteasyReactiveRequestContext.class), extractorHandle, quarkusRestContextHandle); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/FilterClassIntrospector.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/FilterClassIntrospector.java index de6dc434a6e4c..d82e0dfd0ada1 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/FilterClassIntrospector.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/FilterClassIntrospector.java @@ -4,6 +4,8 @@ import java.io.InputStream; import java.io.UncheckedIOException; +import javax.ws.rs.container.ResourceInfo; + import org.jboss.jandex.MethodInfo; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -67,7 +69,7 @@ private UsesGetResourceMethodVisitor() { @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { - if ((opcode == Opcodes.INVOKEINTERFACE) && "javax/ws/rs/container/ResourceInfo".equals(owner) + if ((opcode == Opcodes.INVOKEINTERFACE) && ResourceInfo.class.getName().replace('.', '/').equals(owner) && "getResourceMethod".equals(name)) { usesGetResourceMethod = true; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MessageBodyWriterTransformerUtils.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MessageBodyWriterTransformerUtils.java index 339763d0d06e7..3395f4433b067 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MessageBodyWriterTransformerUtils.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MessageBodyWriterTransformerUtils.java @@ -5,6 +5,8 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; +import javax.ws.rs.core.MediaType; + import org.jboss.logging.Logger; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -57,15 +59,15 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str MethodVisitor superMethodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); if (name.equals("isWriteable")) { // RR isWriteable - if ("(Ljava/lang/Class;Ljava/lang/reflect/Type;Lorg/jboss/resteasy/reactive/server/spi/ResteasyReactiveResourceInfo;Ljavax/ws/rs/core/MediaType;)Z" - .equals(descriptor)) { + if (("(Ljava/lang/Class;Ljava/lang/reflect/Type;Lorg/jboss/resteasy/reactive/server/spi/ResteasyReactiveResourceInfo;L" + + MediaType.class.getName().replace('.', '/') + ";)Z").equals(descriptor)) { AtomicBoolean rrResult = new AtomicBoolean(false); rrIsWritableResult = Optional.of(rrResult); return new MessageBodyWriterIsWriteableMethodVisitor(new CodeSizeEvaluator(superMethodVisitor), rrResult); } // JAX-RS isWriteable - else if ("(Ljava/lang/Class;Ljava/lang/reflect/Type;[Ljava/lang/annotation/Annotation;Ljavax/ws/rs/core/MediaType;)Z" - .equals(descriptor)) { + else if (("(Ljava/lang/Class;Ljava/lang/reflect/Type;[Ljava/lang/annotation/Annotation;L" + + MediaType.class.getName().replace('.', '/') + ";)Z").equals(descriptor)) { AtomicBoolean standardResult = new AtomicBoolean(false); jaxRSIsWritableResult = Optional.of(standardResult); return new MessageBodyWriterIsWriteableMethodVisitor(new CodeSizeEvaluator(superMethodVisitor), 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 fdfab1e721441..ca8dd87631198 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 @@ -159,6 +159,7 @@ import io.quarkus.resteasy.reactive.common.deployment.SerializersUtil; import io.quarkus.resteasy.reactive.common.deployment.ServerDefaultProducesHandlerBuildItem; import io.quarkus.resteasy.reactive.common.runtime.ResteasyReactiveConfig; +import io.quarkus.resteasy.reactive.server.EndpointDisabled; import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveInitialiser; import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveRecorder; import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveRuntimeRecorder; @@ -188,6 +189,7 @@ import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.RuntimeValue; import io.quarkus.security.AuthenticationCompletionException; +import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.AuthenticationRedirectException; import io.quarkus.security.ForbiddenException; import io.quarkus.vertx.http.deployment.FilterBuildItem; @@ -214,6 +216,7 @@ public class ResteasyReactiveProcessor { DotName.createSimple(HttpServerResponse.class.getName()), DotName.createSimple(RoutingContext.class.getName())); private static final DotName FILE = DotName.createSimple(File.class.getName()); + private static final DotName ENDPOINT_DISABLED = DotName.createSimple(EndpointDisabled.class.getName()); private static final int SECURITY_EXCEPTION_MAPPERS_PRIORITY = Priorities.USER + 1; private static final String[] EMPTY_STRING_ARRAY = new String[0]; @@ -588,6 +591,20 @@ private boolean hasAnnotation(MethodInfo method, short paramPosition, DotName an public Status isJava19OrHigher() { return result; } + }) + .setIsDisabledCreator(new Function<>() { + @Override + public Supplier apply(ClassInfo classInfo) { + AnnotationInstance instance = classInfo.declaredAnnotation(ENDPOINT_DISABLED); + if (instance == null) { + return null; + } + String propertyName = instance.value("name").asString(); + String propertyValue = instance.value("stringValue").asString(); + AnnotationValue disableIfMissingValue = instance.value("disableIfMissing"); + boolean disableIfMissing = disableIfMissingValue != null && disableIfMissingValue.asBoolean(); + return recorder.disableIfPropertyMatches(propertyName, propertyValue, disableIfMissing); + } }); if (!serverDefaultProducesHandlers.isEmpty()) { @@ -1106,8 +1123,9 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem, Function> factoryFunction = s -> FactoryUtils.factory(s, singletonClasses, recorder, beanContainerBuildItem); interceptors.initializeDefaultFactories(factoryFunction); - exceptionMapping.initializeDefaultFactories(factoryFunction); contextResolvers.initializeDefaultFactories(factoryFunction); + exceptionMapping.initializeDefaultFactories(factoryFunction); + exceptionMapping.replaceDiscardAtRuntimeIfBeanIsUnavailable(className -> recorder.beanUnavailable(className)); paramConverterProviders.initializeDefaultFactories(factoryFunction); paramConverterProviders.sort(); @@ -1202,8 +1220,30 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem, if (!requestContextFactoryBuildItem.isPresent()) { RuntimeValue restInitialHandler = recorder.restInitialHandler(deployment); Handler handler = recorder.handler(restInitialHandler); - Handler failureHandler = recorder.failureHandler(restInitialHandler); - filterBuildItemBuildProducer.produce(new FilterBuildItem(failureHandler, order, true)); + + final boolean noCustomAuthCompletionExMapper; + final boolean noCustomAuthFailureExMapper; + final boolean noCustomAuthRedirectExMapper; + if (vertxConfig.auth.proactive) { + noCustomAuthCompletionExMapper = notFoundCustomExMapper(AuthenticationCompletionException.class.getName(), + AuthenticationCompletionExceptionMapper.class.getName(), exceptionMapping); + noCustomAuthFailureExMapper = notFoundCustomExMapper(AuthenticationFailedException.class.getName(), + AuthenticationFailedExceptionMapper.class.getName(), exceptionMapping); + noCustomAuthRedirectExMapper = notFoundCustomExMapper(AuthenticationRedirectException.class.getName(), + AuthenticationRedirectExceptionMapper.class.getName(), exceptionMapping); + } else { + // with disabled proactive auth we need to handle exceptions anyway as default auth failure handler did not + noCustomAuthCompletionExMapper = false; + noCustomAuthFailureExMapper = false; + noCustomAuthRedirectExMapper = false; + } + + Handler failureHandler = recorder.failureHandler(restInitialHandler, noCustomAuthCompletionExMapper, + noCustomAuthFailureExMapper, noCustomAuthRedirectExMapper, vertxConfig.auth.proactive); + + // we add failure handler right before QuarkusErrorHandler + // so that user can define failure handlers that precede exception mappers + filterBuildItemBuildProducer.produce(FilterBuildItem.ofAuthenticationFailureHandler(failureHandler)); // Exact match for resources matched to the root path routes.produce(RouteBuildItem.builder() @@ -1221,6 +1261,26 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem, } } + private static boolean notFoundCustomExMapper(String builtInExSignature, String builtInMapperSignature, + ExceptionMapping exceptionMapping) { + for (var entry : exceptionMapping.getMappers().entrySet()) { + if (builtInExSignature.equals(entry.getKey()) + && !entry.getValue().getClassName().startsWith(builtInMapperSignature)) { + return false; + } + } + for (var entry : exceptionMapping.getRuntimeCheckMappers().entrySet()) { + if (builtInExSignature.equals(entry.getKey())) { + for (var resourceExceptionMapper : entry.getValue()) { + if (!resourceExceptionMapper.getClassName().startsWith(builtInMapperSignature)) { + return false; + } + } + } + } + return true; + } + @BuildStep @Record(value = ExecutionTime.STATIC_INIT) public FilterBuildItem addDefaultAuthFailureHandler(ResteasyReactiveRecorder recorder) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java index aacd2043ff891..42d5b78e2d8b9 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java @@ -123,7 +123,6 @@ public List defaultUnwrappedException() { new UnwrappedExceptionBuildItem(RollbackException.class)); } - @SuppressWarnings({ "unchecked", "rawtypes" }) @BuildStep public ExceptionMappersBuildItem scanForExceptionMappers(CombinedIndexBuildItem combinedIndexBuildItem, ApplicationResultBuildItem applicationResultBuildItem, @@ -163,12 +162,32 @@ public ExceptionMappersBuildItem scanForExceptionMappers(CombinedIndexBuildItem ResourceExceptionMapper mapper = new ResourceExceptionMapper<>(); mapper.setPriority(priority); mapper.setClassName(additionalExceptionMapper.getClassName()); + addRuntimeCheckIfNecessary(additionalExceptionMapper, mapper); exceptions.addExceptionMapper(additionalExceptionMapper.getHandledExceptionName(), mapper); } additionalBeanBuildItemBuildProducer.produce(beanBuilder.build()); return new ExceptionMappersBuildItem(exceptions); } + private static void addRuntimeCheckIfNecessary(ExceptionMapperBuildItem additionalExceptionMapper, + ResourceExceptionMapper mapper) { + ClassInfo declaringClass = additionalExceptionMapper.getDeclaringClass(); + if (declaringClass != null) { + boolean needsRuntimeCheck = false; + List classAnnotations = declaringClass.declaredAnnotations(); + for (AnnotationInstance classAnnotation : classAnnotations) { + if (CONDITIONAL_BEAN_ANNOTATIONS.contains(classAnnotation.name())) { + needsRuntimeCheck = true; + break; + } + } + if (needsRuntimeCheck) { + mapper.setDiscardAtRuntime(new ResourceExceptionMapper.DiscardAtRuntimeIfBeanIsUnavailable( + declaringClass.name().toString())); + } + } + } + @BuildStep public ParamConverterProvidersBuildItem scanForParamConverters( BuildProducer additionalBeanBuildItemBuildProducer, @@ -351,7 +370,7 @@ public void handleCustomAnnotatedMethods( .setPriority(generated.getPriority()) .setPreMatching(generated.isPreMatching()) .setNonBlockingRequired(generated.isNonBlocking()) - .setReadBody(generated.isReadBody()) + .setWithFormRead(generated.isWithFormRead()) .setFilterSourceMethod(generated.getFilterSourceMethod()); if (!generated.getNameBindingNames().isEmpty()) { builder.setNameBindingNames(generated.getNameBindingNames()); @@ -387,10 +406,31 @@ public void handleCustomAnnotatedMethods( additionalBeans.addBeanClass(methodInfo.declaringClass().name().toString()); Map generatedClassNames = ServerExceptionMapperGenerator.generateGlobalMapper(methodInfo, new GeneratedBeanGizmoAdaptor(generatedBean), - Set.of(HTTP_SERVER_REQUEST, HTTP_SERVER_RESPONSE, ROUTING_CONTEXT), Set.of(Unremovable.class.getName())); + Set.of(HTTP_SERVER_REQUEST, HTTP_SERVER_RESPONSE, ROUTING_CONTEXT), Set.of(Unremovable.class.getName()), + (m -> { + List methodAnnotations = m.annotations(); + for (AnnotationInstance methodAnnotation : methodAnnotations) { + if (CONDITIONAL_BEAN_ANNOTATIONS.contains(methodAnnotation.name())) { + throw new RuntimeException( + "The combination of '@" + methodAnnotation.name().withoutPackagePrefix() + + "' and '@ServerExceptionMapper' is not allowed. Offending method is '" + + m.name() + "' of class '" + m.declaringClass().name() + "'"); + } + } + + List classAnnotations = m.declaringClass().declaredAnnotations(); + for (AnnotationInstance classAnnotation : classAnnotations) { + if (CONDITIONAL_BEAN_ANNOTATIONS.contains(classAnnotation.name())) { + return true; + } + } + return false; + })); for (Map.Entry entry : generatedClassNames.entrySet()) { ExceptionMapperBuildItem.Builder builder = new ExceptionMapperBuildItem.Builder(entry.getValue(), - entry.getKey()).setRegisterAsBean(false);// it has already been made a bean + entry.getKey()) + .setRegisterAsBean(false) // it has already been made a bean + .setDeclaringClass(methodInfo.declaringClass()); // we'll use this later on AnnotationValue priorityValue = instance.value("priority"); if (priorityValue != null) { builder.setPriority(priorityValue.asInt()); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/EndpointDisabledTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/EndpointDisabledTest.java new file mode 100644 index 0000000000000..02f89968e9887 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/EndpointDisabledTest.java @@ -0,0 +1,113 @@ +package io.quarkus.resteasy.reactive.server.test; + +import static io.restassured.RestAssured.*; +import static org.hamcrest.CoreMatchers.equalTo; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.resteasy.reactive.server.EndpointDisabled; +import io.quarkus.resteasy.reactive.server.test.multipart.InvalidEncodingTest; +import io.quarkus.test.QuarkusUnitTest; + +public class EndpointDisabledTest { + + @RegisterExtension + static QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(InvalidEncodingTest.FeedbackBody.class, InvalidEncodingTest.FeedbackResource.class) + .addAsResource(new StringAsset("dummy.disabled=true"), + "application.properties")); + + @Test + public void endpointWithNoAnnotation() { + get("/no-annotation") + .then() + .statusCode(200) + .body(equalTo("no")); + } + + @Test + public void shouldBeDisabledBecauseOfMatchingProperty() { + get("/dummy-disabled-true") + .then() + .statusCode(404); + } + + @Test + public void shouldBeEnabledBecauseOfNonMatchingProperty() { + get("/dummy-disabled-false") + .then() + .statusCode(200) + .body(equalTo("dummy.disabled=false")); + } + + @Test + public void shouldBeDisabledBecauseOfNonExistingProperty() { + get("/other-dummy-disabled-missing-true") + .then() + .statusCode(404); + } + + @Test + public void shouldBeEnabledBecauseOfNonExistingProperty() { + get("/other-dummy-disabled-missing-false") + .then() + .statusCode(200) + .body(equalTo("missing=false")); + } + + @Path("no-annotation") + public static class NoAnnotation { + + @GET + public String get() { + return "no"; + } + + } + + @Path("dummy-disabled-true") + @EndpointDisabled(name = "dummy.disabled", stringValue = "true", disableIfMissing = false) + public static class DummyDisabledTrue { + + @GET + public String get() { + return "dummy.disabled=true"; + } + } + + @Path("dummy-disabled-false") + @EndpointDisabled(name = "dummy.disabled", stringValue = "false", disableIfMissing = false) + public static class DummyDisabledFalse { + + @GET + public String get() { + return "dummy.disabled=false"; + } + } + + @Path("other-dummy-disabled-missing-true") + @EndpointDisabled(name = "other.dummy.disabled", stringValue = "true", disableIfMissing = true) + public static class OtherDummyDisabledMissingTrue { + + @GET + public String get() { + return "missing=true"; + } + } + + @Path("other-dummy-disabled-missing-false") + @EndpointDisabled(name = "other.dummy.disabled", stringValue = "true", disableIfMissing = false) + public static class OtherDummyDisabledMissingFalse { + + @GET + public String get() { + return "missing=false"; + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/ConditionalExceptionMappersTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/ConditionalExceptionMappersTest.java new file mode 100644 index 0000000000000..31c3270b60806 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/ConditionalExceptionMappersTest.java @@ -0,0 +1,141 @@ +package io.quarkus.resteasy.reactive.server.test.customexceptions; + +import static io.restassured.RestAssured.*; + +import java.util.function.Supplier; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Priorities; +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +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.arc.lookup.LookupUnlessProperty; +import io.quarkus.arc.profile.IfBuildProfile; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.mutiny.Uni; + +public class ConditionalExceptionMappersTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(AbstractException.class, FirstException.class, SecondException.class, + WontBeEnabledMappers.class, WillBeEnabledMappers.class, AlwaysEnabledMappers.class, + TestResource.class); + } + }); + + @Test + public void test() { + get("/first").then().statusCode(903); + get("/second").then().statusCode(801); + get("/third").then().statusCode(555); + } + + @Path("") + public static class TestResource { + + @Path("first") + @GET + public String first() { + throw new FirstException(); + } + + @Path("second") + @GET + public String second() { + throw new SecondException(); + } + + @Path("third") + @GET + public String third() { + throw new ThirdException(); + } + } + + public static abstract class AbstractException extends RuntimeException { + + public AbstractException() { + setStackTrace(new StackTraceElement[0]); + } + } + + public static class FirstException extends AbstractException { + + } + + public static class SecondException extends AbstractException { + + } + + public static class ThirdException extends AbstractException { + + } + + @IfBuildProfile("dummy") + public static class WontBeEnabledMappers { + + @ServerExceptionMapper(FirstException.class) + public Response first() { + return Response.status(900).build(); + } + + @ServerExceptionMapper(value = FirstException.class, priority = Priorities.USER - 100) + public Response firstWithLowerPriority() { + return Response.status(901).build(); + } + + @ServerExceptionMapper(priority = Priorities.USER - 100) + public Response second(SecondException ignored) { + return Response.status(800).build(); + } + } + + @LookupUnlessProperty(name = "notexistingproperty", stringValue = "true", lookupIfMissing = true) + public static class WillBeEnabledMappers { + + @ServerExceptionMapper(value = FirstException.class, priority = Priorities.USER + 10) + public Response first() { + return Response.status(902).build(); + } + + @ServerExceptionMapper(value = FirstException.class, priority = Priorities.USER - 10) + public Response firstWithLowerPriority() { + return Response.status(903).build(); + } + + @ServerExceptionMapper(priority = Priorities.USER - 10) + public RestResponse second(SecondException ignored) { + return RestResponse.status(801); + } + } + + public static class AlwaysEnabledMappers { + + @ServerExceptionMapper(value = FirstException.class, priority = Priorities.USER + 1000) + public Response first() { + return Response.status(555).build(); + } + + @ServerExceptionMapper(value = SecondException.class, priority = Priorities.USER + 1000) + public Response second() { + return Response.status(555).build(); + } + + @ServerExceptionMapper(value = ThirdException.class, priority = Priorities.USER + 1000) + public Uni third() { + return Uni.createFrom().item(Response.status(555).build()); + } + } +} diff --git "a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/InvalidConditional\316\234appersTest.java" "b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/InvalidConditional\316\234appersTest.java" new file mode 100644 index 0000000000000..08cb2f23ae8fc --- /dev/null +++ "b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/InvalidConditional\316\234appersTest.java" @@ -0,0 +1,62 @@ +package io.quarkus.resteasy.reactive.server.test.customexceptions; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.function.Supplier; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +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.arc.profile.IfBuildProfile; +import io.quarkus.test.QuarkusUnitTest; + +public class InvalidConditionalΜappersTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestResource.class, Mappers.class); + } + }).assertException(t -> { + String message = t.getMessage(); + assertTrue(message.contains("@ServerExceptionMapper")); + assertTrue(message.contains("request")); + assertTrue(message.contains(Mappers.class.getName())); + }); + + @Test + public void test() { + fail("Should never have been called"); + } + + @Path("test") + public static class TestResource { + + @GET + public String hello() { + return "hello"; + } + + } + + public static class Mappers { + + @IfBuildProfile("test") + @ServerExceptionMapper + public Response request(IllegalArgumentException ignored) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/ImpliedReadBodyRequestFilterTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/ImpliedReadBodyRequestFilterTest.java new file mode 100644 index 0000000000000..605cb4677e18b --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/ImpliedReadBodyRequestFilterTest.java @@ -0,0 +1,119 @@ +package io.quarkus.resteasy.reactive.server.test.customproviders; + +import java.util.function.Supplier; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.HttpHeaders; + +import org.hamcrest.Matchers; +import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.RestQuery; +import org.jboss.resteasy.reactive.server.ServerRequestFilter; +import org.jboss.resteasy.reactive.server.WithFormRead; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext; +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.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ImpliedReadBodyRequestFilterTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(HelloResource.class); + } + }); + + @Test + public void testMethodWithBody() { + RestAssured.with() + .formParam("name", "Quarkus") + .post("/hello") + .then().body(Matchers.equalTo("hello Quarkus!!!!!!!")); + } + + @Test + public void testMethodWithUndeclaredBody() { + RestAssured.with() + .formParam("name", "Quarkus") + .post("/hello/empty") + .then().body(Matchers.equalTo("hello !!!!!!!")); + } + + @Test + public void testMethodWithStringBody() { + // make sure that a form-reading filter doesn't prevent non-form request bodies from being deserialised + RestAssured.with() + .formParam("name", "Quarkus") + .post("/hello/string") + .then().body(Matchers.equalTo("hello name=Quarkus!!!!!!!")); + RestAssured.with() + .body("Quarkus") + .post("/hello/string") + .then().body(Matchers.equalTo("hello Quarkus?")); + } + + @Test + public void testMethodWithoutBody() { + RestAssured.with() + .queryParam("name", "Quarkus") + .get("/hello") + .then().body(Matchers.equalTo("hello Quarkus!")); + } + + @Path("hello") + public static class HelloResource { + + @POST + public String helloPost(@RestForm String name, HttpHeaders headers) { + return "hello " + name + headers.getHeaderString("suffix"); + } + + @Path("empty") + @POST + public String helloEmptyPost(HttpHeaders headers) { + return "hello " + headers.getHeaderString("suffix"); + } + + @Path("string") + @POST + public String helloStringPost(String body, HttpHeaders headers) { + return "hello " + body + headers.getHeaderString("suffix"); + } + + @GET + public String helloGet(@RestQuery String name, HttpHeaders headers) { + return "hello " + name + headers.getHeaderString("suffix"); + } + } + + public static class Filters { + + @WithFormRead + @ServerRequestFilter + public void addSuffix(ResteasyReactiveContainerRequestContext containerRequestContext) { + ResteasyReactiveRequestContext rrContext = (ResteasyReactiveRequestContext) containerRequestContext + .getServerRequestContext(); + if (containerRequestContext.getMethod().equals("POST")) { + String nameFormParam = (String) rrContext.getFormParameter("name", true, false); + if (nameFormParam != null) { + containerRequestContext.getHeaders().putSingle("suffix", "!".repeat(nameFormParam.length())); + } else { + containerRequestContext.getHeaders().putSingle("suffix", "?"); + } + } else { + containerRequestContext.getHeaders().putSingle("suffix", "!"); + } + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/ReadBodyRequestFilterTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/ReadBodyRequestFilterTest.java index 86aca31051c54..952260f155988 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/ReadBodyRequestFilterTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/ReadBodyRequestFilterTest.java @@ -11,6 +11,7 @@ import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.RestQuery; import org.jboss.resteasy.reactive.server.ServerRequestFilter; +import org.jboss.resteasy.reactive.server.WithFormRead; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext; import org.jboss.shrinkwrap.api.ShrinkWrap; @@ -41,6 +42,27 @@ public void testMethodWithBody() { .then().body(Matchers.equalTo("hello Quarkus!!!!!!!")); } + @Test + public void testMethodWithUndeclaredBody() { + RestAssured.with() + .formParam("name", "Quarkus") + .post("/hello/empty") + .then().body(Matchers.equalTo("hello !!!!!!!")); + } + + @Test + public void testMethodWithStringBody() { + // make sure that a form-reading filter doesn't prevent non-form request bodies from being deserialised + RestAssured.with() + .formParam("name", "Quarkus") + .post("/hello/string") + .then().body(Matchers.equalTo("hello name=Quarkus!!!!!!!")); + RestAssured.with() + .body("Quarkus") + .post("/hello/string") + .then().body(Matchers.equalTo("hello Quarkus?")); + } + @Test public void testMethodWithoutBody() { RestAssured.with() @@ -57,6 +79,18 @@ public String helloPost(@RestForm String name, HttpHeaders headers) { return "hello " + name + headers.getHeaderString("suffix"); } + @Path("empty") + @POST + public String helloEmptyPost(HttpHeaders headers) { + return "hello " + headers.getHeaderString("suffix"); + } + + @Path("string") + @POST + public String helloStringPost(String body, HttpHeaders headers) { + return "hello " + body + headers.getHeaderString("suffix"); + } + @GET public String helloGet(@RestQuery String name, HttpHeaders headers) { return "hello " + name + headers.getHeaderString("suffix"); @@ -65,13 +99,18 @@ public String helloGet(@RestQuery String name, HttpHeaders headers) { public static class Filters { + @WithFormRead @ServerRequestFilter(readBody = true) public void addSuffix(ResteasyReactiveContainerRequestContext containerRequestContext) { ResteasyReactiveRequestContext rrContext = (ResteasyReactiveRequestContext) containerRequestContext .getServerRequestContext(); if (containerRequestContext.getMethod().equals("POST")) { String nameFormParam = (String) rrContext.getFormParameter("name", true, false); - containerRequestContext.getHeaders().putSingle("suffix", "!".repeat(nameFormParam.length())); + if (nameFormParam != null) { + containerRequestContext.getHeaders().putSingle("suffix", "!".repeat(nameFormParam.length())); + } else { + containerRequestContext.getHeaders().putSingle("suffix", "?"); + } } else { containerRequestContext.getHeaders().putSingle("suffix", "!"); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/WithFormBodyTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/WithFormBodyTest.java new file mode 100644 index 0000000000000..bc89943ad593a --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/WithFormBodyTest.java @@ -0,0 +1,64 @@ +package io.quarkus.resteasy.reactive.server.test.customproviders; + +import java.util.function.Supplier; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; + +import org.hamcrest.Matchers; +import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.server.WithFormRead; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; +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.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class WithFormBodyTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(HelloResource.class); + } + }); + + @Test + public void testMethodWithBody() { + RestAssured.with() + .formParam("name", "Quarkus") + .post("/hello") + .then().body(Matchers.equalTo("hello Quarkus")); + } + + @Test + public void testMethodWithUndeclaredBody() { + RestAssured.with() + .formParam("name", "Quarkus") + .post("/hello/empty") + .then().body(Matchers.equalTo("hello Quarkus")); + } + + @Path("hello") + public static class HelloResource { + + @POST + public String helloPost(@RestForm String name) { + return "hello " + name; + } + + @WithFormRead + @Path("empty") + @POST + public String helloEmptyPost(ServerRequestContext requestContext) { + return "hello " + ((ResteasyReactiveRequestContext) requestContext).getFormParameter("name", true, false); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartFormInputDevModeTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartFormInputDevModeTest.java index 83a02058088e2..6b33f16667d46 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartFormInputDevModeTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartFormInputDevModeTest.java @@ -74,7 +74,7 @@ private void doTest(String path) { .post("/multipart/" + path + "/2") .then() .statusCode(200) - .body(equalTo("Alice - true - 50 - WORKING - text/html - true - true")); + .body(equalTo("Alice - true - 50 - WORKING - true - true - true")); } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputBodyHandlerTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputBodyHandlerTest.java index a3e7cf8ed1ef4..f75e0a416573d 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputBodyHandlerTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputBodyHandlerTest.java @@ -92,7 +92,7 @@ public void testSimple() { .post("/multipart/simple/2") .then() .statusCode(200) - .body(equalTo("Alice - true - 50 - WORKING - text/html - true - true")); + .body(equalTo("Alice - true - 50 - WORKING - true - true - true")); // ensure that the 3 uploaded files where created on disk Assertions.assertEquals(3, uploadDir.toFile().listFiles().length); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java index a0d69d00d2c64..0316e038c7800 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java @@ -50,6 +50,9 @@ public JavaArchive get() { private final File HTML_FILE2 = new File("./src/test/resources/test2.html"); private final File XML_FILE = new File("./src/test/resources/test.html"); private final File TXT_FILE = new File("./src/test/resources/lorem.txt"); + private final String TXT = "lorem ipsum"; + private final String XML = ""; + private final String HTML = ""; @BeforeEach public void assertEmptyUploads() { @@ -68,15 +71,15 @@ public void testSimple() { .multiPart("active", "true") .multiPart("num", "25") .multiPart("status", "WORKING") - .multiPart("htmlFile", HTML_FILE, "text/html") - .multiPart("xmlFile", XML_FILE, "text/xml") - .multiPart("txtFile", TXT_FILE, "text/plain") + .multiPart("htmlFile", HTML, "text/html") + .multiPart("xmlFile", XML, "text/xml") + .multiPart("txtFile", TXT, "text/plain") .accept("text/plain") .when() .post("/multipart/simple/2") .then() .statusCode(200) - .body(equalTo("Alice - true - 50 - WORKING - text/html - true - true")); + .body(equalTo("Alice - true - 50 - WORKING - true - true - true")); // ensure that the 3 uploaded files where created on disk Assertions.assertEquals(3, uploadDir.toFile().listFiles().length); @@ -110,15 +113,15 @@ public void testSimpleParam() { .multiPart("active", "true") .multiPart("num", "25") .multiPart("status", "WORKING") - .multiPart("htmlFile", HTML_FILE, "text/html") - .multiPart("xmlFile", XML_FILE, "text/xml") - .multiPart("txtFile", TXT_FILE, "text/plain") + .multiPart("htmlFile", HTML, "text/html") + .multiPart("xmlFile", XML, "text/xml") + .multiPart("txtFile", TXT, "text/plain") .accept("text/plain") .when() .post("/multipart/param/simple/2") .then() .statusCode(200) - .body(equalTo("Alice - true - 50 - WORKING - text/html - true - true")); + .body(equalTo("Alice - true - 50 - WORKING - true - true - true")); // ensure that the 3 uploaded files where created on disk Assertions.assertEquals(3, uploadDir.toFile().listFiles().length); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java index 3e8bb225da17f..62881e6efed67 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java @@ -15,6 +15,7 @@ import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; public class MultipartOutputUsingBlockingEndpointsTest extends AbstractMultipartTest { @@ -65,18 +66,20 @@ public void testRestResponse() { @Test public void testWithFormData() { - String response = RestAssured.get("/multipart/output/with-form-data") + ExtractableResponse extractable = RestAssured.get("/multipart/output/with-form-data") .then() - .log().all() .contentType(ContentType.MULTIPART) .statusCode(200) - .extract().asString(); + .extract(); - assertContainsValue(response, "name", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_NAME); - assertContainsValue(response, "custom-surname", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_SURNAME); - assertContainsValue(response, "custom-status", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_STATUS); - assertContainsValue(response, "active", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_ACTIVE); - assertContainsValue(response, "values", MediaType.TEXT_PLAIN, "[one, two]"); + String body = extractable.asString(); + assertContainsValue(body, "name", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_NAME); + assertContainsValue(body, "custom-surname", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_SURNAME); + assertContainsValue(body, "custom-status", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_STATUS); + assertContainsValue(body, "active", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_ACTIVE); + assertContainsValue(body, "values", MediaType.TEXT_PLAIN, "[one, two]"); + + assertThat(extractable.header("Content-Type")).contains("boundary="); } @Test diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java index 5548a439f98fa..a3958b01ae37a 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java @@ -37,7 +37,7 @@ public String simple(@BeanParam FormData formData, Integer times) { } return formData.getName() + " - " + formData.active + " - " + times * formData.getNum() + " - " + formData.getStatus() + " - " - + formData.getHtmlPart().contentType() + " - " + Files.exists(formData.xmlPart) + " - " + + Files.exists(formData.getHtmlPart().filePath()) + " - " + Files.exists(formData.xmlPart) + " - " + formData.txtFile.exists(); } @@ -74,7 +74,7 @@ public String simple( } return name + " - " + active + " - " + times * num + " - " + status + " - " - + htmlPart.contentType() + " - " + Files.exists(xmlPart) + " - " + + Files.exists(htmlPart.filePath()) + " - " + Files.exists(xmlPart) + " - " + txtFile.exists(); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AuthenticationFailedExceptionHeaderTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AuthenticationFailedExceptionHeaderTest.java new file mode 100644 index 0000000000000..4b4c3d67efe79 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AuthenticationFailedExceptionHeaderTest.java @@ -0,0 +1,91 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import static io.vertx.core.http.HttpHeaders.LOCATION; +import static org.jboss.resteasy.reactive.RestResponse.StatusCode.FOUND; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.BaseAuthenticationRequest; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.restassured.RestAssured; +import io.restassured.http.Header; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +public class AuthenticationFailedExceptionHeaderTest { + + private static final String APP_PROPS = "" + + "quarkus.http.auth.permission.default.paths=/*\n" + + "quarkus.http.auth.permission.default.policy=authenticated"; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); + + @Test + public void testHeaders() { + // case-insensitive test that there is only one location header + // there has been duplicate location when both default auth failure handler and auth ex mapper send challenge + var response = RestAssured + .given() + .redirects() + .follow(false) + .when() + .get("/secured-route"); + response.then().statusCode(FOUND); + assertEquals(1, response.headers().asList().stream().map(Header::getName).map(String::toLowerCase) + .filter(LOCATION.toString()::equals).count()); + } + + @ApplicationScoped + public static class FailingAuthenticator implements HttpAuthenticationMechanism { + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + return Uni.createFrom().failure(new AuthenticationFailedException()); + } + + @Override + public Set> getCredentialTypes() { + return Set.of(BaseAuthenticationRequest.class); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().item(new ChallengeData(FOUND, LOCATION, "http://localhost:8080/")); + } + + } + + @ApplicationScoped + public static class BasicIdentityProvider implements IdentityProvider { + + @Override + public Class getRequestType() { + return BaseAuthenticationRequest.class; + } + + @Override + public Uni authenticate( + BaseAuthenticationRequest simpleAuthenticationRequest, + AuthenticationRequestContext authenticationRequestContext) { + return Uni.createFrom().nothing(); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AuthenticationRedirectExceptionHeaderTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AuthenticationRedirectExceptionHeaderTest.java new file mode 100644 index 0000000000000..35157e762ad77 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AuthenticationRedirectExceptionHeaderTest.java @@ -0,0 +1,100 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import static io.vertx.core.http.HttpHeaders.CACHE_CONTROL; +import static io.vertx.core.http.HttpHeaders.LOCATION; +import static org.jboss.resteasy.reactive.RestResponse.StatusCode.FOUND; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationRedirectException; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.BaseAuthenticationRequest; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.restassured.RestAssured; +import io.restassured.http.Header; +import io.restassured.response.Response; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +public class AuthenticationRedirectExceptionHeaderTest { + + private static final String APP_PROPS = "" + + "quarkus.http.auth.permission.default.paths=/*\n" + + "quarkus.http.auth.permission.default.policy=authenticated"; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); + + @Test + public void testHeaders() { + // case-insensitive test that Pragma, cache-control and location headers are only present once + // there were duplicate headers when both default auth failure handler and auth ex mapper set headers + var response = RestAssured + .given() + .redirects() + .follow(false) + .when() + .get("/secured-route"); + response.then().statusCode(FOUND); + assertEquals(1, getHeaderCount(response, LOCATION.toString())); + assertEquals(1, getHeaderCount(response, CACHE_CONTROL.toString())); + assertEquals(1, getHeaderCount(response, "Pragma")); + } + + private static int getHeaderCount(Response response, String headerName) { + headerName = headerName.toLowerCase(); + return (int) response.headers().asList().stream().map(Header::getName).map(String::toLowerCase) + .filter(headerName::equals).count(); + } + + @ApplicationScoped + public static class RedirectingAuthenticator implements HttpAuthenticationMechanism { + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + return Uni.createFrom().failure(new AuthenticationRedirectException(FOUND, "https://quarkus.io/")); + } + + @Override + public Set> getCredentialTypes() { + return Set.of(BaseAuthenticationRequest.class); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().item(new ChallengeData(FOUND, "header-name", "header-value")); + } + + } + + @ApplicationScoped + public static class BasicIdentityProvider implements IdentityProvider { + + @Override + public Class getRequestType() { + return BaseAuthenticationRequest.class; + } + + @Override + public Uni authenticate( + BaseAuthenticationRequest simpleAuthenticationRequest, + AuthenticationRequestContext authenticationRequestContext) { + return Uni.createFrom().nothing(); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/LazyAuthRolesAllowedConfigExpTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/LazyAuthRolesAllowedConfigExpTestCase.java new file mode 100644 index 0000000000000..32c204a3d3055 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/LazyAuthRolesAllowedConfigExpTestCase.java @@ -0,0 +1,44 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class LazyAuthRolesAllowedConfigExpTestCase { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(RolesAllowedResource.class, UserResource.class, + TestIdentityProvider.class, + TestIdentityController.class, + SecurityOverrideFilter.class) + .addAsResource(new StringAsset("quarkus.http.auth.proactive=false\n" + + "admin-config-property=admin\n"), "application.properties")); + + @BeforeAll + public static void setupUsers() { + TestIdentityController.resetRoles() + .add("admin", "admin", "admin") + .add("user", "user", "user"); + } + + @Test + public void testRolesAllowedConfigExp() { + RestAssured.given() + .header("user", "admin") + .header("role", "admin") + .get("/roles/admin-config-exp") + .then() + .statusCode(200) + .body(Matchers.equalTo("admin-config-exp")); + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionHandlerTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionHandlerTest.java index 946144889fbb4..49adcf5d43756 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionHandlerTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionHandlerTest.java @@ -59,9 +59,6 @@ public void testAuthCompletionExMapper() { .body(Matchers.equalTo(AUTHENTICATION_COMPLETION_EX)); } - /** - * Use failure handler as when proactive security is enabled, JAX-RS exception mappers won't do. - */ public static final class CustomAuthCompletionExceptionHandler { @Route(type = Route.HandlerType.FAILURE) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionMapperTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionMapperTest.java new file mode 100644 index 0000000000000..61f250e251ee3 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionMapperTest.java @@ -0,0 +1,72 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; + +import java.util.function.Supplier; + +import javax.ws.rs.core.Response; + +import org.hamcrest.Matchers; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationCompletionException; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.filter.cookie.CookieFilter; + +public class ProactiveAuthCompletionExceptionMapperTest { + + private static final String AUTHENTICATION_COMPLETION_EX = "AuthenticationCompletionException"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestIdentityController.class, + CustomAuthCompletionExceptionMapper.class) + .addAsResource(new StringAsset("quarkus.http.auth.form.enabled=true\n"), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("a d m i n", "a d m i n", "a d m i n"); + } + + @Test + public void testAuthCompletionExMapper() { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + RestAssured + .given() + .filter(new CookieFilter()) + .redirects().follow(false) + .when() + .formParam("j_username", "a d m i n") + .formParam("j_password", "a d m i n") + .cookie("quarkus-redirect-location", "https://quarkus.io/guides") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(401) + .body(Matchers.equalTo(AUTHENTICATION_COMPLETION_EX)); + } + + public static final class CustomAuthCompletionExceptionMapper { + + @ServerExceptionMapper(value = AuthenticationCompletionException.class) + public Response unauthorized() { + return Response.status(UNAUTHORIZED).entity(AUTHENTICATION_COMPLETION_EX).build(); + } + + } + +} \ No newline at end of file diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenExMapperTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenExMapperTest.java new file mode 100644 index 0000000000000..e16c8b2ee787f --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenExMapperTest.java @@ -0,0 +1,79 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import static javax.ws.rs.core.Response.Status.FORBIDDEN; +import static org.hamcrest.Matchers.equalTo; + +import java.util.function.Supplier; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ProactiveAuthHttpPolicyForbiddenExMapperTest { + + private static final String PROPERTIES = "quarkus.http.auth.basic=true\n" + + "quarkus.http.auth.policy.user-policy.roles-allowed=user\n" + + "quarkus.http.auth.permission.roles.paths=/secured\n" + + "quarkus.http.auth.permission.roles.policy=user-policy"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestIdentityController.class, CustomForbiddenExceptionMapper.class) + .addAsResource(new StringAsset(PROPERTIES), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("a d m i n", "a d m i n", "a d m i n"); + } + + @Test + public void testDeniedAccessAdminResource() { + RestAssured.given() + .auth().basic("a d m i n", "a d m i n") + .when().get("/secured") + .then() + .statusCode(403) + .body(equalTo(CustomForbiddenExceptionMapper.CUSTOM_FORBIDDEN_EXCEPTION_MAPPER)); + } + + @Path("/secured") + public static class SecuredResource { + + @GET + public String get() { + throw new IllegalStateException(); + } + + } + + public static final class CustomForbiddenExceptionMapper { + + public static final String CUSTOM_FORBIDDEN_EXCEPTION_MAPPER = CustomForbiddenExceptionMapper.class.getName(); + + @ServerExceptionMapper(value = ForbiddenException.class) + public Response forbidden() { + return Response.status(FORBIDDEN).entity(CUSTOM_FORBIDDEN_EXCEPTION_MAPPER).build(); + } + + } + +} \ No newline at end of file diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenHandlerTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenHandlerTest.java index 81678e9cddb4d..15ce1ca3a4b96 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenHandlerTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenHandlerTest.java @@ -65,9 +65,6 @@ public String get() { } - /** - * Use failure handler as when proactive security is enabled, JAX-RS exception mappers won't do. - */ public static final class CustomForbiddenFailureHandler { public static final String CUSTOM_FORBIDDEN_EXCEPTION_MAPPER = CustomForbiddenFailureHandler.class.getName(); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedResource.java index 3ea7a09536802..ede7b1a80968b 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RolesAllowedResource.java @@ -32,6 +32,13 @@ public String admin() { return "admin"; } + @Path("/admin-config-exp") + @RolesAllowed("${admin-config-property:missing}") + @GET + public String adminConfigExp() { + return "admin-config-exp"; + } + @NonBlocking @Path("/admin/security-identity") @RolesAllowed("admin") diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SeparatorQueryParamTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SeparatorQueryParamTest.java new file mode 100644 index 0000000000000..c520568020663 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SeparatorQueryParamTest.java @@ -0,0 +1,104 @@ +package io.quarkus.resteasy.reactive.server.test.simple; + +import static io.restassured.RestAssured.*; + +import java.util.List; + +import javax.ws.rs.BeanParam; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.hamcrest.Matchers; +import org.jboss.resteasy.reactive.RestQuery; +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.Separator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class SeparatorQueryParamTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(HelloResource.class)); + + @Test + public void noQueryParams() { + get("/hello") + .then() + .statusCode(200) + .body(Matchers.equalTo("hello world")) + .header("x-size", "0"); + } + + @Test + public void noQueryParamsBean() { + get("/hello/bean") + .then() + .statusCode(200) + .body(Matchers.equalTo("hello world")) + .header("x-size", "0"); + } + + @Test + public void singleQueryParam() { + get("/hello?name=foo") + .then() + .statusCode(200) + .body(Matchers.equalTo("hello foo")) + .header("x-size", "1"); + } + + @Test + public void singleQueryParamBean() { + get("/hello/bean?name=foo") + .then() + .statusCode(200) + .body(Matchers.equalTo("hello foo")) + .header("x-size", "1"); + } + + @Test + public void multipleQueryParams() { + get("/hello?name=foo,bar&name=one,two,three&name=yolo") + .then() + .statusCode(200) + .body(Matchers.equalTo("hello foo bar one two three yolo")) + .header("x-size", "6"); + } + + @Path("hello") + public static class HelloResource { + + @GET + public RestResponse hello(@RestQuery("name") @Separator(",") List names) { + return toResponse(names); + } + + @GET + @Path("bean") + public RestResponse helloBean(@BeanParam Bean bean) { + return toResponse(bean.names); + } + + private RestResponse toResponse(List names) { + int size = names.size(); + String body = ""; + if (names.isEmpty()) { + body = "hello world"; + } else { + body = "hello " + String.join(" ", names); + } + return RestResponse.ResponseBuilder.ok(body).header("x-size", size).build(); + } + + } + + public static class Bean { + @RestQuery("name") + @Separator(",") + public List names; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamResource.java index def237ac8da81..307d5b77d5d81 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamResource.java @@ -8,7 +8,10 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import javax.ws.rs.sse.OutboundSseEvent; +import javax.ws.rs.sse.Sse; import org.jboss.resteasy.reactive.common.util.MultiCollectors; import org.reactivestreams.Publisher; @@ -152,4 +155,13 @@ public Multi sse() { public Multi sseThrows() { throw new IllegalStateException("STOP"); } + + @Path("sse/raw") + @GET + @Produces(MediaType.SERVER_SENT_EVENTS) + public Multi sseRaw(@Context Sse sse) { + return Multi.createFrom().items(sse.newEventBuilder().id("one").data("uno").name("eins").build(), + sse.newEventBuilder().id("two").data("dos").name("zwei").build(), + sse.newEventBuilder().id("three").data("tres").name("drei").build()); + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java index 36df4075e7606..d43b1ddb1249b 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java @@ -237,4 +237,32 @@ public void testSseThrows() throws InterruptedException { Assertions.assertEquals(1, errors.size()); } } + + @Test + public void testSseForMultiWithOutboundSseEvent() throws InterruptedException { + Client client = ClientBuilder.newBuilder().build(); + WebTarget target = client.target(this.uri.toString() + "stream/sse/raw"); + try (SseEventSource sse = SseEventSource.target(target).build()) { + CountDownLatch latch = new CountDownLatch(1); + List errors = new CopyOnWriteArrayList<>(); + List results = new CopyOnWriteArrayList<>(); + List ids = new CopyOnWriteArrayList<>(); + List names = new CopyOnWriteArrayList<>(); + sse.register(event -> { + results.add(event.readData()); + ids.add(event.getId()); + names.add(event.getName()); + }, error -> { + errors.add(error); + }, () -> { + latch.countDown(); + }); + sse.open(); + Assertions.assertTrue(latch.await(20, TimeUnit.SECONDS)); + Assertions.assertEquals(Arrays.asList("uno", "dos", "tres"), results); + Assertions.assertEquals(Arrays.asList("one", "two", "three"), ids); + Assertions.assertEquals(Arrays.asList("eins", "zwei", "drei"), names); + Assertions.assertEquals(0, errors.size()); + } + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/EndpointDisabled.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/EndpointDisabled.java new file mode 100644 index 0000000000000..9e73af1cea4ee --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/EndpointDisabled.java @@ -0,0 +1,31 @@ +package io.quarkus.resteasy.reactive.server; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Provides the ability to conditionally enable a JAX-RS Resource class at runtime based on the value of a property. + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE }) +public @interface EndpointDisabled { + + /** + * Name of the property to check + */ + String name(); + + /** + * Expected {@code String} value of the property (specified by {@code name}) if the Resource class is to be disabled + */ + String stringValue(); + + /** + * Determines if the Resource class is to be disabled when the property name specified by {@code name} has not been + * specified at all + */ + boolean disableIfMissing() default false; +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java index 8cc7fde9154a7..12ff82477be4c 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java @@ -16,6 +16,7 @@ import javax.ws.rs.core.Application; +import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.common.core.SingletonBeanFactory; import org.jboss.resteasy.reactive.common.model.ResourceContextResolver; @@ -54,6 +55,7 @@ import io.quarkus.security.AuthenticationCompletionException; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.AuthenticationRedirectException; +import io.quarkus.security.ForbiddenException; import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; @@ -211,23 +213,52 @@ public void accept(RoutingContext routingContext) { return new ResteasyReactiveVertxHandler(eventCustomizer, initialHandler); } - public Handler failureHandler(RuntimeValue restInitialHandlerRuntimeValue) { + public Handler failureHandler(RuntimeValue restInitialHandlerRuntimeValue, + boolean noCustomAuthCompletionExMapper, boolean noCustomAuthFailureExMapper, boolean noCustomAuthRedirectExMapper, + boolean proactive) { final RestInitialHandler restInitialHandler = restInitialHandlerRuntimeValue.getValue(); // process auth failures with abort handlers return new Handler() { @Override public void handle(RoutingContext event) { - // this condition prevent exception mappers from handling auth failure exceptions when proactive - // security is enabled as for now, community decided that's expected behavior and only way for - // users to handle the exceptions is to define their own failure handler as in Reactive Routes - // more info here: https://github.com/quarkusio/quarkus/pull/28648#issuecomment-1287203946 - final boolean eventFailedByRESTEasyReactive = event - .get(QuarkusHttpUser.AUTH_FAILURE_HANDLER) instanceof FailingDefaultAuthFailureHandler; + // special handling when proactive auth is enabled as then we know default auth failure handler already run + if (proactive && event.get(QuarkusHttpUser.AUTH_FAILURE_HANDLER) instanceof DefaultAuthFailureHandler) { + // we want to prevent repeated handling of exceptions if user don't want to handle exception himself + // we do not pass exception to abort handlers if proactive auth is enabled and user did not + // provide custom ex. mapper; we replace default auth failure handler as soon as we can, so that + // we can handle Quarkus Security Exceptions ourselves + if (event.failure() instanceof AuthenticationFailedException) { + if (noCustomAuthFailureExMapper) { + event.next(); + } else { + // allow response customization + restInitialHandler.beginProcessing(event, event.failure()); + } + return; + } else if (event.failure() instanceof AuthenticationCompletionException) { + if (noCustomAuthCompletionExMapper) { + event.next(); + } else { + // allow response customization + restInitialHandler.beginProcessing(event, event.failure()); + } + return; + } else if (event.failure() instanceof AuthenticationRedirectException) { + if (noCustomAuthRedirectExMapper) { + event.next(); + } else { + // allow response customization + restInitialHandler.beginProcessing(event, event.failure()); + } + return; + } + } - if (eventFailedByRESTEasyReactive && (event.failure() instanceof AuthenticationFailedException + if (event.failure() instanceof AuthenticationFailedException || event.failure() instanceof AuthenticationCompletionException - || event.failure() instanceof AuthenticationRedirectException)) { + || event.failure() instanceof AuthenticationRedirectException + || event.failure() instanceof ForbiddenException) { restInitialHandler.beginProcessing(event, event.failure()); } else { event.next(); @@ -316,6 +347,20 @@ public BeanFactory apply(Class aClass) { }; } + public Supplier disableIfPropertyMatches(String propertyName, String propertyValue, boolean disableIfMissing) { + return new Supplier<>() { + @Override + public Boolean get() { + String value = ConfigProvider.getConfig().getConfigValue(propertyName).getValue(); + if (value == null) { + return disableIfMissing; + } else { + return value.equals(propertyValue); + } + } + }; + } + public ServerSerialisers createServerSerialisers() { return new ServerSerialisers(); } @@ -333,6 +378,20 @@ public void handle(RoutingContext event) { }; } + public Supplier beanUnavailable(String className) { + return new Supplier<>() { + @Override + public Boolean get() { + try { + return !Arc.container().select(Class.forName(className, false, Thread.currentThread() + .getContextClassLoader())).isResolvable(); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Unable to determine if bean '" + className + "' is available", e); + } + } + }; + } + private static final class FailingDefaultAuthFailureHandler implements BiConsumer { @Override diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java index d071a33698b4c..af2fa0ff05c17 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java @@ -35,6 +35,7 @@ public Supplier runtimeConfiguration(RuntimeValue type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { + try { + return super.readFrom(type, genericType, annotations, mediaType, httpHeaders, entityStream); + } catch (StreamReadException | DatabindException e) { + throw new ClientWebApplicationException(e, Response.Status.BAD_REQUEST); + } + } + @Override public void handle(RestClientRequestContext requestContext) { this.context = requestContext; diff --git a/extensions/resteasy-reactive/rest-client-reactive-jsonb/deployment/src/main/java/io/quarkus/rest/client/reactive/jsonb/deployment/RestClientReactiveJsonbProcessor.java b/extensions/resteasy-reactive/rest-client-reactive-jsonb/deployment/src/main/java/io/quarkus/rest/client/reactive/jsonb/deployment/RestClientReactiveJsonbProcessor.java index 533c8f0cbc812..793d05e9bd93c 100644 --- a/extensions/resteasy-reactive/rest-client-reactive-jsonb/deployment/src/main/java/io/quarkus/rest/client/reactive/jsonb/deployment/RestClientReactiveJsonbProcessor.java +++ b/extensions/resteasy-reactive/rest-client-reactive-jsonb/deployment/src/main/java/io/quarkus/rest/client/reactive/jsonb/deployment/RestClientReactiveJsonbProcessor.java @@ -2,9 +2,14 @@ import static io.quarkus.deployment.Feature.REST_CLIENT_REACTIVE_JSONB; +import javax.ws.rs.RuntimeType; + import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.resteasy.reactive.jsonb.common.deployment.ResteasyReactiveJsonbCommonProcessor; +import io.quarkus.resteasy.reactive.spi.MessageBodyReaderBuildItem; +import io.quarkus.resteasy.reactive.spi.MessageBodyWriterBuildItem; public class RestClientReactiveJsonbProcessor { @@ -12,4 +17,11 @@ public class RestClientReactiveJsonbProcessor { void feature(BuildProducer features) { features.produce(new FeatureBuildItem(REST_CLIENT_REACTIVE_JSONB)); } + + @BuildStep + void additionalProviders(BuildProducer additionalReaders, + BuildProducer additionalWriters) { + ResteasyReactiveJsonbCommonProcessor.additionalProviders(additionalReaders, additionalWriters, + RuntimeType.CLIENT); + } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/BasicRestClientTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/BasicRestClientTest.java index ab885f48c3866..3148c53acf512 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/BasicRestClientTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/BasicRestClientTest.java @@ -1,10 +1,7 @@ package io.quarkus.rest.client.reactive; -import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import java.time.Duration; import java.util.Map; import java.util.Set; @@ -75,23 +72,4 @@ void shouldMapQueryParamsWithSpecialCharacters() { assertThat(map.get("p5")).isEqualTo("5"); assertThat(map.get("p6")).isEqualTo("6"); } - - /** - * Test to reproduce https://github.com/quarkusio/quarkus/issues/28818. - */ - @Test - void shouldCloseConnectionsWhenFailures() { - // It's using 30 seconds because it's the default timeout to release connections. This timeout should not be taken into - // account when there are failures, and we should be able to call 3 times to the service without waiting. - await().atMost(Duration.ofSeconds(30)) - .until(() -> { - for (int call = 0; call < 3; call++) { - given() - .when().get("/hello/callClientForImageInfo") - .then() - .statusCode(500); - } - return true; - }); - } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/HelloClient2.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/HelloClient2.java index 2c2c31eab3473..660c55a0260d9 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/HelloClient2.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/HelloClient2.java @@ -4,7 +4,6 @@ import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; -import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; @@ -24,10 +23,4 @@ public interface HelloClient2 { @GET @Path("delay") Uni delay(); - - @POST - @Path("/imageInfo") - @Consumes("image/gif") - @Produces(MediaType.TEXT_PLAIN) - String imageInfo(byte[] imageFile); } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/HelloResource.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/HelloResource.java index 65b0dc25760c7..f298c0b303011 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/HelloResource.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/HelloResource.java @@ -78,23 +78,4 @@ public Uni delay() { return Uni.createFrom().item("Hello") .onItem().delayIt().by(Duration.ofMillis(500)); } - - @Path("callClientForImageInfo") - @GET - public String callClientForImageInfo() { - int size = 1024 * 1024 * 5; - - byte[] buffer = new byte[size]; - - //Should provoke 415 Unsupported Media Type - return client2.imageInfo(buffer); - } - - @POST - @Consumes({ "image/jpeg", "image/png" }) - @Path("/imageInfo") - @Produces(MediaType.TEXT_PLAIN) - public String imageInfo(byte[] imageFile) { - throw new IllegalStateException("This method should never be invoked"); - } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/SubResourceTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/SubResourceTest.java index ee757d0222937..53e6cc1d39fb1 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/SubResourceTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/SubResourceTest.java @@ -61,6 +61,14 @@ void shouldPassParamsToSubSubResource() { assertThat(result).isEqualTo("rt/mthd/sub/subSimple"); } + @Test + void shouldPassPathParamsToSubSubResource() { + // should result in sending GET /path/rt/mthd/sub/s/ss + RootClient rootClient = RestClientBuilder.newBuilder().baseUri(baseUri).build(RootClient.class); + String result = rootClient.sub("rt", "mthd").sub("s").get("ss"); + assertThat(result).isEqualTo("rt/mthd/sub/s/ss"); + } + @Test void shouldDoMultiplePosts() { RootClient rootClient = RestClientBuilder.newBuilder().baseUri(baseUri).build(RootClient.class); @@ -129,6 +137,9 @@ interface SubClient { @Path("/sub") SubSubClient sub(); + @Path("/sub/{methodParam}") + SubSubClient sub(@PathParam("methodParam") String methodParam); + @POST @ClientHeaderParam(name = "overridable", value = "SubClient") @ClientHeaderParam(name = "fromSubMethod", value = "{fillingMethod}") @@ -146,6 +157,10 @@ interface SubSubClient { @Path("/subSimple") String simpleSub(); + @GET + @Path("/{methodParam}") + String get(@PathParam("methodParam") String methodParam); + @POST @ClientHeaderParam(name = "overridable", value = "SubSubClient") @ClientHeaderParam(name = "fromSubMethod", value = "{fillingMethod}") diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java index 81c315c22837d..a6f18ac1f16eb 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java @@ -89,6 +89,11 @@ public RestClientBuilderImpl sslContext(SSLContext sslContext) { return this; } + public RestClientBuilderImpl verifyHost(boolean verifyHost) { + clientBuilder.verifyHost(verifyHost); + return this; + } + @Override public RestClientBuilderImpl trustStore(KeyStore trustStore) { clientBuilder.trustStore(trustStore); @@ -319,6 +324,7 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi .orElse(false); clientBuilder.trustAll(trustAll); + restClientsConfig.verifyHost.ifPresent(clientBuilder::verifyHost); String userAgent = (String) getConfiguration().getProperty(QuarkusRestClientProperties.USER_AGENT); if (userAgent != null) { diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java index d5f998178a40c..da933186bb855 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java @@ -201,6 +201,9 @@ private void configureSsl(RestClientBuilderImpl builder) { if (maybeHostnameVerifier.isPresent()) { registerHostnameVerifier(maybeHostnameVerifier.get(), builder); } + + oneOf(clientConfigByClassName().verifyHost, clientConfigByConfigKey().verifyHost, configRoot.verifyHost) + .ifPresent(builder::verifyHost); } private void registerHostnameVerifier(String verifier, RestClientBuilder builder) { diff --git a/extensions/scheduler/deployment/pom.xml b/extensions/scheduler/deployment/pom.xml index 0140a6cdbe37b..18123b3c006d6 100644 --- a/extensions/scheduler/deployment/pom.xml +++ b/extensions/scheduler/deployment/pom.xml @@ -36,6 +36,11 @@ quarkus-micrometer-registry-prometheus-deployment test + + io.quarkus + quarkus-opentelemetry-deployment + test + io.quarkus quarkus-junit5-internal diff --git a/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java index 8171f6eb91dc4..a5b314107ad2b 100644 --- a/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java +++ b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java @@ -330,14 +330,15 @@ public DevConsoleRouteBuildItem devConsole(BuildProducer metricsCapability) { + public void metrics(SchedulerConfig config, + Optional metricsCapability, + BuildProducer annotationsTransformer) { if (config.metricsEnabled && metricsCapability.isPresent()) { DotName micrometerTimed = DotName.createSimple("io.micrometer.core.annotation.Timed"); DotName mpTimed = DotName.createSimple("org.eclipse.microprofile.metrics.annotation.Timed"); - return new AnnotationsTransformerBuildItem(AnnotationsTransformer.builder() + annotationsTransformer.produce(new AnnotationsTransformerBuildItem(AnnotationsTransformer.builder() .appliesTo(METHOD) .whenContainsAny(List.of(SchedulerDotNames.SCHEDULED_NAME, SchedulerDotNames.SCHEDULES_NAME)) .whenContainsNone(List.of(micrometerTimed, @@ -366,9 +367,32 @@ public AnnotationsTransformerBuildItem metrics(SchedulerConfig config, scheduledMethod.declaringClass().name(), scheduledMethod.name()); } - })); + }))); + } + } + + @BuildStep + public void tracing(SchedulerConfig config, + Capabilities capabilities, BuildProducer annotationsTransformer) { + + if (config.tracingEnabled && capabilities.isPresent(Capability.OPENTELEMETRY_TRACER)) { + DotName withSpan = DotName.createSimple("io.opentelemetry.instrumentation.annotations.WithSpan"); + DotName legacyWithSpan = DotName.createSimple("io.opentelemetry.extension.annotations.WithSpan"); + + annotationsTransformer.produce(new AnnotationsTransformerBuildItem(AnnotationsTransformer.builder() + .appliesTo(METHOD) + .whenContainsAny(List.of(SchedulerDotNames.SCHEDULED_NAME, SchedulerDotNames.SCHEDULES_NAME)) + .whenContainsNone(List.of(withSpan, legacyWithSpan)) + .transform(context -> { + MethodInfo scheduledMethod = context.getTarget().asMethod(); + context.transform() + .add(withSpan) + .done(); + LOGGER.debugf("Added OpenTelemetry @WithSpan to a @Scheduled method %s#%s()", + scheduledMethod.declaringClass().name(), + scheduledMethod.name()); + }))); } - return null; } private String generateInvoker(ScheduledBusinessMethodItem scheduledMethod, ClassOutput classOutput) { diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/tracing/OpenTelemetryTracingTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/tracing/OpenTelemetryTracingTest.java new file mode 100644 index 0000000000000..b2afbbc535be8 --- /dev/null +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/tracing/OpenTelemetryTracingTest.java @@ -0,0 +1,68 @@ +package io.quarkus.scheduler.test.tracing; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.api.trace.Span; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.mutiny.Uni; + +public class OpenTelemetryTracingTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Jobs.class) + .addAsResource(new StringAsset("quarkus.scheduler.tracing.enabled=true"), + "application.properties")); + + @Test + void testWithSpan() throws InterruptedException { + assertTrue(Jobs.latch.await(5, TimeUnit.SECONDS)); + assertTrue(Jobs.nonBlockingLatch.await(5, TimeUnit.SECONDS)); + + // assert that different spand ids were used + assertTrue(Jobs.spanIds.stream().collect(Collectors.toSet()).size() >= 2); + + // assert that non-blocing scheduled methods are not supported + // when the WithSpanInterceptor is fixed and this test fails we should update the assertion and update the docs + assertTrue(Jobs.nonBlockingSpanIds.stream().collect(Collectors.toSet()).size() == 1); + + } + + static class Jobs { + + static final CountDownLatch latch = new CountDownLatch(2); + static final CountDownLatch nonBlockingLatch = new CountDownLatch(2); + + static final List spanIds = new CopyOnWriteArrayList<>(); + static final List nonBlockingSpanIds = new CopyOnWriteArrayList<>(); + + @Scheduled(every = "1s") + void everySecond() { + spanIds.add(Span.current().getSpanContext().getSpanId()); + latch.countDown(); + } + + @Scheduled(every = "1s") + Uni everySecondNonBlocking() { + return Uni.createFrom().item(() -> { + nonBlockingSpanIds.add(Span.current().getSpanContext().getSpanId()); + nonBlockingLatch.countDown(); + return true; + }).replaceWithVoid(); + } + + } + +} diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerConfig.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerConfig.java index 16361106b2a14..9e6274a255de5 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerConfig.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerConfig.java @@ -24,4 +24,10 @@ public class SchedulerConfig { @ConfigItem(name = "metrics.enabled") public boolean metricsEnabled; + /** + * Tracing will be enabled if the OpenTelemetry extension is present and this value is true. + */ + @ConfigItem(name = "tracing.enabled") + public boolean tracingEnabled; + } diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/ConfigExpRolesAllowedSecurityCheckBuildItem.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/ConfigExpRolesAllowedSecurityCheckBuildItem.java new file mode 100644 index 0000000000000..fe77ebcd035f6 --- /dev/null +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/ConfigExpRolesAllowedSecurityCheckBuildItem.java @@ -0,0 +1,14 @@ +package io.quarkus.security.deployment; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.security.runtime.interceptor.check.SupplierRolesAllowedCheck; + +/** + * Marker build item that is used to indicate that there are {@link SupplierRolesAllowedCheck}s whose roles + * contains config expressions that should be resolved at runtime. + */ +public final class ConfigExpRolesAllowedSecurityCheckBuildItem extends SimpleBuildItem { + + ConfigExpRolesAllowedSecurityCheckBuildItem() { + } +} diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java index d690825fbd986..1b8d98a392ae0 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java @@ -14,12 +14,16 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; @@ -49,6 +53,7 @@ import io.quarkus.deployment.builditem.GeneratedNativeImageClassBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.NativeImageFeatureBuildItem; +import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem; import io.quarkus.deployment.builditem.nativeimage.JPMSExportBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageSecurityProviderBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; @@ -64,6 +69,7 @@ import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.RuntimeValue; import io.quarkus.security.runtime.IdentityProviderManagerCreator; +import io.quarkus.security.runtime.QuarkusSecurityRolesAllowedConfigBuilder; import io.quarkus.security.runtime.SecurityBuildTimeConfig; import io.quarkus.security.runtime.SecurityCheckRecorder; import io.quarkus.security.runtime.SecurityIdentityAssociation; @@ -473,8 +479,10 @@ void transformSecurityAnnotations(BuildProducer @BuildStep @Record(ExecutionTime.STATIC_INIT) void gatherSecurityChecks(BuildProducer syntheticBeans, + BuildProducer configExpSecurityCheckProducer, BeanArchiveIndexBuildItem beanArchiveBuildItem, BuildProducer classPredicate, + BuildProducer configBuilderProducer, List additionalSecuredMethods, SecurityCheckRecorder recorder, List additionalSecurityChecks, SecurityBuildTimeConfig config) { @@ -489,8 +497,8 @@ void gatherSecurityChecks(BuildProducer syntheticBeans, } IndexView index = beanArchiveBuildItem.getIndex(); - Map securityChecks = gatherSecurityAnnotations( - index, additionalSecured.values(), config.denyUnannotated, recorder); + Map securityChecks = gatherSecurityAnnotations(index, configExpSecurityCheckProducer, + additionalSecured.values(), config.denyUnannotated, recorder, configBuilderProducer); for (AdditionalSecurityCheckBuildItem additionalSecurityCheck : additionalSecurityChecks) { securityChecks.put(additionalSecurityCheck.getMethodInfo(), additionalSecurityCheck.getSecurityCheck()); @@ -520,22 +528,38 @@ void gatherSecurityChecks(BuildProducer syntheticBeans, }).done()); } - private Map gatherSecurityAnnotations( - IndexView index, - Collection additionalSecuredMethods, boolean denyUnannotated, SecurityCheckRecorder recorder) { + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public void resolveConfigExpressionRoles(Optional configExpRolesChecks, + SecurityCheckRecorder recorder) { + if (configExpRolesChecks.isPresent()) { + // we created supplier security check for each role set with at least one config expression + // now we need to resolve config expression so that if there are any failures they happen when app starts + // rather than first time request is checked (which would be more likely to affect end user) + recorder.resolveRolesAllowedConfigExpRoles(); + } + } + + private Map gatherSecurityAnnotations(IndexView index, + BuildProducer configExpSecurityCheckProducer, + Collection additionalSecuredMethods, boolean denyUnannotated, SecurityCheckRecorder recorder, + BuildProducer configBuilderProducer) { Map methodToInstanceCollector = new HashMap<>(); Map classAnnotations = new HashMap<>(); - Map result = new HashMap<>(gatherSecurityAnnotations( + Map result = new HashMap<>(); + gatherSecurityAnnotations(index, DotNames.PERMIT_ALL, methodToInstanceCollector, classAnnotations, + ((m, i) -> result.put(m, recorder.permitAll()))); + gatherSecurityAnnotations(index, DotNames.AUTHENTICATED, methodToInstanceCollector, classAnnotations, + ((m, i) -> result.put(m, recorder.authenticated()))); + gatherSecurityAnnotations(index, DENY_ALL, methodToInstanceCollector, classAnnotations, + ((m, i) -> result.put(m, recorder.denyAll()))); + + // here we just collect all methods annotated with @RolesAllowed + Map methodToRoles = new HashMap<>(); + gatherSecurityAnnotations( index, ROLES_ALLOWED, methodToInstanceCollector, classAnnotations, - (instance -> recorder.rolesAllowed(instance.value().asStringArray())))); - result.putAll(gatherSecurityAnnotations(index, DotNames.PERMIT_ALL, methodToInstanceCollector, classAnnotations, - (instance -> recorder.permitAll()))); - result.putAll(gatherSecurityAnnotations(index, DotNames.AUTHENTICATED, methodToInstanceCollector, classAnnotations, - (instance -> recorder.authenticated()))); - - result.putAll(gatherSecurityAnnotations(index, DENY_ALL, methodToInstanceCollector, classAnnotations, - (instance -> recorder.denyAll()))); + ((methodInfo, instance) -> methodToRoles.put(methodInfo, instance.value().asStringArray()))); /* * Handle additional secured methods by adding the denyAll/rolesAllowed check to all public non-static methods @@ -548,8 +572,8 @@ private Map gatherSecurityAnnotations( AnnotationInstance alreadyExistingInstance = methodToInstanceCollector.get(additionalSecuredMethod.methodInfo); if (additionalSecuredMethod.rolesAllowed.isPresent()) { if (alreadyExistingInstance == null) { - result.put(additionalSecuredMethod.methodInfo, recorder - .rolesAllowed(additionalSecuredMethod.rolesAllowed.get().toArray(String[]::new))); + methodToRoles.put(additionalSecuredMethod.methodInfo, + additionalSecuredMethod.rolesAllowed.get().toArray(String[]::new)); } else if (alreadyHasAnnotation(alreadyExistingInstance, ROLES_ALLOWED)) { // we should not try to add second @RolesAllowed throw new IllegalStateException("Method " + additionalSecuredMethod.methodInfo.declaringClass() + "#" @@ -568,6 +592,50 @@ private Map gatherSecurityAnnotations( } } + // create roles allowed security checks + // we create only one security check for each role set + Map, SecurityCheck> cache = new HashMap<>(); + final AtomicInteger keyIndex = new AtomicInteger(0); + final AtomicBoolean hasRolesAllowedCheckWithConfigExp = new AtomicBoolean(false); + for (Map.Entry entry : methodToRoles.entrySet()) { + final MethodInfo methodInfo = entry.getKey(); + final String[] allowedRoles = entry.getValue(); + result.put(methodInfo, + cache.computeIfAbsent(getSetForKey(allowedRoles), new Function, SecurityCheck>() { + @Override + public SecurityCheck apply(Set allowedRolesSet) { + final int[] configExpressionPositions = configExpressionPositions(allowedRoles); + if (configExpressionPositions.length > 0) { + // we need to use supplier check as security checks are created during static init + // while config expressions are resolved during runtime + hasRolesAllowedCheckWithConfigExp.set(true); + + // we don't create security check for each method, therefore we need artificial keys + // we can safely use numbers as RolesAllowed config source prefix all keys + final int[] configKeys = new int[configExpressionPositions.length]; + for (int i = 0; i < configExpressionPositions.length; i++) { + // now we just collect artificial keys, but + // before we add the property to the Config system, we prefix it, e.g. + // @RolesAllowed("${admin}") -> QuarkusSecurityRolesAllowedConfigSource.property-0=${admin} + configKeys[i] = keyIndex.getAndIncrement(); + } + return recorder.rolesAllowedSupplier(allowedRoles, configExpressionPositions, configKeys); + } + return recorder.rolesAllowed(allowedRoles); + } + })); + } + + if (hasRolesAllowedCheckWithConfigExp.get()) { + // make sure config expressions are resolved when app starts + configExpSecurityCheckProducer + .produce(new ConfigExpRolesAllowedSecurityCheckBuildItem()); + + // register config source with the Config system + configBuilderProducer + .produce(new RunTimeConfigBuilderBuildItem(QuarkusSecurityRolesAllowedConfigBuilder.class.getName())); + } + /* * If we need to add the denyAll security check to all unannotated methods, we simply go through all secured methods, * collect the declaring classes, then go through all methods of the classes and add the necessary check @@ -593,6 +661,31 @@ private Map gatherSecurityAnnotations( return result; } + public static int[] configExpressionPositions(String[] allowedRoles) { + final Set expPositions = new HashSet<>(); + for (int i = 0; i < allowedRoles.length; i++) { + final int exprStart = allowedRoles[i].indexOf("${"); + if (exprStart >= 0 && allowedRoles[i].indexOf('}', exprStart + 2) > 0) { + expPositions.add(i); + } + } + + if (expPositions.isEmpty()) { + return new int[0]; + } + return expPositions.stream().mapToInt(Integer::intValue).toArray(); + } + + private static Set getSetForKey(String[] allowedRoles) { + if (allowedRoles.length == 0) { // shouldn't happen, but let's be on the safe side + return Collections.emptySet(); + } else if (allowedRoles.length == 1) { + return Collections.singleton(allowedRoles[0]); + } + // use a set in order to avoid caring about the order of elements + return new HashSet<>(Arrays.asList(allowedRoles)); + } + private boolean alreadyHasAnnotation(AnnotationInstance alreadyExistingInstance, DotName annotationName) { return alreadyExistingInstance.target().kind() == AnnotationTarget.Kind.METHOD && alreadyExistingInstance.name().equals(annotationName); @@ -603,13 +696,11 @@ private boolean isPublicNonStaticNonConstructor(MethodInfo methodInfo) { && !"".equals(methodInfo.name()); } - private Map gatherSecurityAnnotations( + private void gatherSecurityAnnotations( IndexView index, DotName dotName, Map alreadyCheckedMethods, Map classLevelAnnotations, - Function securityCheckInstanceCreator) { - - Map result = new HashMap<>(); + BiConsumer putResult) { Collection instances = index.getAnnotations(dotName); // make sure we process annotations on methods first @@ -622,7 +713,7 @@ private Map gatherSecurityAnnotations( + " is annotated with multiple security annotations"); } alreadyCheckedMethods.put(methodInfo, instance); - result.put(methodInfo, securityCheckInstanceCreator.apply(instance)); + putResult.accept(methodInfo, instance); } } // now add the class annotations to methods if they haven't already been annotated @@ -636,7 +727,7 @@ private Map gatherSecurityAnnotations( for (MethodInfo methodInfo : methods) { AnnotationInstance alreadyExistingInstance = alreadyCheckedMethods.get(methodInfo); if ((alreadyExistingInstance == null)) { - result.put(methodInfo, securityCheckInstanceCreator.apply(instance)); + putResult.accept(methodInfo, instance); } } } else { @@ -647,8 +738,6 @@ private Map gatherSecurityAnnotations( } } - - return result; } @BuildStep diff --git a/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/ConfigExpressionDetectionTest.java b/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/ConfigExpressionDetectionTest.java new file mode 100644 index 0000000000000..945e0b93fc5b6 --- /dev/null +++ b/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/ConfigExpressionDetectionTest.java @@ -0,0 +1,102 @@ +package io.quarkus.security.test.rolesallowed; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.security.RolesAllowed; + +import org.jboss.jandex.DotName; +import org.jboss.jandex.Indexer; +import org.jboss.jandex.MethodInfo; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.security.deployment.DotNames; +import io.quarkus.security.deployment.SecurityProcessor; + +public class ConfigExpressionDetectionTest { + + private static final Map VALID_VALUES; + + static { + // point here is to verify expected values gathered from @RolesAllowed annotation are detected correctly + var indexer = new Indexer(); + for (Class aClass : new Class[] { ConfigExpressionDetectionTest.class, ValidValues.class }) { + try (InputStream stream = ConfigExpressionDetectionTest.class.getClassLoader() + .getResourceAsStream(aClass.getName().replace('.', '/') + ".class")) { + assert stream != null; + indexer.index(stream); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + var index = indexer.complete(); + VALID_VALUES = new HashMap<>(); + for (MethodInfo methodInfo : index.getClassByName(DotName.createSimple(ValidValues.class.getName())).methods()) { + var annotation = methodInfo.annotation(DotNames.ROLES_ALLOWED); + if (annotation != null) { + VALID_VALUES.put(methodInfo.name(), annotation.value().asStringArray()); + } + } + } + + @Test + void testConfigExpIsDetected() { + VALID_VALUES.forEach((methodName, rolesAllowed) -> { + final int[] expressionPositions; + switch (methodName) { + case "secured1": + expressionPositions = new ValidValues().secured1(); + break; + case "secured2": + expressionPositions = new ValidValues().secured2(); + break; + case "secured3": + expressionPositions = new ValidValues().secured3(); + break; + case "secured4": + expressionPositions = new ValidValues().secured4(); + break; + case "secured5": + expressionPositions = new ValidValues().secured5(); + break; + default: + throw new IllegalStateException(); + } + Assertions.assertArrayEquals(SecurityProcessor.configExpressionPositions(rolesAllowed), + expressionPositions); + }); + } + + public static final class ValidValues { + + @RolesAllowed({ "first-role", "${second-role}", "third-role" }) + public int[] secured1() { + return new int[] { 1 }; + } + + @RolesAllowed({ "${first-role}", "second-role", "${third-role}" }) + public int[] secured2() { + return new int[] { 0, 2 }; + } + + @RolesAllowed({ "${first-role}", "${second-role}", "${third-role: defaultValue}" }) + public int[] secured3() { + return new int[] { 0, 1, 2 }; + } + + @RolesAllowed({ "first-role", "second-role", "third-role" }) + public int[] secured4() { + return new int[] {}; + } + + @RolesAllowed("${first-role}, ${second-role: defaultValue}, ${third-role}") + public int[] secured5() { + // we expect 1 value as this is considered 1 role + return new int[] { 0 }; + } + } + +} diff --git a/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/RolesAllowedExpressionFailureTest.java b/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/RolesAllowedExpressionFailureTest.java new file mode 100644 index 0000000000000..2a7d3b83a7357 --- /dev/null +++ b/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/RolesAllowedExpressionFailureTest.java @@ -0,0 +1,57 @@ +package io.quarkus.security.test.rolesallowed; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.NoSuchElementException; + +import javax.annotation.security.RolesAllowed; +import javax.inject.Singleton; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.test.utils.AuthData; +import io.quarkus.security.test.utils.IdentityMock; +import io.quarkus.security.test.utils.SecurityTestUtils; +import io.quarkus.test.QuarkusUnitTest; + +public class RolesAllowedExpressionFailureTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(RolesAllowedBean.class, IdentityMock.class, + AuthData.class, SecurityTestUtils.class)) + .assertException(t -> { + Throwable e = t; + NoSuchElementException te = null; + while (e != null) { + if (e instanceof NoSuchElementException) { + te = (NoSuchElementException) e; + break; + } + e = e.getCause(); + } + assertNotNull(te); + // assert + assertTrue(te.getMessage().contains("Could not expand value admin"), te.getMessage()); + }); + + @Test + public void test() { + Assertions.fail(); + } + + @Singleton + public static class RolesAllowedBean { + + @RolesAllowed("${admin}") + public final String admin() { + return "accessibleForAdminOnly"; + } + + } + +} diff --git a/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/RolesAllowedExpressionTest.java b/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/RolesAllowedExpressionTest.java new file mode 100644 index 0000000000000..055c5f5cb79f3 --- /dev/null +++ b/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/RolesAllowedExpressionTest.java @@ -0,0 +1,127 @@ +package io.quarkus.security.test.rolesallowed; + +import static io.quarkus.security.test.utils.IdentityMock.ADMIN; +import static io.quarkus.security.test.utils.IdentityMock.USER; +import static io.quarkus.security.test.utils.SecurityTestUtils.assertFailureFor; +import static io.quarkus.security.test.utils.SecurityTestUtils.assertSuccess; + +import java.util.Collections; +import java.util.Set; + +import javax.annotation.security.RolesAllowed; +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.test.utils.AuthData; +import io.quarkus.security.test.utils.IdentityMock; +import io.quarkus.security.test.utils.SecurityTestUtils; +import io.quarkus.test.QuarkusUnitTest; + +public class RolesAllowedExpressionTest { + + private static final String APP_PROPS = "" + + "sudo=admin\n" + + "sec-group.user-part-one=user\n" + + "sec-group.user-part-two=e\n" + + "su=ad\n" + + "do=min\n" + + "spaces.s=s\n" + + "spaces.p=p\n" + + "spaces.a=a\n" + + "spaces.c=c\n" + + "multiple-roles-grp.1st=multiple-roles.1st\n" + + "multiple-roles-grp.3rd=multiple-roles.3rd\n" + + "test-profile-admin=batman\n" + + "%test.test-profile-admin=admin\n" + + "missing-profile-profile-admin=superman\n" + + "%missing-profile.missing-profile-profile-admin=admin\n"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(RolesAllowedBean.class, IdentityMock.class, + AuthData.class, SecurityTestUtils.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); + + @Inject + RolesAllowedBean bean; + + @Test + public void shouldRestrictAccessToSpecificRole() { + // resolve role from 'sudo' config property + assertSuccess(() -> bean.admin(), "accessibleForAdminOnly", ADMIN); + // use default value 'user' as config property 'kudos' is missing + assertSuccess(() -> bean.user(), "accessibleForUserOnly", USER); + // composed expression + assertSuccess(() -> bean.user2(), "accessibleForUserOnly2", USER); + // multiple expressions + assertSuccess(() -> bean.admin2(), "accessibleForAdminOnly2", ADMIN); + // spaces + assertSuccess(() -> bean.spaces(), "accessibleForSpacesOnly", + new AuthData(Collections.singleton("s p a c e s"), false, "spaces")); + // secured method allow multiple roles + assertSuccess(() -> bean.multipleRoles(), "accessibleForMultipleRoles", + new AuthData(Set.of("multiple-roles.1st"), false, "multiple-roles")); + assertSuccess(() -> bean.multipleRoles(), "accessibleForMultipleRoles", + new AuthData(Set.of("multiple-roles.2nd"), false, "multiple-roles")); + assertSuccess(() -> bean.multipleRoles(), "accessibleForMultipleRoles", + new AuthData(Set.of("multiple-roles.3rd"), false, "multiple-roles")); + assertSuccess(() -> bean.multipleRoles(), "accessibleForMultipleRoles", + new AuthData(Set.of("multiple-roles.4th"), false, "multiple-roles")); + // test profile + assertSuccess(() -> bean.testProfile(), "accessibleForTestProfileAdmin", ADMIN); + assertFailureFor(() -> bean.missingTestProfile(), ForbiddenException.class, ADMIN); + } + + @Singleton + public static class RolesAllowedBean { + + @RolesAllowed("${sudo}") + public final String admin() { + return "accessibleForAdminOnly"; + } + + @RolesAllowed("${su}${do}") + public final String admin2() { + return "accessibleForAdminOnly2"; + } + + @RolesAllowed("${kudos:user}") + public final String user() { + return "accessibleForUserOnly"; + } + + @RolesAllowed("${sec-group.user-part-on${sec-group.user-part-two}}") + public final String user2() { + return "accessibleForUserOnly2"; + } + + @RolesAllowed("${spaces.s} ${spaces.p} ${spaces.a} ${spaces.c} e s") + public final String spaces() { + return "accessibleForSpacesOnly"; + } + + @RolesAllowed({ "${multiple-roles-grp.1st}", "${multiple-roles-grp.2nd:multiple-roles.2nd}", + "${multiple-roles-grp.3rd}", "multiple-roles.4th" }) + public final String multipleRoles() { + return "accessibleForMultipleRoles"; + } + + @RolesAllowed("${test-profile-admin}") + public final String testProfile() { + return "accessibleForTestProfileAdmin"; + } + + @RolesAllowed("${missing-profile-profile-admin}") + public final void missingTestProfile() { + // should throw exception + } + + } + +} diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusSecurityRolesAllowedConfigBuilder.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusSecurityRolesAllowedConfigBuilder.java new file mode 100644 index 0000000000000..d79aab1fa6da0 --- /dev/null +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusSecurityRolesAllowedConfigBuilder.java @@ -0,0 +1,47 @@ +package io.quarkus.security.runtime; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.quarkus.runtime.configuration.ConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilder; + +public final class QuarkusSecurityRolesAllowedConfigBuilder implements ConfigBuilder { + + private static final String ROLES_ALLOWED_CONFIG_SOURCE = "QuarkusSecurityRolesAllowedConfigSource"; + private static final Map properties = new HashMap<>(); + private final ConfigSource configSource = new ConfigSource() { + @Override + public Set getPropertyNames() { + return properties.keySet(); + } + + @Override + public String getValue(String key) { + return properties.get(key); + } + + @Override + public String getName() { + return ROLES_ALLOWED_CONFIG_SOURCE; + } + }; + + @Override + public SmallRyeConfigBuilder configBuilder(SmallRyeConfigBuilder builder) { + builder.getSources().add(configSource); + return builder; + } + + static void addProperty(int key, String value) { + // this method should be called during static init + properties.put(transformToKey(key), value); + } + + static String transformToKey(int i) { + return ROLES_ALLOWED_CONFIG_SOURCE + ".property-" + i; + } +} diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java index 4d3b4e0d23e77..0e26042dc6dbf 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java @@ -1,5 +1,15 @@ package io.quarkus.security.runtime; +import static io.quarkus.security.runtime.QuarkusSecurityRolesAllowedConfigBuilder.transformToKey; + +import java.util.Arrays; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; + import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.security.runtime.interceptor.SecurityCheckStorageBuilder; @@ -7,13 +17,16 @@ import io.quarkus.security.runtime.interceptor.check.DenyAllCheck; import io.quarkus.security.runtime.interceptor.check.PermitAllCheck; import io.quarkus.security.runtime.interceptor.check.RolesAllowedCheck; +import io.quarkus.security.runtime.interceptor.check.SupplierRolesAllowedCheck; import io.quarkus.security.spi.runtime.SecurityCheck; import io.quarkus.security.spi.runtime.SecurityCheckStorage; +import io.smallrye.config.Expressions; @Recorder public class SecurityCheckRecorder { private static volatile SecurityCheckStorage storage; + private static final Set configExpRolesAllowedChecks = ConcurrentHashMap.newKeySet(); public static SecurityCheckStorage getStorage() { return storage; @@ -31,6 +44,42 @@ public SecurityCheck rolesAllowed(String... roles) { return RolesAllowedCheck.of(roles); } + public SecurityCheck rolesAllowedSupplier(String[] allowedRoles, int[] configExpIndexes, int[] configKeys) { + + // here we add generated keys and values with the property expressions to the config source, + // the config source will be registered with the Config system, + // and we get all features available from Config + for (int i = 0; i < configExpIndexes.length; i++) { + QuarkusSecurityRolesAllowedConfigBuilder.addProperty(configKeys[i], allowedRoles[configExpIndexes[i]]); + } + + final var check = new SupplierRolesAllowedCheck( + resolveRolesAllowedConfigExp(allowedRoles, configExpIndexes, configKeys)); + configExpRolesAllowedChecks.add(check); + return check; + } + + private static Supplier resolveRolesAllowedConfigExp(String[] allowedRoles, int[] configExpIndexes, + int[] configKeys) { + + final String[] roles = Arrays.copyOf(allowedRoles, allowedRoles.length); + return new Supplier() { + @Override + public String[] get() { + final var config = ConfigProviderResolver.instance().getConfig(Thread.currentThread().getContextClassLoader()); + if (config.getOptionalValue(Config.PROPERTY_EXPRESSIONS_ENABLED, Boolean.class).orElse(Boolean.TRUE) + && Expressions.isEnabled()) { + // property expressions are enabled + for (int i = 0; i < configExpIndexes.length; i++) { + // resolve configuration expressions specified as value of the @RolesAllowed annotation + roles[configExpIndexes[i]] = config.getValue(transformToKey(configKeys[i]), String.class); + } + } + return roles; + } + }; + } + public SecurityCheck authenticated() { return AuthenticatedCheck.INSTANCE; } @@ -50,4 +99,12 @@ public void create(RuntimeValue builder) { storage = builder.getValue().create(); } + public void resolveRolesAllowedConfigExpRoles() { + if (!configExpRolesAllowedChecks.isEmpty()) { + for (SupplierRolesAllowedCheck configExpRolesAllowedCheck : configExpRolesAllowedChecks) { + configExpRolesAllowedCheck.resolveAllowedRoles(); + } + configExpRolesAllowedChecks.clear(); + } + } } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/SupplierRolesAllowedCheck.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/SupplierRolesAllowedCheck.java index 5ec82dc3cd65f..f7550a570b911 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/SupplierRolesAllowedCheck.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/SupplierRolesAllowedCheck.java @@ -37,4 +37,10 @@ private void doApply(SecurityIdentity identity) { } RolesAllowedCheck.doApply(identity, allowedRoles); } + + public void resolveAllowedRoles() { + if (allowedRoles == null) { + allowedRoles = allowedRolesSupplier.get(); + } + } } diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java index e6e11bc9419e8..0ac3da55c1ebd 100644 --- a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java +++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java @@ -71,6 +71,11 @@ import io.smallrye.graphql.api.AdaptWith; import io.smallrye.graphql.api.Entry; import io.smallrye.graphql.api.ErrorExtensionProvider; +import io.smallrye.graphql.api.federation.Extends; +import io.smallrye.graphql.api.federation.External; +import io.smallrye.graphql.api.federation.Key; +import io.smallrye.graphql.api.federation.Provides; +import io.smallrye.graphql.api.federation.Requires; import io.smallrye.graphql.cdi.config.ConfigKey; import io.smallrye.graphql.cdi.config.MicroProfileConfig; import io.smallrye.graphql.cdi.producer.GraphQLProducer; @@ -244,6 +249,11 @@ void buildFinalIndex( try { indexer.indexClass(Map.class); indexer.indexClass(Entry.class); + indexer.indexClass(Extends.class); + indexer.indexClass(External.class); + indexer.indexClass(Key.class); + indexer.indexClass(Provides.class); + indexer.indexClass(Requires.class); } catch (IOException ex) { LOG.warn("Failure while creating index", ex); } diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLFederationTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLFederationTest.java new file mode 100644 index 0000000000000..587f597fea5a8 --- /dev/null +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLFederationTest.java @@ -0,0 +1,59 @@ +package io.quarkus.smallrye.graphql.deployment; + +import static org.hamcrest.Matchers.containsString; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.smallrye.graphql.api.federation.Extends; + +public class GraphQLFederationTest extends AbstractGraphQLTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(FooApi.class, Foo.class) + .addAsResource(new StringAsset("quarkus.smallrye-graphql.schema-include-directives=true"), + "application.properties") + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); + + @Test + public void checkServiceDeclarationInSchema() { + RestAssured.given() + .get("/graphql/schema.graphql") + .then() + .body(containsString("type _Service {")); + } + + @Test + public void checkFederationDirectivesInSchema() { + RestAssured.given() + .get("/graphql/schema.graphql") + .then() + .body(containsString("name: String @extends")); + } + + @GraphQLApi + static class FooApi { + + @Query + public Foo foo() { + return new Foo(); + } + + } + + static class Foo { + + @Extends + public String name; + + } + +} diff --git a/extensions/smallrye-jwt-build/runtime/src/main/java/io/quarkus/smallrye/jwt/build/runtime/graalvm/Substitutions.java b/extensions/smallrye-jwt-build/runtime/src/main/java/io/quarkus/smallrye/jwt/build/runtime/graalvm/Substitutions.java new file mode 100644 index 0000000000000..4d09817c95dc1 --- /dev/null +++ b/extensions/smallrye-jwt-build/runtime/src/main/java/io/quarkus/smallrye/jwt/build/runtime/graalvm/Substitutions.java @@ -0,0 +1,29 @@ +package io.quarkus.smallrye.jwt.build.runtime.graalvm; + +import java.util.function.BooleanSupplier; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "org.jose4j.jwk.OctetKeyPairJsonWebKey", onlyWith = JavaVersionLessThan17.class) +final class Target_org_jose4j_jwk_OctetKeyPairJsonWebKey { + @Substitute + public Target_org_jose4j_jwk_OctetKeyPairJsonWebKey(java.security.PublicKey publicKey) { + } + + @Substitute + Target_org_jose4j_jwk_OctetKeyPairUtil subtypeKeyUtil() { + return null; + } +} + +@TargetClass(className = "org.jose4j.keys.OctetKeyPairUtil", onlyWith = JavaVersionLessThan17.class) +final class Target_org_jose4j_jwk_OctetKeyPairUtil { +} + +class JavaVersionLessThan17 implements BooleanSupplier { + @Override + public boolean getAsBoolean() { + return Runtime.version().version().get(0) < 17; + } +} diff --git a/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/EnabledProactiveAuthFailedExceptionMapperTest.java b/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/EnabledProactiveAuthFailedExceptionMapperTest.java new file mode 100644 index 0000000000000..ebd85a97539b8 --- /dev/null +++ b/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/EnabledProactiveAuthFailedExceptionMapperTest.java @@ -0,0 +1,47 @@ +package io.quarkus.jwt.test; + +import javax.ws.rs.core.Response; + +import org.hamcrest.Matchers; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class EnabledProactiveAuthFailedExceptionMapperTest { + + private static final String CUSTOMIZED_RESPONSE = "AuthenticationFailedException"; + protected static final Class[] classes = { JsonValuejectionEndpoint.class, TokenUtils.class, + AuthFailedExceptionMapper.class }; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(classes) + .addAsResource(new StringAsset("quarkus.http.auth.proactive=true\n"), "application.properties")); + + @Test + public void testExMapperCustomizedResponse() { + RestAssured + .given() + .auth().oauth2("absolute-nonsense") + .get("/endp/verifyInjectedIssuer").then() + .statusCode(401) + .body(Matchers.equalTo(CUSTOMIZED_RESPONSE)); + } + + public static class AuthFailedExceptionMapper { + + @ServerExceptionMapper(value = AuthenticationFailedException.class) + public Response unauthorized() { + return Response + .status(401) + .entity(CUSTOMIZED_RESPONSE).build(); + } + + } +} \ No newline at end of file diff --git a/extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/SmallRyeMetricsRecorder.java b/extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/SmallRyeMetricsRecorder.java index 08966f4712840..840be46047a59 100644 --- a/extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/SmallRyeMetricsRecorder.java +++ b/extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/SmallRyeMetricsRecorder.java @@ -28,10 +28,10 @@ import org.eclipse.microprofile.metrics.MetricUnits; import org.eclipse.microprofile.metrics.Tag; import org.eclipse.microprofile.metrics.Timer; -import org.graalvm.nativeimage.ImageInfo; import org.jboss.logging.Logger; import io.quarkus.arc.runtime.BeanContainer; +import io.quarkus.runtime.ImageMode; import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.metrics.MetricsFactory; @@ -328,7 +328,7 @@ public Number getValue() { // some metrics are only available in jdk internal class 'com.sun.management.OperatingSystemMXBean': cast to it. // com.sun.management.OperatingSystemMXBean is not available in SubstratVM // the cast will fail for some JVM not derived from HotSpot (J9 for example) so we check if it is assignable to it - if (!ImageInfo.inImageCode() + if (ImageMode.current() == ImageMode.JVM && com.sun.management.OperatingSystemMXBean.class.isAssignableFrom(operatingSystemMXBean.getClass())) { try { com.sun.management.OperatingSystemMXBean internalOperatingSystemMXBean = (com.sun.management.OperatingSystemMXBean) operatingSystemMXBean; @@ -367,7 +367,7 @@ private void vendorOperatingSystemMetrics(MetricRegistry registry) { // some metrics are only available in jdk internal class 'com.sun.management.OperatingSystemMXBean': cast to it. // com.sun.management.OperatingSystemMXBean is not available in SubstratVM // the cast will fail for some JVM not derived from HotSpot (J9 for example) so we check if it is assignable to it - if (!ImageInfo.inImageCode() + if (ImageMode.current() == ImageMode.JVM && com.sun.management.OperatingSystemMXBean.class.isAssignableFrom(operatingSystemMXBean.getClass())) { try { com.sun.management.OperatingSystemMXBean internalOperatingSystemMXBean = (com.sun.management.OperatingSystemMXBean) operatingSystemMXBean; @@ -608,7 +608,7 @@ public Number getValue() { private void memoryPoolMetrics(MetricRegistry registry) { // MemoryPoolMXBean doesn't work in native mode - if (!ImageInfo.inImageCode()) { + if (ImageMode.current() == ImageMode.JVM) { List mps = ManagementFactory.getMemoryPoolMXBeans(); Metadata usageMetadata = Metadata.builder() .withName("memoryPool.usage") @@ -665,7 +665,7 @@ public Number getValue() { } private void micrometerJvmGcMetrics(MetricRegistry registry, ShutdownContext shutdownContext) { - if (!ImageInfo.inImageCode()) { + if (ImageMode.current() == ImageMode.JVM) { MicrometerGCMetrics gcMetrics = new MicrometerGCMetrics(); registry.register(new ExtendedMetadataBuilder() @@ -815,7 +815,7 @@ public Number getValue() { } }); - if (!ImageInfo.inImageCode()) { + if (ImageMode.current() == ImageMode.JVM) { ExtendedMetadata threadStatesMetadata = new ExtendedMetadataBuilder() .withName("jvm.threads.states") .withType(MetricType.GAUGE) @@ -837,7 +837,7 @@ public Number getValue() { } private void micrometerJvmMemoryMetrics(MetricRegistry registry) { - if (!ImageInfo.inImageCode()) { + if (ImageMode.current() == ImageMode.JVM) { for (MemoryPoolMXBean memoryPoolMXBean : ManagementFactory.getMemoryPoolMXBeans()) { String area = MemoryType.HEAP.equals(memoryPoolMXBean.getType()) ? "heap" : "nonheap"; Tag[] tags = new Tag[] { new Tag("id", memoryPoolMXBean.getName()), @@ -951,7 +951,7 @@ public Number getValue() { private void micrometerJvmClassLoaderMetrics(MetricRegistry registry) { // The ClassLoadingMXBean can be used in native mode, but it only returns zeroes, so there's no point in including such metrics. - if (!ImageInfo.inImageCode()) { + if (ImageMode.current() == ImageMode.JVM) { ClassLoadingMXBean classLoadingBean = ManagementFactory.getClassLoadingMXBean(); registry.register( diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiHttpRootDefaultPathTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiHttpRootDefaultPathTestCase.java index 77c31d6bcb0a3..7d57df969f1ca 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiHttpRootDefaultPathTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiHttpRootDefaultPathTestCase.java @@ -16,7 +16,8 @@ public class OpenApiHttpRootDefaultPathTestCase { .withApplicationRoot((jar) -> jar .addClasses(OpenApiRoute.class) .addAsResource(new StringAsset("quarkus.http.root-path=/foo\n" + - "quarkus.http.cors=true"), "application.properties")); + "quarkus.http.cors=true\n" + + "quarkus.http.cors.origins=*"), "application.properties")); @Test public void testOpenApiPathAccessResource() { diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiHttpRootPathCorsTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiHttpRootPathCorsTestCase.java index f8f22caa5a418..b8ee94e44d66d 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiHttpRootPathCorsTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiHttpRootPathCorsTestCase.java @@ -15,6 +15,7 @@ public class OpenApiHttpRootPathCorsTestCase { .withApplicationRoot((jar) -> jar .addClasses(OpenApiRoute.class) .addAsResource(new StringAsset("quarkus.http.cors=true\n" + + "quarkus.http.cors.origins=*\n" + "quarkus.http.non-application-root-path=/api/q\n" + "quarkus.http.root-path=/api"), "application.properties")); diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java index 6803bd135f0a2..898774bf6d92e 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java @@ -1,9 +1,14 @@ package io.quarkus.smallrye.reactivemessaging.kafka.deployment; +import static io.quarkus.smallrye.reactivemessaging.kafka.HibernateOrmStateStore.HIBERNATE_ORM_STATE_STORE; +import static io.quarkus.smallrye.reactivemessaging.kafka.HibernateReactiveStateStore.HIBERNATE_REACTIVE_STATE_STORE; +import static io.quarkus.smallrye.reactivemessaging.kafka.RedisStateStore.REDIS_STATE_STORE; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Function; @@ -41,12 +46,16 @@ import io.quarkus.smallrye.reactivemessaging.kafka.ReactiveMessagingKafkaConfig; import io.quarkus.smallrye.reactivemessaging.kafka.RedisStateStore; import io.smallrye.mutiny.tuples.Functions.TriConsumer; +import io.smallrye.reactive.messaging.kafka.KafkaConnector; import io.vertx.kafka.client.consumer.impl.KafkaReadStreamImpl; public class SmallRyeReactiveMessagingKafkaProcessor { private static final Logger LOGGER = Logger.getLogger("io.quarkus.smallrye-reactive-messaging-kafka.deployment.processor"); + public static final String CHECKPOINT_STATE_STORE_MESSAGE = "Quarkus detected the use of `%s` for the" + + " Kafka checkpoint commit strategy but the extension has not been added. Consider adding '%s'."; + @BuildStep FeatureBuildItem feature() { return new FeatureBuildItem(Feature.SMALLRYE_REACTIVE_MESSAGING_KAFKA); @@ -74,25 +83,61 @@ public void ignoreDuplicateJmxRegistrationInDevAndTestModes(LaunchModeBuildItem } } + static boolean hasStateStoreConfig(String stateStoreName, Config config) { + Optional connectorStrategy = getConnectorProperty("checkpoint.state-store", config); + if (connectorStrategy.isPresent() && connectorStrategy.get().equals(stateStoreName)) { + return true; + } + List stateStores = getChannelProperties("checkpoint.state-store", config); + return stateStores.contains(stateStoreName); + } + + private static Optional getConnectorProperty(String keySuffix, Config config) { + return config.getOptionalValue("mp.messaging.connector." + KafkaConnector.CONNECTOR_NAME + "." + keySuffix, + String.class); + } + + private static List getChannelProperties(String keySuffix, Config config) { + List values = new ArrayList<>(); + for (String propertyName : config.getPropertyNames()) { + if (propertyName.startsWith("mp.messaging.incoming.") && propertyName.endsWith("." + keySuffix)) { + values.add(config.getValue(propertyName, String.class)); + } + } + return values; + } + @BuildStep public void checkpointRedis(BuildProducer additionalBean, Capabilities capabilities) { - if (capabilities.isPresent(Capability.REDIS_CLIENT)) { - additionalBean.produce(new AdditionalBeanBuildItem(RedisStateStore.Factory.class)); - additionalBean.produce(new AdditionalBeanBuildItem(DatabindProcessingStateCodec.Factory.class)); + if (hasStateStoreConfig(REDIS_STATE_STORE, ConfigProvider.getConfig())) { + if (capabilities.isPresent(Capability.REDIS_CLIENT)) { + additionalBean.produce(new AdditionalBeanBuildItem(RedisStateStore.Factory.class)); + additionalBean.produce(new AdditionalBeanBuildItem(DatabindProcessingStateCodec.Factory.class)); + } else { + LOGGER.warnf(CHECKPOINT_STATE_STORE_MESSAGE, REDIS_STATE_STORE, "quarkus-redis-client"); + } } } @BuildStep public void checkpointHibernateReactive(BuildProducer additionalBean, Capabilities capabilities) { - if (capabilities.isPresent(Capability.HIBERNATE_REACTIVE)) { - additionalBean.produce(new AdditionalBeanBuildItem(HibernateReactiveStateStore.Factory.class)); + if (hasStateStoreConfig(HIBERNATE_REACTIVE_STATE_STORE, ConfigProvider.getConfig())) { + if (capabilities.isPresent(Capability.HIBERNATE_REACTIVE)) { + additionalBean.produce(new AdditionalBeanBuildItem(HibernateReactiveStateStore.Factory.class)); + } else { + LOGGER.warnf(CHECKPOINT_STATE_STORE_MESSAGE, HIBERNATE_REACTIVE_STATE_STORE, "quarkus-hibernate-reactive"); + } } } @BuildStep public void checkpointHibernateOrm(BuildProducer additionalBean, Capabilities capabilities) { - if (capabilities.isPresent(Capability.HIBERNATE_ORM)) { - additionalBean.produce(new AdditionalBeanBuildItem(HibernateOrmStateStore.Factory.class)); + if (hasStateStoreConfig(HIBERNATE_ORM_STATE_STORE, ConfigProvider.getConfig())) { + if (capabilities.isPresent(Capability.HIBERNATE_ORM)) { + additionalBean.produce(new AdditionalBeanBuildItem(HibernateOrmStateStore.Factory.class)); + } else { + LOGGER.warnf(CHECKPOINT_STATE_STORE_MESSAGE, HIBERNATE_ORM_STATE_STORE, "quarkus-hibernate-orm"); + } } } diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/CheckpointStateStoreConfigTest.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/CheckpointStateStoreConfigTest.java new file mode 100644 index 0000000000000..03167b1e4edcb --- /dev/null +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/CheckpointStateStoreConfigTest.java @@ -0,0 +1,64 @@ +package io.quarkus.smallrye.reactivemessaging.kafka.deployment; + +import static io.quarkus.smallrye.reactivemessaging.kafka.HibernateOrmStateStore.HIBERNATE_ORM_STATE_STORE; +import static io.quarkus.smallrye.reactivemessaging.kafka.HibernateReactiveStateStore.HIBERNATE_REACTIVE_STATE_STORE; +import static io.quarkus.smallrye.reactivemessaging.kafka.RedisStateStore.REDIS_STATE_STORE; +import static io.quarkus.smallrye.reactivemessaging.kafka.deployment.SmallRyeReactiveMessagingKafkaProcessor.hasStateStoreConfig; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; + +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.common.MapBackedConfigSource; + +public class CheckpointStateStoreConfigTest { + + SmallRyeConfig config; + + @AfterEach + void tearDown() { + if (config != null) { + ConfigProviderResolver.instance().releaseConfig(config); + } + } + + private void createConfig(Map configMap) { + config = new SmallRyeConfigBuilder() + .withSources(new MapBackedConfigSource("test", configMap) { + }) + .build(); + } + + @Test + void testHasStateStoreConfigWithConnectorConfig() { + createConfig(Map.of("mp.messaging.connector.smallrye-kafka.checkpoint.state-store", HIBERNATE_ORM_STATE_STORE)); + assertTrue(hasStateStoreConfig(HIBERNATE_ORM_STATE_STORE, config)); + } + + @Test + void testHasStateStoreConfigWithChannelConfig() { + createConfig(Map.of("mp.messaging.incoming.my-channel.checkpoint.state-store", HIBERNATE_REACTIVE_STATE_STORE)); + assertTrue(hasStateStoreConfig(HIBERNATE_REACTIVE_STATE_STORE, config)); + } + + @Test + void testHasStateStoreConfigWithInvalidChannelConfig() { + createConfig(Map.of( + "mp.messaging.outgoing.my-channel.checkpoint.state-store", HIBERNATE_REACTIVE_STATE_STORE, + "mp.messaging.incoming.my-channel.state-store", HIBERNATE_ORM_STATE_STORE)); + assertFalse(hasStateStoreConfig(HIBERNATE_REACTIVE_STATE_STORE, config)); + assertFalse(hasStateStoreConfig(HIBERNATE_ORM_STATE_STORE, config)); + } + + @Test + void testHasStateStoreConfigEmptyConfig() { + createConfig(Map.of()); + assertFalse(hasStateStoreConfig(REDIS_STATE_STORE, config)); + } +} diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/KafkaDevServicesDevModeTestCase.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/KafkaDevServicesDevModeTestCase.java index 887a01b0f04e5..869d81a53c702 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/KafkaDevServicesDevModeTestCase.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/dev/KafkaDevServicesDevModeTestCase.java @@ -1,7 +1,6 @@ package io.quarkus.smallrye.reactivemessaging.kafka.deployment.dev; import java.net.URI; -import java.time.Duration; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Supplier; @@ -58,7 +57,6 @@ public void sseStream() { source.open(); Awaitility.await() - .atMost(Duration.ofSeconds(1)) .until(() -> received.size() >= 2); } @@ -77,7 +75,6 @@ public void sseStream() { source.open(); Awaitility.await() - .atMost(Duration.ofSeconds(3)) .until(() -> received.size() >= 2); } diff --git a/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateOrmStateStore.java b/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateOrmStateStore.java index b4f8637ea0a76..dacadc1645387 100644 --- a/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateOrmStateStore.java +++ b/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateOrmStateStore.java @@ -29,7 +29,7 @@ public class HibernateOrmStateStore implements CheckpointStateStore { - public static final String QUARKUS_HIBERNATE_ORM = "quarkus-hibernate-orm"; + public static final String HIBERNATE_ORM_STATE_STORE = "quarkus-hibernate-orm"; private final String consumerGroupId; private final SessionFactory sf; private final Class stateType; @@ -42,7 +42,7 @@ public HibernateOrmStateStore(String consumerGroupId, SessionFactory sf, } @ApplicationScoped - @Identifier(QUARKUS_HIBERNATE_ORM) + @Identifier(HIBERNATE_ORM_STATE_STORE) public static class Factory implements CheckpointStateStore.Factory { @Inject @@ -57,7 +57,7 @@ public CheckpointStateStore create(KafkaConnectorIncomingConfiguration config, V throw new IllegalArgumentException("State type needs to extend `CheckpointEntity`"); } String persistenceUnit = config.config().getOptionalValue(KafkaCommitHandler.Strategy.CHECKPOINT + "." + - QUARKUS_HIBERNATE_ORM + ".persistence-unit", String.class) + HIBERNATE_ORM_STATE_STORE + ".persistence-unit", String.class) .orElse(null); SessionFactory sf = persistenceUnit != null ? sessionFactories.select(new PersistenceUnit.PersistenceUnitLiteral(persistenceUnit)).get() diff --git a/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateReactiveStateStore.java b/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateReactiveStateStore.java index 94fed9eb48e27..811b29bb10115 100644 --- a/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateReactiveStateStore.java +++ b/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateReactiveStateStore.java @@ -25,6 +25,7 @@ public class HibernateReactiveStateStore implements CheckpointStateStore { + public static final String HIBERNATE_REACTIVE_STATE_STORE = "quarkus-hibernate-reactive"; private final String consumerGroupId; private final Mutiny.SessionFactory sf; private final Class stateType; @@ -37,7 +38,7 @@ public HibernateReactiveStateStore(String consumerGroupId, Mutiny.SessionFactory } @ApplicationScoped - @Identifier("quarkus-hibernate-reactive") + @Identifier(HIBERNATE_REACTIVE_STATE_STORE) public static class Factory implements CheckpointStateStore.Factory { @Inject diff --git a/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/RedisStateStore.java b/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/RedisStateStore.java index d73cbcbfc4a1b..b606a2e14a67b 100644 --- a/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/RedisStateStore.java +++ b/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/RedisStateStore.java @@ -33,7 +33,7 @@ public class RedisStateStore implements CheckpointStateStore { - public static final String REDIS_CHECKPOINT_NAME = "quarkus-redis"; + public static final String REDIS_STATE_STORE = "quarkus-redis"; private final ReactiveRedisDataSource redis; private final String consumerGroupId; @@ -47,7 +47,7 @@ public RedisStateStore(ReactiveRedisDataSource redis, String consumerGroupId, Pr } @ApplicationScoped - @Identifier(REDIS_CHECKPOINT_NAME) + @Identifier(REDIS_STATE_STORE) public static class Factory implements CheckpointStateStore.Factory { @Inject @@ -62,7 +62,7 @@ public CheckpointStateStore create(KafkaConnectorIncomingConfiguration config, V KafkaConsumer consumer, Class stateType) { String consumerGroupId = (String) consumer.configuration().get(ConsumerConfig.GROUP_ID_CONFIG); String clientName = config.config().getOptionalValue(KafkaCommitHandler.Strategy.CHECKPOINT + "." + - REDIS_CHECKPOINT_NAME + ".client-name", String.class) + REDIS_STATE_STORE + ".client-name", String.class) .orElse(null); ReactiveRedisDataSource rds = clientName != null ? redisDataSource.select(RedisClientName.Literal.of(clientName)).get() diff --git a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/ReactiveMessagingDotNames.java b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/ReactiveMessagingDotNames.java index 19c664f2c09be..19dc9c4b664eb 100644 --- a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/ReactiveMessagingDotNames.java +++ b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/ReactiveMessagingDotNames.java @@ -21,6 +21,8 @@ import io.smallrye.reactive.messaging.annotations.Incomings; import io.smallrye.reactive.messaging.annotations.Merge; import io.smallrye.reactive.messaging.annotations.OnOverflow; +import io.smallrye.reactive.messaging.connector.InboundConnector; +import io.smallrye.reactive.messaging.connector.OutboundConnector; public final class ReactiveMessagingDotNames { @@ -55,6 +57,9 @@ public final class ReactiveMessagingDotNames { static final DotName INCOMING_CONNECTOR_FACTORY = DotName.createSimple(IncomingConnectorFactory.class.getName()); static final DotName OUTGOING_CONNECTOR_FACTORY = DotName.createSimple(OutgoingConnectorFactory.class.getName()); + static final DotName INBOUND_CONNECTOR = DotName.createSimple(InboundConnector.class.getName()); + static final DotName OUTBOUND_CONNECTOR = DotName.createSimple(OutboundConnector.class.getName()); + static final DotName SMALLRYE_BLOCKING = DotName.createSimple(io.smallrye.common.annotation.Blocking.class.getName()); // Do not directly reference the MetricDecorator (due to its direct references to MP Metrics, which may not be present) diff --git a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java index 67495210a2f63..c329c7f8f7891 100644 --- a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java +++ b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java @@ -428,7 +428,9 @@ public boolean appliesTo(AnnotationTarget.Kind kind) { public void transform(TransformationContext ctx) { ClassInfo clazz = ctx.getTarget().asClass(); if (doesImplement(clazz, ReactiveMessagingDotNames.INCOMING_CONNECTOR_FACTORY, index.getIndex()) - || doesImplement(clazz, ReactiveMessagingDotNames.OUTGOING_CONNECTOR_FACTORY, index.getIndex())) { + || doesImplement(clazz, ReactiveMessagingDotNames.INBOUND_CONNECTOR, index.getIndex()) + || doesImplement(clazz, ReactiveMessagingDotNames.OUTGOING_CONNECTOR_FACTORY, index.getIndex()) + || doesImplement(clazz, ReactiveMessagingDotNames.OUTBOUND_CONNECTOR, index.getIndex())) { ctx.transform().add(DevModeSupportConnectorFactory.class).done(); } } @@ -505,7 +507,8 @@ public boolean appliesTo(AnnotationTarget.Kind kind) { @Override public void transform(TransformationContext ctx) { ClassInfo clazz = ctx.getTarget().asClass(); - if (doesImplement(clazz, ReactiveMessagingDotNames.INCOMING_CONNECTOR_FACTORY, index.getIndex())) { + if (doesImplement(clazz, ReactiveMessagingDotNames.INCOMING_CONNECTOR_FACTORY, index.getIndex()) + || doesImplement(clazz, ReactiveMessagingDotNames.INBOUND_CONNECTOR, index.getIndex())) { ctx.transform().add(DuplicatedContextConnectorFactory.class).done(); } } diff --git a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/WiringHelper.java b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/WiringHelper.java index 47e744385dd41..c09bffb28ef62 100644 --- a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/WiringHelper.java +++ b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/WiringHelper.java @@ -110,8 +110,8 @@ static boolean isChannelEnabled(ChannelDirection direction, String channel) { * @return {@code true} if the class implements the inbound connector interface */ static boolean isInboundConnector(ClassInfo ci) { - // TODO Add the internal interface support - return ci.interfaceNames().contains(ReactiveMessagingDotNames.INCOMING_CONNECTOR_FACTORY); + return ci.interfaceNames().contains(ReactiveMessagingDotNames.INCOMING_CONNECTOR_FACTORY) + || ci.interfaceNames().contains(ReactiveMessagingDotNames.INBOUND_CONNECTOR); } /** @@ -121,8 +121,8 @@ static boolean isInboundConnector(ClassInfo ci) { * @return {@code true} if the class implements the outbound connector interface */ static boolean isOutboundConnector(ClassInfo ci) { - // TODO Add the internal interface support - return ci.interfaceNames().contains(ReactiveMessagingDotNames.OUTGOING_CONNECTOR_FACTORY); + return ci.interfaceNames().contains(ReactiveMessagingDotNames.OUTGOING_CONNECTOR_FACTORY) + || ci.interfaceNames().contains(ReactiveMessagingDotNames.OUTBOUND_CONNECTOR); } /** diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/DuplicatedContextConnectorFactoryInterceptor.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/DuplicatedContextConnectorFactoryInterceptor.java index daa1c4e4467ed..a0659ecad6333 100644 --- a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/DuplicatedContextConnectorFactoryInterceptor.java +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/DuplicatedContextConnectorFactoryInterceptor.java @@ -9,9 +9,11 @@ import org.eclipse.microprofile.reactive.messaging.Message; import org.eclipse.microprofile.reactive.streams.operators.PublisherBuilder; +import org.reactivestreams.Publisher; import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle; import io.smallrye.common.vertx.VertxContext; +import io.smallrye.mutiny.Multi; import io.smallrye.reactive.messaging.providers.locals.LocalContextMetadata; import io.vertx.core.Context; @@ -24,18 +26,25 @@ public class DuplicatedContextConnectorFactoryInterceptor { public Object intercept(InvocationContext ctx) throws Exception { if (ctx.getMethod().getName().equals("getPublisherBuilder")) { PublisherBuilder> result = (PublisherBuilder>) ctx.proceed(); - return result.map(message -> { - Optional metadata = message.getMetadata(LocalContextMetadata.class); - if (metadata.isPresent()) { - Context context = metadata.get().context(); - if (context != null && VertxContext.isDuplicatedContext(context)) { - VertxContextSafetyToggle.setContextSafe(context, true); - } - } - return message; - }); + return result.map(DuplicatedContextConnectorFactoryInterceptor::setMessageContextSafe); + } + if (ctx.getMethod().getName().equals("getPublisher")) { + Publisher> result = (Publisher>) ctx.proceed(); + return Multi.createFrom().publisher(result) + .map(DuplicatedContextConnectorFactoryInterceptor::setMessageContextSafe); } return ctx.proceed(); } + + private static Message setMessageContextSafe(Message message) { + Optional metadata = message.getMetadata(LocalContextMetadata.class); + if (metadata.isPresent()) { + Context context = metadata.get().context(); + if (context != null && VertxContext.isDuplicatedContext(context)) { + VertxContextSafetyToggle.setContextSafe(context, true); + } + } + return message; + } } diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devmode/DevModeSupportConnectorFactoryInterceptor.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devmode/DevModeSupportConnectorFactoryInterceptor.java index 0d5478919ee28..e89e6979e2a7a 100644 --- a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devmode/DevModeSupportConnectorFactoryInterceptor.java +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devmode/DevModeSupportConnectorFactoryInterceptor.java @@ -12,9 +12,13 @@ import org.eclipse.microprofile.reactive.streams.operators.PublisherBuilder; import org.eclipse.microprofile.reactive.streams.operators.ReactiveStreams; import org.eclipse.microprofile.reactive.streams.operators.SubscriberBuilder; +import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + @Interceptor @DevModeSupportConnectorFactory @Priority(Interceptor.Priority.PLATFORM_BEFORE + 10) @@ -45,6 +49,19 @@ public Object intercept(InvocationContext ctx) throws Exception { return future; }); } + if (ctx.getMethod().getName().equals("getPublisher")) { + Publisher> result = (Publisher>) ctx.proceed(); + return Multi.createFrom().publisher(result) + .onItem().transformToUniAndConcatenate(msg -> Uni.createFrom().emitter(e -> { + onMessage.get().whenComplete((restarted, error) -> { + if (!restarted) { + // if restarted, a new stream is already running, + // no point in emitting an event to the old stream + e.complete(msg); + } + }); + })); + } if (ctx.getMethod().getName().equals("getSubscriberBuilder")) { SubscriberBuilder, Void> result = (SubscriberBuilder, Void>) ctx.proceed(); @@ -76,6 +93,36 @@ public void onComplete() { } }); } + if (ctx.getMethod().getName().equals("getSubscriber")) { + Subscriber> result = (Subscriber>) ctx.proceed(); + return new Subscriber>() { + private Subscriber> subscriber; + + @Override + public void onSubscribe(Subscription s) { + subscriber = result; + subscriber.onSubscribe(s); + } + + @Override + public void onNext(Message o) { + subscriber.onNext(o); + onMessage.get(); + } + + @Override + public void onError(Throwable t) { + subscriber.onError(t); + onMessage.get(); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + onMessage.get(); + } + }; + } return ctx.proceed(); } diff --git a/extensions/spring-di/deployment/src/test/java/io/quarkus/spring/di/deployment/SpringDIProcessorTest.java b/extensions/spring-di/deployment/src/test/java/io/quarkus/spring/di/deployment/SpringDIProcessorTest.java index 5791891b3fca2..41e6975855dec 100644 --- a/extensions/spring-di/deployment/src/test/java/io/quarkus/spring/di/deployment/SpringDIProcessorTest.java +++ b/extensions/spring-di/deployment/src/test/java/io/quarkus/spring/di/deployment/SpringDIProcessorTest.java @@ -205,7 +205,8 @@ public void getAnnotationsToAddBeanMethodWithScope() { private IndexView getIndex(final Class... classes) { try { Index index = Index.of(classes); - return BeanArchives.buildBeanArchiveIndex(getClass().getClassLoader(), new ConcurrentHashMap<>(), index); + return BeanArchives.buildComputingBeanArchiveIndex(getClass().getClassLoader(), new ConcurrentHashMap<>(), + BeanArchives.buildImmutableBeanArchiveIndex(index)); } catch (IOException e) { throw new IllegalStateException("Failed to index classes", e); } diff --git a/extensions/spring-scheduled/deployment/src/test/java/io/quarkus/spring/scheduled/deployment/SpringScheduledProcessorTest.java b/extensions/spring-scheduled/deployment/src/test/java/io/quarkus/spring/scheduled/deployment/SpringScheduledProcessorTest.java index 3abb0f2f3334b..45db58171f526 100644 --- a/extensions/spring-scheduled/deployment/src/test/java/io/quarkus/spring/scheduled/deployment/SpringScheduledProcessorTest.java +++ b/extensions/spring-scheduled/deployment/src/test/java/io/quarkus/spring/scheduled/deployment/SpringScheduledProcessorTest.java @@ -115,7 +115,8 @@ public void testBuildDelayParamFromInvalidFormat() { private IndexView getIndex(final Class... classes) { try { Index index = Index.of(classes); - return BeanArchives.buildBeanArchiveIndex(getClass().getClassLoader(), new ConcurrentHashMap<>(), index); + return BeanArchives.buildComputingBeanArchiveIndex(getClass().getClassLoader(), new ConcurrentHashMap<>(), + BeanArchives.buildImmutableBeanArchiveIndex(index)); } catch (IOException e) { throw new IllegalStateException("Failed to index classes", e); } diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/HttpSessionContext.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/HttpSessionContext.java index da04a1be1d38a..25a5d54942d29 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/HttpSessionContext.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/HttpSessionContext.java @@ -16,6 +16,8 @@ import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; +import org.jboss.logging.Logger; + import io.quarkus.arc.Arc; import io.quarkus.arc.ContextInstanceHandle; import io.quarkus.arc.InjectableBean; @@ -31,6 +33,10 @@ public class HttpSessionContext implements InjectableContext, HttpSessionListene private static final String CONTEXTUAL_INSTANCES_KEY = HttpSessionContext.class.getName() + ".contextualInstances"; + private static final ThreadLocal DESTRUCT_SESSION = new ThreadLocal<>(); + + private static final Logger LOG = Logger.getLogger(HttpSessionContext.class); + @Override public Class getScope() { return SessionScoped.class; @@ -39,17 +45,16 @@ public Class getScope() { @SuppressWarnings("unchecked") @Override public T get(Contextual contextual, CreationalContext creationalContext) { - HttpServletRequest request = servletRequest(); - if (request == null) { + HttpSession session = session(true); + if (session == null) { throw new ContextNotActiveException(); } InjectableBean bean = (InjectableBean) contextual; - ComputingCache> contextualInstances = getContextualInstances(request); + ComputingCache> contextualInstances = getContextualInstances(session); if (creationalContext != null) { return (T) contextualInstances.getValue(new Key(creationalContext, bean.getIdentifier())).get(); } else { - InstanceHandle handle = (InstanceHandle) contextualInstances - .getValueIfPresent(new Key(null, bean.getIdentifier())); + InstanceHandle handle = (InstanceHandle) contextualInstances.getValueIfPresent(Key.of(bean.getIdentifier())); return handle != null ? handle.get() : null; } } @@ -61,7 +66,7 @@ public T get(Contextual contextual) { @Override public boolean isActive() { - return servletRequest() != null; + return session(true) != null; } @Override @@ -70,9 +75,9 @@ public ContextState getState() { @Override public Map, Object> getContextualInstances() { - HttpServletRequest httpServletRequest = servletRequest(); - if (httpServletRequest != null) { - return HttpSessionContext.this.getContextualInstances(httpServletRequest).getPresentValues().stream() + HttpSession session = session(false); + if (session != null) { + return HttpSessionContext.this.getContextualInstances(session).getPresentValues().stream() .collect(Collectors.toMap(ContextInstanceHandle::getBean, ContextInstanceHandle::get)); } return Collections.emptyMap(); @@ -82,13 +87,12 @@ public Map, Object> getContextualInstances() { @Override public void destroy(Contextual contextual) { - HttpServletRequest httpServletRequest = servletRequest(); - if (httpServletRequest == null) { + HttpSession session = session(true); + if (session == null) { throw new ContextNotActiveException(); } InjectableBean bean = (InjectableBean) contextual; - InstanceHandle instanceHandle = getContextualInstances(httpServletRequest) - .remove(new Key(null, bean.getIdentifier())); + InstanceHandle instanceHandle = getContextualInstances(session).remove(Key.of(bean.getIdentifier())); if (instanceHandle != null) { instanceHandle.destroy(); } @@ -96,34 +100,38 @@ public void destroy(Contextual contextual) { @Override public void destroy() { - HttpServletRequest httpServletRequest = servletRequest(); - if (httpServletRequest == null) { + HttpSession session = session(true); + if (session == null) { throw new ContextNotActiveException(); } - HttpSession session = httpServletRequest.getSession(false); - if (session != null) { - destroy(session); - } + destroy(session); } private void destroy(HttpSession session) { synchronized (this) { - ComputingCache> contextualInstances = getContextualInstances(session); - for (ContextInstanceHandle instance : contextualInstances.getPresentValues()) { - try { - instance.destroy(); - } catch (Exception e) { - throw new IllegalStateException("Unable to destroy instance" + instance.get(), e); + ComputingCache> instances = getContextualInstances(session); + for (ContextInstanceHandle instance : instances.getPresentValues()) { + // try to remove the contextual instance from the context + ContextInstanceHandle val = instances.remove(Key.of(instance.getBean().getIdentifier())); + if (val != null) { + // destroy it afterwards + try { + val.destroy(); + } catch (Exception e) { + LOG.errorf(e, "Unable to destroy bean instance: %s", val.get()); + } } } - contextualInstances.clear(); + if (!instances.isEmpty()) { + LOG.warnf( + "Some @SessionScoped beans were created during destruction of the session context: %s\n\t- potential @PreDestroy callbacks declared on the beans were not invoked\n\t- in general, @SessionScoped beans should not call other @SessionScoped beans in a @PreDestroy callback", + instances.getPresentValues().stream().map(ContextInstanceHandle::getBean) + .collect(Collectors.toList())); + } + instances.clear(); } } - private ComputingCache> getContextualInstances(HttpServletRequest httpServletRequest) { - return getContextualInstances(httpServletRequest.getSession()); - } - @SuppressWarnings({ "unchecked", "rawtypes" }) private ComputingCache> getContextualInstances(HttpSession session) { ComputingCache> contextualInstances = (ComputingCache>) session @@ -135,6 +143,9 @@ private ComputingCache> getContextualInstances(Htt if (contextualInstances == null) { contextualInstances = new ComputingCache<>(key -> { InjectableBean bean = Arc.container().bean(key.beanIdentifier); + if (key.creationalContext == null) { + throw new IllegalStateException("Cannot create bean "); + } return new ContextInstanceHandleImpl(bean, bean.create(key.creationalContext), key.creationalContext); }); session.setAttribute(CONTEXTUAL_INSTANCES_KEY, contextualInstances); @@ -144,16 +155,22 @@ private ComputingCache> getContextualInstances(Htt return contextualInstances; } - private HttpServletRequest servletRequest() { + private HttpSession session(boolean create) { + HttpSession session = null; try { - return (HttpServletRequest) ServletRequestContext.requireCurrent().getServletRequest(); - } catch (IllegalStateException e) { - return null; + session = ((HttpServletRequest) ServletRequestContext.requireCurrent().getServletRequest()).getSession(create); + } catch (IllegalStateException ignored) { + session = DESTRUCT_SESSION.get(); } + return session; } static class Key { + static Key of(String beanIdentifier) { + return new Key(null, beanIdentifier); + } + CreationalContext creationalContext; String beanIdentifier; @@ -187,7 +204,13 @@ public boolean equals(Object obj) { @Override public void sessionDestroyed(HttpSessionEvent se) { - destroy(se.getSession()); + HttpSession session = se.getSession(); + try { + DESTRUCT_SESSION.set(session); + destroy(session); + } finally { + DESTRUCT_SESSION.remove(); + } } } diff --git a/extensions/vertx-http/deployment/pom.xml b/extensions/vertx-http/deployment/pom.xml index a5154ddd7b939..bc6f99b58d5db 100644 --- a/extensions/vertx-http/deployment/pom.xml +++ b/extensions/vertx-http/deployment/pom.xml @@ -167,6 +167,11 @@ + + + de.thetaphi + forbiddenapis + diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java index 5e3253279e06c..ab7c6f4fd9883 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java @@ -15,6 +15,7 @@ public final class FilterBuildItem extends MultiBuildItem { public static final int CORS = 300; public static final int AUTHENTICATION = 200; public static final int AUTHORIZATION = 100; + private static final int AUTH_FAILURE_HANDLER = Integer.MIN_VALUE + 1; private final Handler handler; private final int priority; @@ -35,18 +36,22 @@ public FilterBuildItem(Handler handler, int priority) { } /** - * Creates a new instance of {@link FilterBuildItem}. + * Creates a new instance of {@link FilterBuildItem} with an authentication failure handler. * - * @param handler the handler, if {@code null} the filter won't be used. - * @param priority the priority, higher priority gets invoked first. Priority is only used to sort filters, user - * routes are called afterwards. Must be positive. - * @param isFailureHandler whether an HTTP request or failure should be routed to a handler. + * @param authFailureHandler authentication failure handler */ - public FilterBuildItem(Handler handler, int priority, boolean isFailureHandler) { - this.handler = handler; - checkPriority(priority); - this.priority = priority; - this.isFailureHandler = isFailureHandler; + private FilterBuildItem(Handler authFailureHandler) { + this.handler = authFailureHandler; + this.isFailureHandler = true; + this.priority = AUTH_FAILURE_HANDLER; + } + + /** + * Creates a new instance of {@link FilterBuildItem} with an authentication failure handler. + * The handler will be added as next to last, right before {@link io.quarkus.vertx.http.runtime.QuarkusErrorHandler}. + */ + public static FilterBuildItem ofAuthenticationFailureHandler(Handler authFailureHandler) { + return new FilterBuildItem(authFailureHandler); } private void checkPriority(int priority) { @@ -71,7 +76,16 @@ public boolean isFailureHandler() { * @return a filter object wrapping the handler and priority. */ public Filter toFilter() { - return new Filters.SimpleFilter(handler, priority, isFailureHandler); + if (isFailureHandler && priority == AUTH_FAILURE_HANDLER) { + // create filter for penultimate auth failure handler + final Filters.SimpleFilter filter = new Filters.SimpleFilter(); + filter.setPriority(AUTH_FAILURE_HANDLER); + filter.setFailureHandler(true); + filter.setHandler(handler); + return filter; + } else { + return new Filters.SimpleFilter(handler, priority, isFailureHandler); + } } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java index 238f4203f402f..c737487120cb4 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java @@ -12,12 +12,10 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.Set; import io.quarkus.arc.deployment.BeanContainerBuildItem; -import io.quarkus.builder.item.SimpleBuildItem; import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -30,7 +28,7 @@ import io.quarkus.runtime.util.ClassPathUtils; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; import io.quarkus.vertx.http.deployment.spi.AdditionalStaticResourceBuildItem; -import io.quarkus.vertx.http.runtime.HttpConfiguration; +import io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem; import io.quarkus.vertx.http.runtime.StaticResourcesRecorder; /** @@ -38,107 +36,40 @@ */ public class StaticResourcesProcessor { - @Deprecated - public static final class StaticResourcesBuildItem extends SimpleBuildItem { - - private final Set entries; - - public StaticResourcesBuildItem(Set entries) { - this.entries = entries; - } - - public Set getEntries() { - return entries; - } - - public Set getPaths() { - Set paths = new HashSet<>(entries.size()); - for (Entry entry : entries) { - paths.add(entry.getPath()); - } - return paths; - } - - public static class Entry { - private final String path; - private final boolean isDirectory; - - public Entry(String path, boolean isDirectory) { - this.path = path; - this.isDirectory = isDirectory; - } - - public String getPath() { - return path; - } - - public boolean isDirectory() { - return isDirectory; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - Entry entry = (Entry) o; - return isDirectory == entry.isDirectory && path.equals(entry.path); - } - - @Override - public int hashCode() { - return Objects.hash(path, isDirectory); - } - } - - } - @BuildStep void collectStaticResources(Capabilities capabilities, ApplicationArchivesBuildItem applicationArchivesBuildItem, List additionalStaticResources, - Optional deprecatedStaticResources, - BuildProducer staticResources) throws Exception { + BuildProducer staticResources) throws Exception { if (capabilities.isPresent(Capability.SERVLET)) { // Servlet container handles static resources return; } - // Copy deprecated build item - Set paths = getClasspathResources( - applicationArchivesBuildItem); - if (deprecatedStaticResources.isPresent()) { - Set deprecatedEntries = deprecatedStaticResources.get().getEntries(); - for (StaticResourcesBuildItem.Entry deprecatedEntry : deprecatedEntries) { - paths.add(new io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem.Entry(deprecatedEntry.getPath(), - deprecatedEntry.isDirectory())); - } - } + Set paths = getClasspathResources(applicationArchivesBuildItem); for (AdditionalStaticResourceBuildItem bi : additionalStaticResources) { - paths.add(new io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem.Entry(bi.getPath(), bi.isDirectory())); + paths.add(new StaticResourcesBuildItem.Entry(bi.getPath(), bi.isDirectory())); } if (!paths.isEmpty()) { - staticResources.produce(new io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem(paths)); + staticResources.produce(new StaticResourcesBuildItem(paths)); } } @BuildStep @Record(RUNTIME_INIT) - public void runtimeInit(Optional staticResources, - StaticResourcesRecorder recorder, CoreVertxBuildItem vertx, BeanContainerBuildItem beanContainer, - BuildProducer defaultRoutes, HttpConfiguration config) { + public void runtimeInit(Optional staticResources, StaticResourcesRecorder recorder, + CoreVertxBuildItem vertx, BeanContainerBuildItem beanContainer, + BuildProducer defaultRoutes) { if (staticResources.isPresent()) { defaultRoutes.produce(new DefaultRouteBuildItem(recorder.start(staticResources.get().getPaths()))); } } @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) - public void nativeImageResource(Optional staticResources, + public void nativeImageResource(Optional staticResources, BuildProducer producer) { if (staticResources.isPresent()) { - Set entries = staticResources.get() - .getEntries(); + Set entries = staticResources.get().getEntries(); List metaInfResources = new ArrayList<>(entries.size()); - for (io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem.Entry entry : entries) { + for (StaticResourcesBuildItem.Entry entry : entries) { if (entry.isDirectory()) { // TODO: do we perhaps want to register the whole directory? continue; @@ -157,10 +88,9 @@ public void nativeImageResource(Optional getClasspathResources( - ApplicationArchivesBuildItem applicationArchivesBuildItem) + private Set getClasspathResources(ApplicationArchivesBuildItem applicationArchivesBuildItem) throws Exception { - Set knownPaths = new HashSet<>(); + Set knownPaths = new HashSet<>(); for (ApplicationArchive i : applicationArchivesBuildItem.getAllApplicationArchives()) { i.accept(tree -> { @@ -178,34 +108,19 @@ private Set return knownPaths; } - private void collectKnownPaths(Path resource, - Set knownPaths) { + private void collectKnownPaths(Path resource, Set knownPaths) { try { Files.walkFileTree(resource, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path p, BasicFileAttributes attrs) throws IOException { - String simpleName = p.getFileName().toString(); String file = resource.relativize(p).toString(); - if (simpleName.equals("index.html") || simpleName.equals("index.htm")) { - Path parent = resource.relativize(p).getParent(); - if (parent == null) { - knownPaths.add(new io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem.Entry("/", true)); - } else { - String parentString = parent.toString(); - if (!parentString.startsWith("/")) { - parentString = "/" + parentString; - } - knownPaths.add(new io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem.Entry( - parentString + "/", true)); - } - } if (!file.startsWith("/")) { file = "/" + file; } // Windows has a backslash file = file.replace('\\', '/'); - knownPaths.add(new io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem.Entry(file, false)); + knownPaths.add(new StaticResourcesBuildItem.Entry(file, false)); return FileVisitResult.CONTINUE; } }); diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AbstractStaticResourcesTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AbstractStaticResourcesTest.java index c60ad3f28cb52..2fa199e967f5e 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AbstractStaticResourcesTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AbstractStaticResourcesTest.java @@ -9,22 +9,17 @@ public abstract class AbstractStaticResourcesTest { @Test public void shouldEncodeHtmlPage() { - RestAssured.when().get("/static-file.html") - .then() - .header("Content-Encoding", "gzip") - .header("Transfer-Encoding", "chunked") - .body(Matchers.containsString("This is the title of the webpage!")) - .statusCode(200); + assertEncodedResponse("/static-file.html"); } @Test public void shouldEncodeRootPage() { - RestAssured.when().get("/") - .then() - .header("Content-Encoding", "gzip") - .header("Transfer-Encoding", "chunked") - .body(Matchers.containsString("This is the title of the webpage!")) - .statusCode(200); + assertEncodedResponse("/"); + } + + @Test + public void shouldEncodeHiddenHtmlPage() { + assertEncodedResponse("/.hidden-file.html"); } @Test @@ -36,4 +31,22 @@ public void shouldNotEncodeSVG() { .statusCode(200); } + @Test + public void shouldReturnRangeSupport() { + RestAssured.when().head("/") + .then() + .header("Accept-Ranges", "bytes") + .header("Content-Length", Integer::parseInt, Matchers.greaterThan(0)) + .statusCode(200); + } + + protected void assertEncodedResponse(String path) { + RestAssured.when().get(path) + .then() + .header("Content-Encoding", "gzip") + .header("Transfer-Encoding", "chunked") + .body(Matchers.containsString("This is the title of the webpage!")) + .statusCode(200); + } + } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesCachingDisabledTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesCachingDisabledTest.java new file mode 100644 index 0000000000000..852381648e108 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesCachingDisabledTest.java @@ -0,0 +1,31 @@ +package io.quarkus.vertx.http; + +import static org.hamcrest.Matchers.nullValue; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class StaticResourcesCachingDisabledTest { + + @RegisterExtension + final static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .add(new StringAsset( + "quarkus.http.static-resources.caching-enabled=false\n"), + "application.properties") + .addAsResource("static-file.html", "META-INF/resources/index.html")); + + @Test + public void shouldNotContainCachingHeaders() { + RestAssured.when().get("/") + .then() + .header("Cache-Control", nullValue()) + .header("Last-Modified", nullValue()) + .statusCode(200); + } + +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesCustomizedPagesTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesCustomizedPagesTest.java new file mode 100644 index 0000000000000..b5e065e919769 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesCustomizedPagesTest.java @@ -0,0 +1,52 @@ +package io.quarkus.vertx.http; + +import static org.hamcrest.Matchers.containsStringIgnoringCase; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class StaticResourcesCustomizedPagesTest { + + @RegisterExtension + final static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .add(new StringAsset("" + + "quarkus.http.static-resources.index-page=default.html\n" + + "quarkus.http.static-resources.include-hidden=false\n" + + "quarkus.http.static-resources.enable-range-support=false\n"), + "application.properties") + .addAsResource("static-file.html", "META-INF/resources/.hidden-file.html") + .addAsResource("static-file.html", "META-INF/resources/default.html")); + + @Test + public void shouldContainCachingHeaders() { + RestAssured.when().get("/") + .then() + .header("Cache-Control", containsStringIgnoringCase("max-age=")) + .header("Last-Modified", notNullValue()) + .statusCode(200); + } + + @Test + public void shouldNotReturnHiddenHtmlPage() { + RestAssured.when().get("/.hidden-file.html") + .then() + .statusCode(404); + } + + @Test + public void shouldNotReturnRangeSupport() { + RestAssured.when().head("/") + .then() + .header("Accept-Ranges", nullValue()) + .header("Content-Length", nullValue()) + .statusCode(200); + } + +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesTest.java index 81df34794aa72..d1a7f6c623c53 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/StaticResourcesTest.java @@ -13,6 +13,7 @@ public class StaticResourcesTest extends AbstractStaticResourcesTest { .add(new StringAsset("quarkus.http.enable-compression=true\n"), "application.properties") .addAsResource("static-file.html", "META-INF/resources/static-file.html") + .addAsResource("static-file.html", "META-INF/resources/.hidden-file.html") .addAsResource("static-file.html", "META-INF/resources/index.html") .addAsResource("static-file.html", "META-INF/resources/image.svg")); diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestCase.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestCase.java index d099842238f66..404627d4a32c3 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestCase.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestCase.java @@ -1,6 +1,7 @@ package io.quarkus.vertx.http.cors; import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.Is.is; import org.junit.jupiter.api.DisplayName; @@ -22,7 +23,7 @@ public class CORSHandlerTestCase { public void corsPreflightTestServlet() { String origin = "http://custom.origin.quarkus"; String methods = "GET,POST"; - String headers = "X-Custom"; + String headers = "X-Custom,content-type"; given().header("Origin", origin) .header("Access-Control-Request-Method", methods) .header("Access-Control-Request-Headers", headers) @@ -35,12 +36,29 @@ public void corsPreflightTestServlet() { .header("Access-Control-Allow-Headers", headers); } + @Test + public void corsPreflightTestUnmatchedHeader() { + String origin = "http://custom.origin.quarkus"; + String methods = "GET,POST"; + String headers = "X-Customs,content-types"; + given().header("Origin", origin) + .header("Access-Control-Request-Method", methods) + .header("Access-Control-Request-Headers", headers) + .when() + .options("/test").then() + .statusCode(200) + .header("Access-Control-Allow-Origin", origin) + .header("Access-Control-Allow-Methods", methods) + .header("Access-Control-Allow-Credentials", "true") + .header("Access-Control-Allow-Headers", nullValue()); + } + @Test @DisplayName("Handles a direct CORS request correctly") public void corsNoPreflightTestServlet() { String origin = "http://custom.origin.quarkus"; String methods = "GET,POST"; - String headers = "X-Custom"; + String headers = "x-custom,CONTENT-TYPE"; given().header("Origin", origin) .header("Access-Control-Request-Method", methods) .header("Access-Control-Request-Headers", headers) diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestWildcardOriginCase.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestWildcardOriginCase.java index bea0ee1578301..c1852e10dd84d 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestWildcardOriginCase.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestWildcardOriginCase.java @@ -49,6 +49,60 @@ void corsNotMatchingOrigin() { .header("Access-Control-Allow-Credentials", "false"); } + @Test + void corsSameOriginRequest() { + String origin = "http://localhost:8081"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(200) + .header("Access-Control-Allow-Origin", origin); + } + + @Test + void corsInvalidSameOriginRequest1() { + String origin = "http"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()); + } + + @Test + void corsInvalidSameOriginRequest2() { + String origin = "http://local"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()); + } + + @Test + void corsInvalidSameOriginRequest3() { + String origin = "http://localhost"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()); + } + + @Test + void corsInvalidSameOriginRequest4() { + String origin = "http://localhost:9999"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()); + } + + @Test + void corsInvalidSameOriginRequest5() { + String origin = "https://localhost:8483"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()); + } + @Test @DisplayName("Returns false 'Access-Control-Allow-Credentials' header on matching origin '*'") void corsMatchingOriginWithWildcard() { diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSSecurityTestCase.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSSecurityTestCase.java index 6b60a1b8ee620..8117bf00dbd57 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSSecurityTestCase.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSSecurityTestCase.java @@ -22,6 +22,7 @@ public class CORSSecurityTestCase { private static final String APP_PROPS = "" + "quarkus.http.cors=true\n" + + "quarkus.http.cors.origins=*\n" + "quarkus.http.cors.methods=GET, OPTIONS, POST\n" + "quarkus.http.auth.basic=true\n" + "quarkus.http.auth.policy.r1.roles-allowed=test\n" + diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSWildcardSecurityTestCase.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSWildcardSecurityTestCase.java index 0170bc7186484..0cbda431190ef 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSWildcardSecurityTestCase.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSWildcardSecurityTestCase.java @@ -22,6 +22,7 @@ public class CORSWildcardSecurityTestCase { private static final String APP_PROPS = "" + "quarkus.http.cors=true\n" + + "quarkus.http.cors.origins=*\n" + "quarkus.http.auth.basic=true\n" + "quarkus.http.auth.policy.r1.roles-allowed=test\n" + "quarkus.http.auth.permission.roles1.paths=/test\n" + diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devmode/StaticResourcesDevModeTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devmode/StaticResourcesDevModeTest.java index 3c358a610b5e9..c4b8ced645ee0 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devmode/StaticResourcesDevModeTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devmode/StaticResourcesDevModeTest.java @@ -17,6 +17,7 @@ public class StaticResourcesDevModeTest extends AbstractStaticResourcesTest { .add(new StringAsset("quarkus.http.enable-compression=true\n"), "application.properties") .addAsResource("static-file.html", "META-INF/resources/static-file.html") + .addAsResource("static-file.html", "META-INF/resources/.hidden-file.html") .addAsResource("static-file.html", "META-INF/resources/index.html") .addAsResource("static-file.html", "META-INF/resources/image.svg")); diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/FormAuthNoRedirectAfterLoginTestCase.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/FormAuthNoRedirectAfterLoginTestCase.java new file mode 100644 index 0000000000000..9eb3442946575 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/FormAuthNoRedirectAfterLoginTestCase.java @@ -0,0 +1,142 @@ +package io.quarkus.vertx.http.security; + +import static org.hamcrest.Matchers.*; + +import java.util.function.Supplier; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.filter.cookie.CookieFilter; + +public class FormAuthNoRedirectAfterLoginTestCase { + + private static final String APP_PROPS = "" + + "quarkus.http.auth.form.enabled=true\n" + + "quarkus.http.auth.form.login-page=login\n" + + "quarkus.http.auth.form.error-page=error\n" + + "quarkus.http.auth.form.landing-page=landing\n" + + "quarkus.http.auth.form.redirect-after-login=false\n" + + "quarkus.http.auth.policy.r1.roles-allowed=a d m i n\n" + + "quarkus.http.auth.permission.roles1.paths=/admin\n" + + "quarkus.http.auth.permission.roles1.policy=r1\n"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestIdentityController.class, TestTrustedIdentityProvider.class, + PathHandler.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles() + .add("a d m i n", "a d m i n", "a d m i n"); + } + + /** + * First, protected /admin resource is accessed. No quarkus-credential cookie + * is presented by the client, so server should redirect to /login page. + * + * Next, let's assume there was a login form on the /login page, + * we do POST with valid credentials. + * Server should provide a response with quarkus-credential cookie + * and a redirect to the previously attempted /admin page. + * Note the redirect takes place despite having quarkus.http.auth.form.redirect-after-login=false + * because there is some previous location to redirect to. + * + * Last but not least, client accesses the protected /admin resource again, + * this time providing server with stored quarkus-credential cookie. + * Access is granted and landing page displayed. + */ + @Test + public void testFormBasedAuthSuccess() { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + CookieFilter cookies = new CookieFilter(); + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/login")) + .cookie("quarkus-redirect-location", containsString("/admin")); + + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .formParam("j_username", "a d m i n") + .formParam("j_password", "a d m i n") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/admin")) + .cookie("quarkus-credential", notNullValue()); + + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .get("/admin") + .then() + .assertThat() + .statusCode(200) + .body(equalTo("a d m i n:/admin")); + } + + @Test + public void testFormBasedAuthSuccessLandingPage() { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + CookieFilter cookies = new CookieFilter(); + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .formParam("j_username", "a d m i n") + .formParam("j_password", "a d m i n") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(200) + .cookie("quarkus-credential", notNullValue()); + } + + @Test + public void testFormAuthFailure() { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + CookieFilter cookies = new CookieFilter(); + RestAssured + .given() + .filter(cookies) + .redirects().follow(false) + .when() + .formParam("j_username", "a d m i n") + .formParam("j_password", "wrongpassword") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/error")) + .header("quarkus-credential", nullValue()); + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/FormAuthNoRedirectTestCase.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/FormAuthNoRedirectTestCase.java index e95d00b2d0f51..b6e5352f9d7f7 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/FormAuthNoRedirectTestCase.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/FormAuthNoRedirectTestCase.java @@ -1,6 +1,9 @@ package io.quarkus.vertx.http.security; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import java.util.function.Supplier; @@ -21,10 +24,9 @@ public class FormAuthNoRedirectTestCase { private static final String APP_PROPS = "" + "quarkus.http.auth.form.enabled=true\n" + - "quarkus.http.auth.form.login-page=login\n" + - "quarkus.http.auth.form.error-page=error\n" + - "quarkus.http.auth.form.landing-page=landing\n" + - "quarkus.http.auth.form.redirect-after-login=false\n" + + "quarkus.http.auth.form.login-page=\n" + + "quarkus.http.auth.form.error-page=\n" + + "quarkus.http.auth.form.landing-page=\n" + "quarkus.http.auth.policy.r1.roles-allowed=a d m i n\n" + "quarkus.http.auth.permission.roles1.paths=/admin\n" + "quarkus.http.auth.permission.roles1.policy=r1\n"; @@ -48,14 +50,11 @@ public static void setup() { /** * First, protected /admin resource is accessed. No quarkus-credential cookie - * is presented by the client, so server should redirect to /login page. + * is presented by the client, so server should respond with 401. * * Next, let's assume there was a login form on the /login page, * we do POST with valid credentials. - * Server should provide a response with quarkus-credential cookie - * and a redirect to the previously attempted /admin page. - * Note the redirect takes place despite having quarkus.http.auth.form.redirect-after-login=false - * because there is some previous location to redirect to. + * Server should provide a response with quarkus-credential cookie and respond with 200. * * Last but not least, client accesses the protected /admin resource again, * this time providing server with stored quarkus-credential cookie. @@ -73,9 +72,8 @@ public void testFormBasedAuthSuccess() { .get("/admin") .then() .assertThat() - .statusCode(302) - .header("location", containsString("/login")) - .cookie("quarkus-redirect-location", containsString("/admin")); + .statusCode(401) + .header("location", nullValue()); RestAssured .given() @@ -87,8 +85,8 @@ public void testFormBasedAuthSuccess() { .post("/j_security_check") .then() .assertThat() - .statusCode(302) - .header("location", containsString("/admin")) + .statusCode(200) + .header("location", nullValue()) .cookie("quarkus-credential", notNullValue()); RestAssured @@ -104,7 +102,7 @@ public void testFormBasedAuthSuccess() { } @Test - public void testFormBasedAuthSuccessLandingPage() { + public void testFormBasedAuthSuccessNoLocation() { RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); CookieFilter cookies = new CookieFilter(); RestAssured @@ -121,6 +119,24 @@ public void testFormBasedAuthSuccessLandingPage() { .cookie("quarkus-credential", notNullValue()); } + @Test + public void testFormBasedAuthSuccessWithLocation() { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + RestAssured + .given() + .cookie("quarkus-redirect-location", "http://localhost:8081/admin") + .redirects().follow(false) + .when() + .formParam("j_username", "a d m i n") + .formParam("j_password", "a d m i n") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(302) + .header("location", containsString("/admin")) + .cookie("quarkus-credential", notNullValue()); + } + @Test public void testFormAuthFailure() { RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); @@ -135,8 +151,7 @@ public void testFormAuthFailure() { .post("/j_security_check") .then() .assertThat() - .statusCode(302) - .header("location", containsString("/error")) - .header("quarkus-credential", nullValue()); + .header("location", nullValue()) + .statusCode(401); } } diff --git a/extensions/vertx-http/deployment/src/test/resources/conf/cors-config.properties b/extensions/vertx-http/deployment/src/test/resources/conf/cors-config.properties index d9a2687cc383d..6e587318d781d 100644 --- a/extensions/vertx-http/deployment/src/test/resources/conf/cors-config.properties +++ b/extensions/vertx-http/deployment/src/test/resources/conf/cors-config.properties @@ -1,4 +1,6 @@ quarkus.http.cors=true +quarkus.http.cors.origins=* # whitespaces added to test that they are not taken into account config is parsed quarkus.http.cors.methods=GET, OPTIONS, POST quarkus.http.cors.access-control-allow-credentials=true +quarkus.http.cors.headers=x-custom,CONTENT-TYPE diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/BodyConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/BodyConfig.java index 144033f305561..1c8723f7f33e4 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/BodyConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/BodyConfig.java @@ -55,4 +55,10 @@ public class BodyConfig { */ @ConfigItem public boolean preallocateBodyBuffer; + + /** + * HTTP multipart request related settings + */ + @ConfigItem + public MultiPartConfig multipart; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java index d7b396f06dbf0..2f3c1c4ae726c 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FormAuthConfig.java @@ -1,6 +1,7 @@ package io.quarkus.vertx.http.runtime; import java.time.Duration; +import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -11,16 +12,16 @@ @ConfigGroup public class FormAuthConfig { /** - * If form authentication is enabled + * If form authentication is enabled. */ @ConfigItem public boolean enabled; /** - * The login page + * The login page. Redirect to login page can be disabled by setting `quarkus.http.auth.form.login-page=`. */ @ConfigItem(defaultValue = "/login.html") - public String loginPage; + public Optional loginPage; /** * The post location. @@ -41,22 +42,28 @@ public class FormAuthConfig { public String passwordParameter; /** - * The error page + * The error page. Redirect to error page can be disabled by setting `quarkus.http.auth.form.error-page=`. */ @ConfigItem(defaultValue = "/error.html") - public String errorPage; + public Optional errorPage; /** - * The landing page to redirect to if there is no saved page to redirect back to + * The landing page to redirect to if there is no saved page to redirect back to. + * Redirect to landing page can be disabled by setting `quarkus.http.auth.form.landing-page=`. */ @ConfigItem(defaultValue = "/index.html") - public String landingPage; + public Optional landingPage; /** * Option to disable redirect to landingPage if there is no saved page to redirect back to. Form Auth POST is followed * by redirect to landingPage by default. + * + * @deprecated redirect to landingPage can be disabled by removing default landing page + * (via `quarkus.http.auth.form.landing-page=`). Quarkus will ignore this configuration property + * if there is no landing page. */ @ConfigItem(defaultValue = "true") + @Deprecated public boolean redirectAfterLogin; /** diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index 86e6eb7d89581..e62490a3dddbe 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -91,6 +91,11 @@ public class HttpConfiguration { */ public ServerSslConfig ssl; + /** + * The Static Resources config + */ + public StaticResourcesConfig staticResources; + /** * When set to {@code true}, the HTTP server automatically sends `100 CONTINUE` * response when the request expects it (with the `Expect: 100-Continue` header). diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/MultiPartConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/MultiPartConfig.java new file mode 100644 index 0000000000000..a0e262b28d6e5 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/MultiPartConfig.java @@ -0,0 +1,23 @@ +package io.quarkus.vertx.http.runtime; + +import java.util.List; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConvertWith; +import io.quarkus.runtime.configuration.TrimmedStringConverter; + +/** + * A {@link ConfigGroup} for the settings related to HTTP multipart request handling. + */ +@ConfigGroup +public class MultiPartConfig { + + /** + * A list of {@code ContentType} to indicate whether a given multipart field should be handled as a file part. + */ + @ConfigItem + @ConvertWith(TrimmedStringConverter.class) + public Optional> fileContentTypes; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesConfig.java new file mode 100644 index 0000000000000..94ff031402d81 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesConfig.java @@ -0,0 +1,53 @@ +package io.quarkus.vertx.http.runtime; + +import java.time.Duration; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class StaticResourcesConfig { + + /** + * Set the index page when serving static resources. + */ + @ConfigItem(defaultValue = "index.html") + public String indexPage; + + /** + * Set whether hidden files should be served. + */ + @ConfigItem(defaultValue = "true") + public boolean includeHidden; + + /** + * Set whether range requests (resumable downloads; media streaming) should be enabled. + */ + @ConfigItem(defaultValue = "true") + public boolean enableRangeSupport; + + /** + * Set whether cache handling is enabled. + */ + @ConfigItem(defaultValue = "true") + public boolean cachingEnabled; + + /** + * Set the cache entry timeout. The default is {@code 30} seconds. + */ + @ConfigItem(defaultValue = "30S") + public Duration cacheEntryTimeout; + + /** + * Set value for max age in caching headers. The default is {@code 24} hours. + */ + @ConfigItem(defaultValue = "24H") + public Duration maxAge; + + /** + * Set the max cache size. + */ + @ConfigItem(defaultValue = "10000") + public int maxCacheSize; + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java index c21561bb2d0f3..e6e3a42c10af8 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java @@ -13,6 +13,7 @@ import io.vertx.core.http.impl.MimeMapping; import io.vertx.ext.web.Route; import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.FileSystemAccess; import io.vertx.ext.web.handler.StaticHandler; @Recorder @@ -41,15 +42,17 @@ public Consumer start(Set knownPaths) { this.compressMediaTypes = Set.copyOf(httpBuildTimeConfig.compressMediaTypes.get()); } List> handlers = new ArrayList<>(); + StaticResourcesConfig config = httpConfiguration.getValue().staticResources; if (hotDeploymentResourcePaths != null && !hotDeploymentResourcePaths.isEmpty()) { for (Path resourcePath : hotDeploymentResourcePaths) { String root = resourcePath.toAbsolutePath().toString(); - StaticHandler staticHandler = StaticHandler.create(); - staticHandler.setCachingEnabled(false); - staticHandler.setAllowRootFileSystemAccess(true); - staticHandler.setWebRoot(root); - staticHandler.setDefaultContentEncoding("UTF-8"); + StaticHandler staticHandler = StaticHandler.create(FileSystemAccess.ROOT, root) + .setDefaultContentEncoding("UTF-8") + .setCachingEnabled(false) + .setIndexPage(config.indexPage) + .setIncludeHidden(config.includeHidden) + .setEnableRangeSupport(config.enableRangeSupport); handlers.add(new Handler<>() { @Override public void handle(RoutingContext ctx) { @@ -67,7 +70,20 @@ public void handle(RoutingContext ctx) { } if (!knownPaths.isEmpty()) { ClassLoader currentCl = Thread.currentThread().getContextClassLoader(); - StaticHandler staticHandler = StaticHandler.create(META_INF_RESOURCES).setDefaultContentEncoding("UTF-8"); + StaticHandler staticHandler = StaticHandler.create(META_INF_RESOURCES) + .setDefaultContentEncoding("UTF-8") + .setCachingEnabled(config.cachingEnabled) + .setIndexPage(config.indexPage) + .setIncludeHidden(config.includeHidden) + .setEnableRangeSupport(config.enableRangeSupport) + .setMaxCacheSize(config.maxCacheSize) + .setCacheEntryTimeout(config.cacheEntryTimeout.toMillis()) + .setMaxAgeSeconds(config.maxAge.toSeconds()); + // normalize index page like StaticHandler because its not expose + // TODO: create a converter to normalize filename in config.indexPage? + final String indexPage = (config.indexPage.charAt(0) == '/') + ? config.indexPage.substring(1) + : config.indexPage; handlers.add(new Handler<>() { @Override public void handle(RoutingContext ctx) { @@ -75,7 +91,8 @@ public void handle(RoutingContext ctx) { : ctx.normalizedPath().substring( // let's be extra careful here in case Vert.x normalizes the mount points at some point ctx.mountPoint().endsWith("/") ? ctx.mountPoint().length() - 1 : ctx.mountPoint().length()); - if (knownPaths.contains(rel)) { + // check effective path, otherwise the index page when path ends with '/' + if (knownPaths.contains(rel) || (rel.endsWith("/") && knownPaths.contains(rel.concat(indexPage)))) { compressIfNeeded(ctx, rel); staticHandler.handle(ctx); } else { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSConfig.java index d98820d59c099..4b2a57fd22573 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSConfig.java @@ -18,9 +18,6 @@ public class CORSConfig { * Comma separated list of valid URLs, e.g.: http://www.quarkus.io,http://localhost:3000 * In case an entry of the list is surrounded by forward slashes, * it is interpreted as a regular expression. - * The filter allows any origin if this is not set. - * - * default: returns any requested origin as valid */ @ConfigItem @ConvertWith(TrimmedStringConverter.class) diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSFilter.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSFilter.java index 4a7fee1a14e4f..30d1ad80ec87e 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSFilter.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSFilter.java @@ -1,8 +1,11 @@ package io.quarkus.vertx.http.runtime.cors; +import java.net.URI; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.regex.Pattern; @@ -22,18 +25,20 @@ public class CORSFilter implements Handler { // Must be static because the filter is created(deployed) at build time and runtime config is still not available final CORSConfig corsConfig; - final List allowedOriginsRegex; + private final boolean wildcardOrigin; + private final List allowedOriginsRegex; private final List configuredHttpMethods; public CORSFilter(CORSConfig corsConfig) { this.corsConfig = corsConfig; - this.allowedOriginsRegex = parseAllowedOriginsRegex(this.corsConfig.origins); - configuredHttpMethods = createConfiguredHttpMethods(this.corsConfig.methods); + this.wildcardOrigin = isOriginConfiguredWithWildcard(this.corsConfig.origins); + this.allowedOriginsRegex = this.wildcardOrigin ? List.of() : parseAllowedOriginsRegex(this.corsConfig.origins); + this.configuredHttpMethods = createConfiguredHttpMethods(this.corsConfig.methods); } private List createConfiguredHttpMethods(Optional> methods) { if (methods.isEmpty()) { - return Collections.emptyList(); + return List.of(); } List corsConfigMethods = methods.get(); List result = new ArrayList<>(corsConfigMethods.size()); @@ -52,6 +57,10 @@ public static boolean isConfiguredWithWildcard(Optional> optionalLi return list.isEmpty() || (list.size() == 1 && "*".equals(list.get(0))); } + private static boolean isOriginConfiguredWithWildcard(Optional> origins) { + return !origins.isEmpty() && origins.get().size() == 1 && "*".equals(origins.get().get(0)); + } + /** * Parse the provided allowed origins for any regexes * @@ -60,7 +69,7 @@ public static boolean isConfiguredWithWildcard(Optional> optionalLi */ public static List parseAllowedOriginsRegex(Optional> allowedOrigins) { if (allowedOrigins == null || !allowedOrigins.isPresent()) { - return Collections.emptyList(); + return List.of(); } // extract configured origins and find any Regular Expressions @@ -78,7 +87,7 @@ public static List parseAllowedOriginsRegex(Optional> allo * If any regular expression origins are configured, try to match on them. * Regular expressions must begin and end with '/' * - * @param allowedOrigins the configured regex origins. + * @param allowOriginsRegex the configured regex origins. * @param origin the specified origin * @return true if any configured regular expressions match the specified origin, false otherwise */ @@ -98,24 +107,25 @@ private void processRequestedHeaders(HttpServerResponse response, String allowHe if (isConfiguredWithWildcard(corsConfig.headers)) { response.headers().set(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, allowHeadersValue); } else { - List requestedHeaders; + Map requestedHeaders; String[] allowedParts = COMMA_SEPARATED_SPLIT_REGEX.split(allowHeadersValue); - requestedHeaders = new ArrayList<>(allowedParts.length); + requestedHeaders = new HashMap<>(); for (String requestedHeader : allowedParts) { - requestedHeaders.add(requestedHeader.toLowerCase()); + requestedHeaders.put(requestedHeader.toLowerCase(), requestedHeader); } List corsConfigHeaders = corsConfig.headers.get(); StringBuilder allowedHeaders = new StringBuilder(); boolean isFirst = true; for (String configHeader : corsConfigHeaders) { - if (requestedHeaders.contains(configHeader.toLowerCase())) { + String configHeaderLowerCase = configHeader.toLowerCase(); + if (requestedHeaders.containsKey(configHeaderLowerCase)) { if (isFirst) { isFirst = false; } else { allowedHeaders.append(','); } - allowedHeaders.append(configHeader); + allowedHeaders.append(requestedHeaders.get(configHeaderLowerCase)); } } @@ -175,8 +185,13 @@ public void handle(RoutingContext event) { processRequestedHeaders(response, requestedHeaders); } - boolean allowsOrigin = isConfiguredWithWildcard(corsConfig.origins) || corsConfig.origins.get().contains(origin) - || isOriginAllowedByRegex(allowedOriginsRegex, origin); + boolean allowsOrigin = wildcardOrigin; + if (!allowsOrigin) { + allowsOrigin = !corsConfig.origins.isEmpty() + && (corsConfig.origins.get().contains(origin) + || isOriginAllowedByRegex(allowedOriginsRegex, origin) + || isSameOrigin(request, origin)); + } if (allowsOrigin) { response.headers().set(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin); @@ -210,4 +225,80 @@ public void handle(RoutingContext event) { } } } + + static boolean isSameOrigin(HttpServerRequest request, String origin) { + //fast path check, when everything is the same + if (origin.startsWith(request.scheme())) { + if (!substringMatch(origin, request.scheme().length(), "://", false)) { + return false; + } + if (substringMatch(origin, request.scheme().length() + 3, request.host(), true)) { + //they are a simple match + return true; + } + return isSameOriginSlowPath(request, origin); + } else { + return false; + } + } + + static boolean isSameOriginSlowPath(HttpServerRequest request, String origin) { + String absUriString = request.absoluteURI(); + //we already know the scheme is correct, as the fast path will reject that + URI baseUri = URI.create(absUriString); + URI originUri = URI.create(origin); + if (!originUri.getPath().isEmpty()) { + //origin should not contain a path component + //just reject it in this case + return false; + } + if (!baseUri.getHost().equals(originUri.getHost())) { + return false; + } + if (baseUri.getPort() == originUri.getPort()) { + return true; + } + if (baseUri.getPort() != -1 && originUri.getPort() != -1) { + //ports are explictly set + return false; + } + if (baseUri.getScheme().equals("http")) { + if (baseUri.getPort() == 80 || baseUri.getPort() == -1) { + if (originUri.getPort() == 80 || originUri.getPort() == -1) { + //port is either unset or 80 + return true; + } + } + } else if (baseUri.getScheme().equals("https")) { + if (baseUri.getPort() == 443 || baseUri.getPort() == -1) { + if (originUri.getPort() == 443 || originUri.getPort() == -1) { + //port is either unset or 443 + return true; + } + } + } + return false; + } + + static boolean substringMatch(String str, int pos, String substring, boolean requireFull) { + int length = str.length(); + int subLength = substring.length(); + int strPos = pos; + int subPos = 0; + if (pos + subLength > length) { + //too long, avoid checking in the loop + return false; + } + for (;;) { + if (subPos == subLength) { + //if we are at the end return the correct value, depending on if we are also at the end of the origin + return !requireFull || strPos == length; + } + if (str.charAt(strPos) != substring.charAt(subPos)) { + return false; + } + strPos++; + subPos++; + } + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java index 3cf9640b11713..b4e055b25eff4 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java @@ -10,6 +10,7 @@ import org.jboss.logging.Logger; import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpResponseStatus; import io.quarkus.security.AuthenticationCompletionException; import io.quarkus.security.credential.PasswordCredential; import io.quarkus.security.identity.IdentityProviderManager; @@ -37,7 +38,9 @@ public class FormAuthenticationMechanism implements HttpAuthenticationMechanism private final String passwordParameter; private final String locationCookie; private final String landingPage; - private final boolean redirectAfterLogin; + private final boolean redirectToLandingPage; + private final boolean redirectToErrorPage; + private final boolean redirectToLoginPage; private final PersistentLoginManager loginManager; @@ -51,7 +54,9 @@ public FormAuthenticationMechanism(String loginPage, String postLocation, this.locationCookie = locationCookie; this.errorPage = errorPage; this.landingPage = landingPage; - this.redirectAfterLogin = redirectAfterLogin; + this.redirectToLandingPage = landingPage != null && redirectAfterLogin; + this.redirectToLoginPage = loginPage != null; + this.redirectToErrorPage = errorPage != null; this.loginManager = loginManager; } @@ -85,7 +90,8 @@ public void handle(Void event) { public void accept(SecurityIdentity identity) { try { loginManager.save(identity, exchange, null, exchange.request().isSSL()); - if (redirectAfterLogin || exchange.getCookie(locationCookie) != null) { + if (redirectToLandingPage + || exchange.request().getCookie(locationCookie) != null) { handleRedirectBack(exchange); //we have authenticated, but we want to just redirect back to the original page //so we don't actually authenticate the current request @@ -117,7 +123,7 @@ public void accept(Throwable throwable) { } protected void handleRedirectBack(final RoutingContext exchange) { - Cookie redirect = exchange.getCookie(locationCookie); + Cookie redirect = exchange.request().getCookie(locationCookie); String location; if (redirect != null) { verifyRedirectBackLocation(exchange.request().absoluteURI(), redirect.getValue()); @@ -125,6 +131,12 @@ protected void handleRedirectBack(final RoutingContext exchange) { location = redirect.getValue(); exchange.response().addCookie(redirect.setMaxAge(0)); } else { + if (landingPage == null) { + // we know this won't happen with default implementation as we only call handleRedirectBack + // when landingPage is not null, however we can't control inheritors + throw new IllegalStateException( + "Landing page is no set, please make sure 'quarkus.http.auth.form.landing-page' is configured properly."); + } location = exchange.request().scheme() + "://" + exchange.request().host() + landingPage; } exchange.response().setStatusCode(302); @@ -194,15 +206,22 @@ public void accept(SecurityIdentity securityIdentity) { @Override public Uni getChallenge(RoutingContext context) { if (context.normalizedPath().endsWith(postLocation) && context.request().method().equals(HttpMethod.POST)) { - log.debugf("Serving form auth error page %s for %s", loginPage, context); - // This method would no longer be called if authentication had already occurred. - return getRedirect(context, errorPage); + if (redirectToErrorPage) { + log.debugf("Serving form auth error page %s for %s", errorPage, context); + // This method would no longer be called if authentication had already occurred. + return getRedirect(context, errorPage); + } } else { - log.debugf("Serving login form %s for %s", loginPage, context); - // we need to store the URL - storeInitialLocation(context); - return getRedirect(context, loginPage); + if (redirectToLoginPage) { + log.debugf("Serving login form %s for %s", loginPage, context); + // we need to store the URL + storeInitialLocation(context); + return getRedirect(context, loginPage); + } } + + // redirect is disabled + return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.UNAUTHORIZED.code(), null, null)); } @Override diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index 6267661eb99e1..0c294203a3e2f 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -176,9 +176,7 @@ public Uni apply(SecurityIdentity securityIdentity) public void accept(SecurityIdentity identity, Throwable throwable, Boolean aBoolean) { if (identity != null) { //when the result is evaluated we set the user, even if it is evaluated lazily - if (identity != null) { - event.setUser(new QuarkusHttpUser(identity)); - } + event.setUser(new QuarkusHttpUser(identity)); } else if (throwable != null) { //handle the auth failure //this can be customised @@ -224,6 +222,7 @@ public void created(BeanContainer container) { public Supplier setupFormAuth() { return new Supplier() { + @Override public FormAuthenticationMechanism get() { String key; @@ -243,10 +242,10 @@ public FormAuthenticationMechanism get() { FormAuthConfig form = buildTimeConfig.auth.form; PersistentLoginManager loginManager = new PersistentLoginManager(key, form.cookieName, form.timeout.toMillis(), form.newCookieInterval.toMillis(), form.httpOnlyCookie); - String loginPage = form.loginPage.startsWith("/") ? form.loginPage : "/" + form.loginPage; - String errorPage = form.errorPage.startsWith("/") ? form.errorPage : "/" + form.errorPage; - String landingPage = form.landingPage.startsWith("/") ? form.landingPage : "/" + form.landingPage; - String postLocation = form.postLocation.startsWith("/") ? form.postLocation : "/" + form.postLocation; + String loginPage = startWithSlash(form.loginPage.orElse(null)); + String errorPage = startWithSlash(form.errorPage.orElse(null)); + String landingPage = startWithSlash(form.landingPage.orElse(null)); + String postLocation = startWithSlash(form.postLocation); String usernameParameter = form.usernameParameter; String passwordParameter = form.passwordParameter; String locationCookie = form.locationCookie; @@ -257,6 +256,13 @@ public FormAuthenticationMechanism get() { }; } + private static String startWithSlash(String page) { + if (page == null) { + return null; + } + return page.startsWith("/") ? page : "/" + page; + } + public Supplier setupBasicAuth(HttpBuildTimeConfig buildTimeConfig) { return new Supplier() { @Override @@ -312,6 +318,9 @@ protected DefaultAuthFailureHandler() { @Override public void accept(RoutingContext event, Throwable throwable) { + if (event.response().ended()) { + return; + } throwable = extractRootCause(throwable); //auth failed if (throwable instanceof AuthenticationFailedException) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PersistentLoginManager.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PersistentLoginManager.java index bb98e0b1d5ef3..ff6f47c36d7eb 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PersistentLoginManager.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PersistentLoginManager.java @@ -63,7 +63,7 @@ public RestoreResult restore(RoutingContext context) { } public RestoreResult restore(RoutingContext context, String cookieName) { - Cookie existing = context.getCookie(cookieName); + Cookie existing = context.request().getCookie(cookieName); // If there is no credential cookie, we have nothing to restore. if (existing == null) { // Enforce new login. diff --git a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/cors/CORSFilterTest.java b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/cors/CORSFilterTest.java index 5686242b7eaef..e2348e9efcf7b 100644 --- a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/cors/CORSFilterTest.java +++ b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/cors/CORSFilterTest.java @@ -2,7 +2,9 @@ import static io.quarkus.vertx.http.runtime.cors.CORSFilter.isConfiguredWithWildcard; import static io.quarkus.vertx.http.runtime.cors.CORSFilter.isOriginAllowedByRegex; +import static io.quarkus.vertx.http.runtime.cors.CORSFilter.isSameOrigin; import static io.quarkus.vertx.http.runtime.cors.CORSFilter.parseAllowedOriginsRegex; +import static io.quarkus.vertx.http.runtime.cors.CORSFilter.substringMatch; import java.util.Arrays; import java.util.Collections; @@ -12,6 +14,9 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.vertx.core.http.HttpServerRequest; public class CORSFilterTest { @@ -37,4 +42,46 @@ public void isOriginAllowedByRegexTest() { Assertions.assertEquals(regexList.size(), 1); Assertions.assertTrue(isOriginAllowedByRegex(regexList, "https://abc-123.app.mydomain.com")); } + + @Test + public void sameOriginTest() { + var request = Mockito.mock(HttpServerRequest.class); + Mockito.when(request.scheme()).thenReturn("http"); + Mockito.when(request.host()).thenReturn("localhost"); + Mockito.when(request.absoluteURI()).thenReturn("http://localhost"); + Assertions.assertTrue(isSameOrigin(request, "http://localhost")); + Assertions.assertTrue(isSameOrigin(request, "http://localhost:80")); + Assertions.assertFalse(isSameOrigin(request, "http://localhost:8080")); + Assertions.assertFalse(isSameOrigin(request, "https://localhost")); + Mockito.when(request.host()).thenReturn("localhost:8080"); + Mockito.when(request.absoluteURI()).thenReturn("http://localhost:8080"); + Assertions.assertFalse(isSameOrigin(request, "http://localhost")); + Assertions.assertFalse(isSameOrigin(request, "http://localhost:80")); + Assertions.assertTrue(isSameOrigin(request, "http://localhost:8080")); + Assertions.assertFalse(isSameOrigin(request, "https://localhost:8080")); + Mockito.when(request.scheme()).thenReturn("https"); + Mockito.when(request.host()).thenReturn("localhost"); + Mockito.when(request.absoluteURI()).thenReturn("http://localhost"); + Assertions.assertFalse(isSameOrigin(request, "http://localhost")); + Assertions.assertFalse(isSameOrigin(request, "http://localhost:443")); + Assertions.assertFalse(isSameOrigin(request, "https://localhost:8080")); + Assertions.assertTrue(isSameOrigin(request, "https://localhost")); + Mockito.when(request.host()).thenReturn("localhost:8443"); + Mockito.when(request.absoluteURI()).thenReturn("https://localhost:8443"); + Assertions.assertFalse(isSameOrigin(request, "http://localhost")); + Assertions.assertFalse(isSameOrigin(request, "http://localhost:80")); + Assertions.assertFalse(isSameOrigin(request, "http://localhost:8443")); + Assertions.assertTrue(isSameOrigin(request, "https://localhost:8443")); + + } + + @Test + public void testSubstringMatches() { + Assertions.assertTrue(substringMatch("localhost", 0, "local", false)); + Assertions.assertFalse(substringMatch("localhost", 0, "local", true)); + Assertions.assertFalse(substringMatch("localhost", 1, "local", false)); + Assertions.assertTrue(substringMatch("localhost", 5, "host", false)); + Assertions.assertTrue(substringMatch("localhost", 5, "host", true)); + + } } diff --git a/extensions/vertx/deployment/pom.xml b/extensions/vertx/deployment/pom.xml index 8e926aeee9ed4..1066bf68b64b1 100644 --- a/extensions/vertx/deployment/pom.xml +++ b/extensions/vertx/deployment/pom.xml @@ -70,6 +70,10 @@ + + de.thetaphi + forbiddenapis + diff --git a/extensions/vertx/deployment/src/main/java/io/quarkus/vertx/deployment/EventBusCodecProcessor.java b/extensions/vertx/deployment/src/main/java/io/quarkus/vertx/deployment/EventBusCodecProcessor.java index b365fb3f0ca15..27ae071ec82e7 100644 --- a/extensions/vertx/deployment/src/main/java/io/quarkus/vertx/deployment/EventBusCodecProcessor.java +++ b/extensions/vertx/deployment/src/main/java/io/quarkus/vertx/deployment/EventBusCodecProcessor.java @@ -78,7 +78,7 @@ public void registerCodecs( .toString()); } else if (!codecByTypes.containsKey(codecTargetFromParameter.name())) { LOGGER.debugf("Local Message Codec registered for type %s", - codecTargetFromParameter.toString()); + codecTargetFromParameter); codecByTypes.put(codecTargetFromParameter.name(), LOCAL_EVENT_BUS_CODEC); } } @@ -87,7 +87,7 @@ public void registerCodecs( if (codecTargetFromReturnType != null && !hasBuiltInCodec(codecTargetFromReturnType) && !codecByTypes.containsKey(codecTargetFromReturnType.name())) { - LOGGER.debugf("Local Message Codec registered for type %s", codecTargetFromReturnType.toString()); + LOGGER.debugf("Local Message Codec registered for type %s", codecTargetFromReturnType); codecByTypes.put(codecTargetFromReturnType.name(), LOCAL_EVENT_BUS_CODEC); } } diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/context/VertxContextSafetyToggle.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/context/VertxContextSafetyToggle.java index d637809ddf828..7022a24dc501a 100644 --- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/context/VertxContextSafetyToggle.java +++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/context/VertxContextSafetyToggle.java @@ -120,6 +120,10 @@ public static void setContextSafe(final Context context, final boolean safe) { throw new IllegalStateException( "Can't set the context safety flag: the current context is not a duplicated context"); } else { + // save storm of true -> true transitions by shielding it + if (safe && context.getLocal(ACCESS_TOGGLE_KEY) == Boolean.TRUE) { + return; + } context.putLocal(ACCESS_TOGGLE_KEY, Boolean.valueOf(safe)); } } diff --git a/extensions/websockets/client/runtime/src/main/java/io/quarkus/websockets/client/runtime/WebsocketCoreRecorder.java b/extensions/websockets/client/runtime/src/main/java/io/quarkus/websockets/client/runtime/WebsocketCoreRecorder.java index ca321461c596d..670656f1a0a5a 100644 --- a/extensions/websockets/client/runtime/src/main/java/io/quarkus/websockets/client/runtime/WebsocketCoreRecorder.java +++ b/extensions/websockets/client/runtime/src/main/java/io/quarkus/websockets/client/runtime/WebsocketCoreRecorder.java @@ -169,12 +169,12 @@ public T call(C context, UndertowSession session) throws Exception { boolean required = !requestContext.isActive(); if (required) { requestContext.activate(); - Principal p = session.getUserPrincipal(); - if (p instanceof WebSocketPrincipal) { - var current = getCurrentIdentityAssociation(); - if (current != null) { - current.setIdentity(((WebSocketPrincipal) p).getSecurityIdentity()); - } + } + Principal p = session.getUserPrincipal(); + if (p instanceof WebSocketPrincipal) { + var current = getCurrentIdentityAssociation(); + if (current != null) { + current.setIdentity(((WebSocketPrincipal) p).getSecurityIdentity()); } } try { diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 356733efabf4c..ab7cdca02cd3c 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -42,7 +42,7 @@ 2.0.2 1.3.3 - 3.0.4 + 3.0.5 5.9.1 3.8.6 3.23.1 diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationLiteralProcessor.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationLiteralProcessor.java index 88e8e83a89377..78c2a429fec3c 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationLiteralProcessor.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AnnotationLiteralProcessor.java @@ -46,7 +46,7 @@ public class AnnotationLiteralProcessor { generateAnnotationLiteralClassName(key.annotationName()), applicationClassPredicate.test(key.annotationName()), key.annotationClass)); - this.beanArchiveIndex = beanArchiveIndex; + this.beanArchiveIndex = Objects.requireNonNull(beanArchiveIndex); } boolean hasLiteralsToGenerate() { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanArchives.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanArchives.java index bd14d9e679fbd..4a6dd5c88542b 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanArchives.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanArchives.java @@ -52,14 +52,23 @@ public final class BeanArchives { /** * * @param applicationIndexes - * @return the final bean archive index + * @return the immutable bean archive index */ - public static IndexView buildBeanArchiveIndex(ClassLoader deploymentClassLoader, - Map> additionalClasses, IndexView... applicationIndexes) { + public static IndexView buildImmutableBeanArchiveIndex(IndexView... applicationIndexes) { List indexes = new ArrayList<>(); Collections.addAll(indexes, applicationIndexes); indexes.add(buildAdditionalIndex()); - return new IndexWrapper(CompositeIndex.create(indexes), deploymentClassLoader, additionalClasses); + return CompositeIndex.create(indexes); + } + + /** + * + * @param wrappedIndexes + * @return the computing bean archive index + */ + public static IndexView buildComputingBeanArchiveIndex(ClassLoader deploymentClassLoader, + Map> additionalClasses, IndexView immutableIndex) { + return new IndexWrapper(immutableIndex, deploymentClassLoader, additionalClasses); } private static IndexView buildAdditionalIndex() { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java index dfcca8ff7ebde..4997affe0f467 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -55,8 +56,8 @@ public class BeanDeployment { private final BuildContextImpl buildContext; - private final IndexView beanArchiveIndex; - + private final IndexView beanArchiveComputingIndex; + private final IndexView beanArchiveImmutableIndex; private final IndexView applicationIndex; private final Map qualifiers; @@ -124,7 +125,8 @@ public class BeanDeployment { } this.beanDefiningAnnotations = beanDefiningAnnotations; this.resourceAnnotations = new HashSet<>(builder.resourceAnnotations); - this.beanArchiveIndex = builder.beanArchiveIndex; + this.beanArchiveComputingIndex = builder.beanArchiveComputingIndex; + this.beanArchiveImmutableIndex = Objects.requireNonNull(builder.beanArchiveImmutableIndex); this.applicationIndex = builder.applicationIndex; this.annotationStore = new AnnotationStore(initAndSort(builder.annotationTransformers, buildContext), buildContext); if (buildContext != null) { @@ -141,11 +143,11 @@ public class BeanDeployment { this.excludeTypes = builder.excludeTypes != null ? new ArrayList<>(builder.excludeTypes) : Collections.emptyList(); qualifierNonbindingMembers = new HashMap<>(); - qualifiers = findQualifiers(this.beanArchiveIndex); + qualifiers = findQualifiers(); for (QualifierRegistrar registrar : builder.qualifierRegistrars) { for (Map.Entry> entry : registrar.getAdditionalQualifiers().entrySet()) { DotName dotName = entry.getKey(); - ClassInfo classInfo = getClassByName(this.beanArchiveIndex, dotName); + ClassInfo classInfo = getClassByName(getBeanArchiveIndex(), dotName); if (classInfo != null) { Set nonbindingMembers = entry.getValue(); if (nonbindingMembers == null) { @@ -156,15 +158,15 @@ public class BeanDeployment { } } } - repeatingQualifierAnnotations = findContainerAnnotations(qualifiers, this.beanArchiveIndex); + repeatingQualifierAnnotations = findContainerAnnotations(qualifiers); buildContextPut(Key.QUALIFIERS.asString(), Collections.unmodifiableMap(qualifiers)); interceptorNonbindingMembers = new HashMap<>(); - interceptorBindings = findInterceptorBindings(this.beanArchiveIndex); + interceptorBindings = findInterceptorBindings(); for (InterceptorBindingRegistrar registrar : builder.interceptorBindingRegistrars) { for (InterceptorBindingRegistrar.InterceptorBinding binding : registrar.getAdditionalBindings()) { DotName dotName = binding.getName(); - ClassInfo annotationClass = getClassByName(this.beanArchiveIndex, dotName); + ClassInfo annotationClass = getClassByName(getBeanArchiveIndex(), dotName); if (annotationClass != null) { Set nonbinding = new HashSet<>(); for (MethodInfo method : annotationClass.methods()) { @@ -177,7 +179,7 @@ public class BeanDeployment { interceptorBindings.put(dotName, annotationClass); } } - repeatingInterceptorBindingAnnotations = findContainerAnnotations(interceptorBindings, this.beanArchiveIndex); + repeatingInterceptorBindingAnnotations = findContainerAnnotations(interceptorBindings); buildContextPut(Key.INTERCEPTOR_BINDINGS.asString(), Collections.unmodifiableMap(interceptorBindings)); Set additionalStereotypes = new HashSet<>(); @@ -185,12 +187,11 @@ public class BeanDeployment { additionalStereotypes.addAll(stereotypeRegistrar.getAdditionalStereotypes()); } - this.stereotypes = findStereotypes(this.beanArchiveIndex, interceptorBindings, customContexts, additionalStereotypes, + this.stereotypes = findStereotypes(interceptorBindings, customContexts, additionalStereotypes, annotationStore); buildContextPut(Key.STEREOTYPES.asString(), Collections.unmodifiableMap(stereotypes)); this.transitiveInterceptorBindings = findTransitiveInterceptorBindings(interceptorBindings.keySet(), - this.beanArchiveIndex, new HashMap<>(), interceptorBindings, annotationStore); this.injectionPoints = new CopyOnWriteArrayList<>(); @@ -199,7 +200,7 @@ public class BeanDeployment { this.beans = new CopyOnWriteArrayList<>(); this.observers = new CopyOnWriteArrayList<>(); - this.assignabilityCheck = new AssignabilityCheck(beanArchiveIndex, applicationIndex); + this.assignabilityCheck = new AssignabilityCheck(getBeanArchiveIndex(), applicationIndex); this.beanResolver = new BeanResolverImpl(this); this.delegateInjectionPointResolver = new DelegateInjectionPointResolverImpl(this); this.interceptorResolver = new InterceptorResolver(this); @@ -517,12 +518,17 @@ public Collection getStereotypes() { } /** - * This index was used to discover components (beans, interceptors, qualifiers, etc.) and during type-safe resolution. + * Returns the index that was used during discovery and type-safe resolution. + *

    + * In general, the returned index is usually "computing" which means that it attempts to compute the information for the + * classes that were not part of the initial bean archive index. I.e. the returned index corresponds to + * {@link BeanProcessor.Builder#setComputingBeanArchiveIndex(IndexView)}. However, if the computing index was not set then + * the index set by {@link BeanProcessor.Builder#setImmutableBeanArchiveIndex(IndexView)} is used instead. * * @return the bean archive index */ public IndexView getBeanArchiveIndex() { - return beanArchiveIndex; + return beanArchiveComputingIndex != null ? beanArchiveComputingIndex : beanArchiveImmutableIndex; } /** @@ -670,9 +676,9 @@ private void buildContextPut(String key, Object value) { } } - private Map findQualifiers(IndexView index) { + private Map findQualifiers() { Map qualifiers = new HashMap<>(); - for (AnnotationInstance qualifier : index.getAnnotations(DotNames.QUALIFIER)) { + for (AnnotationInstance qualifier : beanArchiveImmutableIndex.getAnnotations(DotNames.QUALIFIER)) { ClassInfo qualifierClass = qualifier.target().asClass(); if (isExcluded(qualifierClass)) { continue; @@ -682,23 +688,23 @@ private Map findQualifiers(IndexView index) { return qualifiers; } - private Map findContainerAnnotations(Map annotations, IndexView index) { + private Map findContainerAnnotations(Map annotations) { Map containerAnnotations = new HashMap<>(); for (ClassInfo annotation : annotations.values()) { AnnotationInstance repeatableMetaAnnotation = annotation.declaredAnnotation(DotNames.REPEATABLE); if (repeatableMetaAnnotation != null) { DotName containerAnnotationName = repeatableMetaAnnotation.value().asClass().name(); - ClassInfo containerClass = getClassByName(index, containerAnnotationName); + ClassInfo containerClass = getClassByName(getBeanArchiveIndex(), containerAnnotationName); containerAnnotations.put(containerAnnotationName, containerClass); } } return containerAnnotations; } - private Map findInterceptorBindings(IndexView index) { + private Map findInterceptorBindings() { Map bindings = new HashMap<>(); // Note: doesn't use AnnotationStore, this will operate on classes without applying annotation transformers - for (AnnotationInstance binding : index.getAnnotations(DotNames.INTERCEPTOR_BINDING)) { + for (AnnotationInstance binding : beanArchiveImmutableIndex.getAnnotations(DotNames.INTERCEPTOR_BINDING)) { ClassInfo bindingClass = binding.target().asClass(); if (isExcluded(bindingClass)) { continue; @@ -709,7 +715,6 @@ private Map findInterceptorBindings(IndexView index) { } private static Map> findTransitiveInterceptorBindings(Collection initialBindings, - IndexView index, Map> result, Map interceptorBindings, AnnotationStore annotationStore) { // for all known interceptor bindings @@ -748,20 +753,20 @@ private static Set recursiveBuild(DotName name, return result; } - private Map findStereotypes(IndexView index, Map interceptorBindings, + private Map findStereotypes(Map interceptorBindings, Map> customContexts, Set additionalStereotypes, AnnotationStore annotationStore) { Map stereotypes = new HashMap<>(); Set stereotypeNames = new HashSet<>(); - for (AnnotationInstance annotation : index.getAnnotations(DotNames.STEREOTYPE)) { + for (AnnotationInstance annotation : beanArchiveImmutableIndex.getAnnotations(DotNames.STEREOTYPE)) { stereotypeNames.add(annotation.target().asClass().name()); } stereotypeNames.addAll(additionalStereotypes); for (DotName stereotypeName : stereotypeNames) { - ClassInfo stereotypeClass = getClassByName(index, stereotypeName); + ClassInfo stereotypeClass = getClassByName(getBeanArchiveIndex(), stereotypeName); if (stereotypeClass != null && !isExcluded(stereotypeClass)) { boolean isAlternative = false; @@ -852,7 +857,8 @@ private List findBeans(Collection beanDefiningAnnotations, Li .map(StereotypeInfo::getName) .collect(Collectors.toSet()); - for (ClassInfo beanClass : beanArchiveIndex.getKnownClasses()) { + // If needed use the specialized immutable index to discover beans + for (ClassInfo beanClass : beanArchiveImmutableIndex.getKnownClasses()) { if (Modifier.isInterface(beanClass.flags()) || Modifier.isAbstract(beanClass.flags()) || beanClass.isAnnotation() || beanClass.isEnum()) { @@ -987,7 +993,7 @@ private List findBeans(Collection beanDefiningAnnotations, Li } DotName superType = aClass.superName(); aClass = superType != null && !superType.equals(DotNames.OBJECT) - ? getClassByName(beanArchiveIndex, superType) + ? getClassByName(getBeanArchiveIndex(), superType) : null; } for (FieldInfo field : beanClass.fields()) { @@ -1233,7 +1239,7 @@ static void processErrors(List errors) { private List findInterceptors(List injectionPoints) { Map interceptorClasses = new HashMap<>(); - for (AnnotationInstance annotation : beanArchiveIndex.getAnnotations(DotNames.INTERCEPTOR)) { + for (AnnotationInstance annotation : beanArchiveImmutableIndex.getAnnotations(DotNames.INTERCEPTOR)) { if (Kind.CLASS.equals(annotation.target().kind())) { interceptorClasses.put(annotation.target().asClass().name(), annotation.target().asClass()); } @@ -1260,7 +1266,7 @@ private List findInterceptors(List injectio private List findDecorators(List injectionPoints) { Map decoratorClasses = new HashMap<>(); - for (AnnotationInstance annotation : beanArchiveIndex.getAnnotations(DotNames.DECORATOR)) { + for (AnnotationInstance annotation : beanArchiveImmutableIndex.getAnnotations(DotNames.DECORATOR)) { if (Kind.CLASS.equals(annotation.target().kind())) { decoratorClasses.put(annotation.target().asClass().name(), annotation.target().asClass()); } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java index 952895860541d..b58e17cbcdfb4 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java @@ -27,6 +27,7 @@ import javax.enterprise.context.spi.Contextual; import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.CreationException; import javax.enterprise.inject.IllegalProductException; import javax.enterprise.inject.literal.InjectLiteral; import javax.enterprise.inject.spi.InterceptionType; @@ -63,6 +64,8 @@ import io.quarkus.gizmo.FieldCreator; import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.FunctionCreator; +import io.quarkus.gizmo.Gizmo; +import io.quarkus.gizmo.Gizmo.StringBuilderGenerator; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; @@ -946,7 +949,25 @@ protected void implementCreate(ClassOutput classOutput, ClassCreator beanCreator injectionPointToProviderSupplierField, reflectionRegistration, targetPackage, isApplicationClass, create); } else if (bean.isSynthetic()) { - bean.getCreatorConsumer().accept(create); + if (bean.getScope().isNormal()) { + // Normal scoped synthetic beans should never return null + MethodCreator createSynthetic = beanCreator + .getMethodCreator("createSynthetic", providerType.descriptorName(), CreationalContext.class) + .setModifiers(ACC_PRIVATE); + bean.getCreatorConsumer().accept(createSynthetic); + ResultHandle ret = create.invokeVirtualMethod(createSynthetic.getMethodDescriptor(), create.getThis(), + create.getMethodParam(0)); + BytecodeCreator nullBeanInstance = create.ifNull(ret).trueBranch(); + StringBuilderGenerator message = Gizmo.newStringBuilder(nullBeanInstance); + message.append("Null contextual instance was produced by a normal scoped synthetic bean: "); + message.append(Gizmo.toString(nullBeanInstance, nullBeanInstance.getThis())); + ResultHandle e = nullBeanInstance.newInstance( + MethodDescriptor.ofConstructor(CreationException.class, String.class), message.callToString()); + nullBeanInstance.throwException(e); + create.returnValue(ret); + } else { + bean.getCreatorConsumer().accept(create); + } } // Bridge method needed diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java index b869031c5461b..4427cfe4bea47 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java @@ -91,7 +91,10 @@ private BeanProcessor(Builder builder) { this.applicationClassPredicate = builder.applicationClassPredicate; this.name = builder.name; this.output = builder.output; - this.annotationLiterals = new AnnotationLiteralProcessor(builder.beanArchiveIndex, applicationClassPredicate); + this.annotationLiterals = new AnnotationLiteralProcessor( + builder.beanArchiveComputingIndex != null ? builder.beanArchiveComputingIndex + : builder.beanArchiveImmutableIndex, + applicationClassPredicate); this.generateSources = builder.generateSources; this.allowMocking = builder.allowMocking; this.transformUnproxyableClasses = builder.transformUnproxyableClasses; @@ -99,7 +102,9 @@ private BeanProcessor(Builder builder) { // Initialize all build processors buildContext = new BuildContextImpl(); - buildContext.putInternal(Key.INDEX.asString(), builder.beanArchiveIndex); + buildContext.putInternal(Key.INDEX.asString(), + builder.beanArchiveComputingIndex != null ? builder.beanArchiveComputingIndex + : builder.beanArchiveImmutableIndex); this.beanRegistrars = initAndSort(builder.beanRegistrars, buildContext); this.observerRegistrars = initAndSort(builder.observerRegistrars, buildContext); @@ -440,7 +445,8 @@ public Predicate getInjectionPointAnnotationsPredicate() { public static class Builder { String name; - IndexView beanArchiveIndex; + IndexView beanArchiveComputingIndex; + IndexView beanArchiveImmutableIndex; IndexView applicationIndex; Collection additionalBeanDefiningAnnotations; ResourceOutput output; @@ -510,14 +516,33 @@ public Builder setName(String name) { } /** - * Set the bean archive index. This index is mandatory and is used to discover components (beans, interceptors, - * qualifiers, etc.) and during type-safe resolution. + * Set the computing bean archive index. This index is optional and can be used for example during type-safe resolution. + * If it's not set then the immutable index is used instead. + *

    + * The computing index must be built on top of the immutable index and compute only the classes that are not part of the + * immutable index. + *

    + * This index is never used to discover components (beans, observers, etc.). + * + * @param index + * @return self + * @see Builder#setImmutableBeanArchiveIndex(IndexView) + */ + public Builder setComputingBeanArchiveIndex(IndexView index) { + this.beanArchiveComputingIndex = index; + return this; + } + + /** + * Set the immutable bean archive index. This index is mandatory and is used to discover components (beans, observers, + * etc.). * - * @param beanArchiveIndex + * @param index * @return self + * @see Builder#setComputingBeanArchiveIndex(IndexView) */ - public Builder setBeanArchiveIndex(IndexView beanArchiveIndex) { - this.beanArchiveIndex = beanArchiveIndex; + public Builder setImmutableBeanArchiveIndex(IndexView index) { + this.beanArchiveImmutableIndex = index; return this; } @@ -526,11 +551,11 @@ public Builder setBeanArchiveIndex(IndexView beanArchiveIndex) { *

    * Some types may not be part of the bean archive index but are still needed during type-safe resolution. * - * @param applicationIndex + * @param index * @return self */ - public Builder setApplicationIndex(IndexView applicationIndex) { - this.applicationIndex = applicationIndex; + public Builder setApplicationIndex(IndexView index) { + this.applicationIndex = index; return this; } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java index a3c63d605ddf7..a0e5fa6f38592 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java @@ -205,7 +205,12 @@ Collection generate(BeanInfo bean, String beanClassName, // Always use invokevirtual and the original descriptor for java.lang.Object#toString() ret = forward.invokeVirtualMethod(originalMethodDescriptor, delegate, params); } else if (isInterface) { - ret = forward.invokeInterfaceMethod(method, delegate, params); + // make sure we invoke the method upon the provider type, i.e. don't use the original method descriptor + MethodDescriptor virtualMethod = MethodDescriptor.ofMethod(providerType.className(), + originalMethodDescriptor.getName(), + originalMethodDescriptor.getReturnType(), + originalMethodDescriptor.getParameterTypes()); + ret = forward.invokeInterfaceMethod(virtualMethod, delegate, params); } else if (isReflectionFallbackNeeded(method, targetPackage)) { // Reflection fallback ResultHandle paramTypesArray = forward.newArray(Class.class, forward.load(method.parametersCount())); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Types.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Types.java index 0b914cad9ef5b..1aee1542711da 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Types.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Types.java @@ -432,6 +432,30 @@ static List getResolvedParameters(ClassInfo classInfo, Map r } } + static void detectWildcardAndThrow(Type type, AnnotationTarget producerFieldOrMethod) { + if (producerFieldOrMethod == null) { + // not a producer, no further analysis required + return; + } + if (type.kind().equals(Kind.WILDCARD_TYPE)) { + throw new DefinitionException("Producer " + + (producerFieldOrMethod.kind().equals(AnnotationTarget.Kind.FIELD) ? "field " : "method ") + + producerFieldOrMethod + + " declared on class " + + (producerFieldOrMethod.kind().equals(AnnotationTarget.Kind.FIELD) + ? producerFieldOrMethod.asField().declaringClass().name() + : producerFieldOrMethod.asMethod().declaringClass().name()) + + + " contains a parameterized type with a wildcard. This type is not a legal bean type" + + " according to CDI specification."); + } else if (type.kind().equals(Kind.PARAMETERIZED_TYPE)) { + for (Type t : type.asParameterizedType().arguments()) { + // recursive check of all parameterized types + detectWildcardAndThrow(t, producerFieldOrMethod); + } + } + } + static Set getTypeClosure(ClassInfo classInfo, AnnotationTarget producerFieldOrMethod, Map resolvedTypeParameters, BeanDeployment beanDeployment, BiConsumer> resolvedTypeVariablesConsumer) { @@ -445,19 +469,12 @@ static Set getTypeClosure(ClassInfo classInfo, AnnotationTarget producerFi } else { // Canonical ParameterizedType with unresolved type variables Type[] typeParams = new Type[typeParameters.size()]; - boolean skipThisType = false; for (int i = 0; i < typeParameters.size(); i++) { typeParams[i] = resolvedTypeParameters.get(typeParameters.get(i).identifier()); - // this should only be the case for producers; wildcard is not a legal bean type + // for producers, wildcard is not a legal bean type and results in a definition error // see https://docs.jboss.org/cdi/spec/2.0/cdi-spec.html#legal_bean_types - if (typeParams[i].kind().equals(Kind.WILDCARD_TYPE) && producerFieldOrMethod != null) { - LOGGER.info("Producer " + - (producerFieldOrMethod.kind().equals(AnnotationTarget.Kind.FIELD) ? "field " : "method ") + - producerFieldOrMethod + - " contains a parameterized typed with a wildcard. This type is not a legal bean type" + - " according to CDI specification and will be ignored during bean resolution."); - skipThisType = true; - } + // NOTE: wildcard can be nested, such as List> + detectWildcardAndThrow(typeParams[i], producerFieldOrMethod); } if (resolvedTypeVariablesConsumer != null) { Map resolved = new HashMap<>(); @@ -466,9 +483,7 @@ static Set getTypeClosure(ClassInfo classInfo, AnnotationTarget producerFi } resolvedTypeVariablesConsumer.accept(classInfo, resolved); } - if (!skipThisType) { - types.add(ParameterizedType.create(classInfo.name(), typeParams, null)); - } + types.add(ParameterizedType.create(classInfo.name(), typeParams, null)); } // Interfaces for (Type interfaceType : classInfo.interfaceTypes()) { diff --git a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/BeanInfoInjectionsTest.java b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/BeanInfoInjectionsTest.java index 981ae74c5bd4c..ff22bbd568845 100644 --- a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/BeanInfoInjectionsTest.java +++ b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/BeanInfoInjectionsTest.java @@ -42,7 +42,7 @@ public void testInjections() throws IOException { Type listStringType = ParameterizedType.create(name(List.class), new Type[] { Type.create(name(String.class), Kind.CLASS) }, null); - BeanDeployment deployment = BeanProcessor.builder().setBeanArchiveIndex(index).build().getBeanDeployment(); + BeanDeployment deployment = BeanProcessor.builder().setImmutableBeanArchiveIndex(index).build().getBeanDeployment(); deployment.registerCustomContexts(Collections.emptyList()); deployment.registerBeans(Collections.emptyList()); BeanInfo barBean = deployment.getBeans().stream().filter(b -> b.getTarget().get().equals(barClass)).findFirst().get(); diff --git a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/BeanInfoQualifiersTest.java b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/BeanInfoQualifiersTest.java index 6e5203478b4a4..3c95a2c557f76 100644 --- a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/BeanInfoQualifiersTest.java +++ b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/BeanInfoQualifiersTest.java @@ -37,7 +37,7 @@ public void testQualifiers() throws IOException { ClassInfo fooClass = index.getClassByName(fooName); BeanInfo bean = Beans.createClassBean(fooClass, - BeanProcessor.builder().setBeanArchiveIndex(index).build().getBeanDeployment(), + BeanProcessor.builder().setImmutableBeanArchiveIndex(index).build().getBeanDeployment(), null); AnnotationInstance requiredFooQualifier = index.getAnnotations(fooQualifierName).stream() diff --git a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/BeanInfoTypesTest.java b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/BeanInfoTypesTest.java index 75dd27ee2e3e8..bc53674cd78a9 100644 --- a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/BeanInfoTypesTest.java +++ b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/BeanInfoTypesTest.java @@ -37,7 +37,7 @@ public void testResolver() throws IOException { Collection.class, List.class, Iterable.class, Object.class, String.class); - BeanDeployment deployment = BeanProcessor.builder().setBeanArchiveIndex(index).build().getBeanDeployment(); + BeanDeployment deployment = BeanProcessor.builder().setImmutableBeanArchiveIndex(index).build().getBeanDeployment(); DotName fooName = name(Foo.class); ClassInfo fooClass = index.getClassByName(fooName); diff --git a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/TypesTest.java b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/TypesTest.java index b6bcc93594732..eb35c5ff8acb5 100644 --- a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/TypesTest.java +++ b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/TypesTest.java @@ -1,6 +1,7 @@ package io.quarkus.arc.processor; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; @@ -11,11 +12,11 @@ import java.util.Map; import java.util.Set; +import javax.enterprise.inject.spi.DefinitionException; + import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; -import org.jboss.jandex.FieldInfo; import org.jboss.jandex.IndexView; -import org.jboss.jandex.MethodInfo; import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; import org.jboss.jandex.Type.Kind; @@ -26,13 +27,14 @@ public class TypesTest { @Test public void testGetTypeClosure() throws IOException { IndexView index = Basics.index(Foo.class, Baz.class, Producer.class, Object.class, List.class, Collection.class, - Iterable.class); + Iterable.class, Set.class); DotName bazName = DotName.createSimple(Baz.class.getName()); DotName fooName = DotName.createSimple(Foo.class.getName()); DotName producerName = DotName.createSimple(Producer.class.getName()); ClassInfo fooClass = index.getClassByName(fooName); Map> resolvedTypeVariables = new HashMap<>(); - BeanDeployment dummyDeployment = BeanProcessor.builder().setBeanArchiveIndex(index).build().getBeanDeployment(); + BeanDeployment dummyDeployment = BeanProcessor.builder().setImmutableBeanArchiveIndex(index).build() + .getBeanDeployment(); // Baz, Foo, Object Set bazTypes = Types.getTypeClosure(index.getClassByName(bazName), null, @@ -63,18 +65,19 @@ public void testGetTypeClosure() throws IOException { } } ClassInfo producerClass = index.getClassByName(producerName); - String producersName = "produce"; - MethodInfo producerMethod = producerClass.method(producersName); - // Object is the sole type - Set producerMethodTypes = Types.getProducerMethodTypeClosure(producerMethod, - dummyDeployment); - assertEquals(1, producerMethodTypes.size()); + final String producersName = "produce"; + assertThrows(DefinitionException.class, + () -> Types.getProducerMethodTypeClosure(producerClass.method(producersName), dummyDeployment)); + assertThrows(DefinitionException.class, + () -> Types.getProducerFieldTypeClosure(producerClass.field(producersName), dummyDeployment)); + + // now assert the same with nested wildcard + final String nestedWildCardProducersName = "produceNested"; + assertThrows(DefinitionException.class, + () -> Types.getProducerMethodTypeClosure(producerClass.method(nestedWildCardProducersName), dummyDeployment)); + assertThrows(DefinitionException.class, + () -> Types.getProducerFieldTypeClosure(producerClass.field(nestedWildCardProducersName), dummyDeployment)); - // Object is the sole type - FieldInfo producerField = producerClass.field(producersName); - Set producerFieldTypes = Types.getProducerFieldTypeClosure(producerField, - dummyDeployment); - assertEquals(1, producerFieldTypes.size()); } static class Foo { @@ -93,6 +96,12 @@ public List produce() { return null; } + public List> produceNested() { + return null; + } + List produce; + + List> produceNested; } } diff --git a/independent-projects/arc/tests/pom.xml b/independent-projects/arc/tests/pom.xml index 23b332a521341..cb28436b23edf 100644 --- a/independent-projects/arc/tests/pom.xml +++ b/independent-projects/arc/tests/pom.xml @@ -48,7 +48,7 @@ org.jetbrains.kotlin kotlin-stdlib - 1.7.21 + 1.7.22 test diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java index 6d428b83335a6..fda3d072e7158 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java @@ -20,6 +20,7 @@ import org.jboss.jandex.DotName; import org.jboss.jandex.Index; +import org.jboss.jandex.IndexView; import org.jboss.jandex.Indexer; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; @@ -322,14 +323,14 @@ private ClassLoader init(ExtensionContext context) { Arc.shutdown(); // Build index - Index beanArchiveIndex; + IndexView immutableBeanArchiveIndex; try { - beanArchiveIndex = index(beanClasses); + immutableBeanArchiveIndex = BeanArchives.buildImmutableBeanArchiveIndex(index(beanClasses)); } catch (IOException e) { throw new IllegalStateException("Failed to create index", e); } - Index applicationIndex; + IndexView applicationIndex; if (additionalClasses.isEmpty()) { applicationIndex = null; } else { @@ -370,8 +371,9 @@ private ClassLoader init(ExtensionContext context) { BeanProcessor.Builder builder = BeanProcessor.builder() .setName(testClass.getSimpleName()) - .setBeanArchiveIndex(BeanArchives.buildBeanArchiveIndex(getClass().getClassLoader(), - new ConcurrentHashMap<>(), beanArchiveIndex)) + .setImmutableBeanArchiveIndex(immutableBeanArchiveIndex) + .setComputingBeanArchiveIndex(BeanArchives.buildComputingBeanArchiveIndex(getClass().getClassLoader(), + new ConcurrentHashMap<>(), immutableBeanArchiveIndex)) .setApplicationIndex(applicationIndex); if (!resourceAnnotations.isEmpty()) { builder.addResourceAnnotations(resourceAnnotations.stream() diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/NormalScopedSyntheticBeanProducedNullTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/NormalScopedSyntheticBeanProducedNullTest.java new file mode 100644 index 0000000000000..7814f10472cc3 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/NormalScopedSyntheticBeanProducedNullTest.java @@ -0,0 +1,55 @@ +package io.quarkus.arc.test.buildextension.beans; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.CreationException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.processor.BeanRegistrar; +import io.quarkus.arc.test.ArcTestContainer; + +public class NormalScopedSyntheticBeanProducedNullTest { + + public static volatile boolean beanDestroyerInvoked = false; + + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder().beanRegistrars(new TestRegistrar()).build(); + + @Test + public void testCreationException() { + CreationException e = assertThrows(CreationException.class, () -> { + Arc.container().instance(CharSequence.class).get().length(); + }); + assertTrue(e.getMessage().contains("Null contextual instance was produced by a normal scoped synthetic bean"), + e.getMessage()); + } + + static class TestRegistrar implements BeanRegistrar { + + @Override + public void register(RegistrationContext context) { + context.configure(CharSequence.class).types(CharSequence.class).unremovable().scope(ApplicationScoped.class) + .creator(CharSequenceCreator.class).done(); + } + + } + + public static class CharSequenceCreator implements BeanCreator { + + @Override + public CharSequence create(CreationalContext creationalContext, Map params) { + return null; + } + + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/packageprivate/BaseInterface.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/packageprivate/BaseInterface.java new file mode 100644 index 0000000000000..c1ea1982b7adf --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/packageprivate/BaseInterface.java @@ -0,0 +1,7 @@ +package io.quarkus.arc.test.clientproxy.packageprivate; + +// this interface is intentionally package-private +interface BaseInterface { + + String ping(); +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/packageprivate/MyInterface.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/packageprivate/MyInterface.java new file mode 100644 index 0000000000000..7d87e859a45cd --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/packageprivate/MyInterface.java @@ -0,0 +1,5 @@ +package io.quarkus.arc.test.clientproxy.packageprivate; + +public interface MyInterface extends BaseInterface { + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/packageprivate/PackagePrivateInterfaceInHierarchyTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/packageprivate/PackagePrivateInterfaceInHierarchyTest.java new file mode 100644 index 0000000000000..fd3ad09ca61df --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/packageprivate/PackagePrivateInterfaceInHierarchyTest.java @@ -0,0 +1,24 @@ +package io.quarkus.arc.test.clientproxy.packageprivate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.test.ArcTestContainer; +import io.quarkus.arc.test.clientproxy.packageprivate.foo.MyInterface2; +import io.quarkus.arc.test.clientproxy.packageprivate.foo.Producer; + +public class PackagePrivateInterfaceInHierarchyTest { + + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(BaseInterface.class, MyInterface.class, MyInterface2.class, + Producer.class); + + @Test + public void testProducer() { + assertEquals("quarkus", Arc.container().instance(MyInterface2.class).get().ping()); + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/packageprivate/foo/MyInterface2.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/packageprivate/foo/MyInterface2.java new file mode 100644 index 0000000000000..f5fa6bd00d3f4 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/packageprivate/foo/MyInterface2.java @@ -0,0 +1,7 @@ +package io.quarkus.arc.test.clientproxy.packageprivate.foo; + +import io.quarkus.arc.test.clientproxy.packageprivate.MyInterface; + +public interface MyInterface2 extends MyInterface { + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/packageprivate/foo/Producer.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/packageprivate/foo/Producer.java new file mode 100644 index 0000000000000..3301df337c4c8 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/packageprivate/foo/Producer.java @@ -0,0 +1,20 @@ +package io.quarkus.arc.test.clientproxy.packageprivate.foo; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; + +@ApplicationScoped +public class Producer { + + @Produces + @ApplicationScoped + public MyInterface2 myInterface2() { + return new MyInterface2() { + @Override + public String ping() { + return "quarkus"; + } + }; + } + +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/workspace/ArtifactSources.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/workspace/ArtifactSources.java index 5b8e385f08b00..13569de58cf3e 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/workspace/ArtifactSources.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/workspace/ArtifactSources.java @@ -4,13 +4,14 @@ import java.util.Collection; import java.util.List; +import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.paths.EmptyPathTree; import io.quarkus.paths.MultiRootPathTree; import io.quarkus.paths.PathTree; public interface ArtifactSources { - String MAIN = ""; + String MAIN = ArtifactCoords.DEFAULT_CLASSIFIER; String TEST = "tests"; static ArtifactSources main(SourceDir sources, SourceDir resources) { diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java index bfa14a3f460dc..35dfd19b162cb 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java @@ -447,6 +447,15 @@ protected Class findClass(String moduleName, String name) { } } + protected URL findResource(String name) { + return getResource(name); + } + + @Override + protected Enumeration findResources(String name) throws IOException { + return getResources(name); + } + @Override public Class loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/utils/BuildToolHelper.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/utils/BuildToolHelper.java index fe9fd8358f9ae..637db4e7c9bd0 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/utils/BuildToolHelper.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/utils/BuildToolHelper.java @@ -21,7 +21,7 @@ public class BuildToolHelper { private static final Logger log = Logger.getLogger(BuildToolHelper.class); private final static String[] DEVMODE_REQUIRED_TASKS = new String[] { "classes" }; - private final static String[] TEST_REQUIRED_TASKS = new String[] { "classes", "testClasses" }; + private final static String[] TEST_REQUIRED_TASKS = new String[] { "classes", "testClasses", "integrationTestClasses" }; private final static List ENABLE_JAR_PACKAGING = Collections .singletonList("-Dorg.gradle.java.compile-classpath-packaging=true"); @@ -108,6 +108,7 @@ public static ApplicationModel enableGradleAppModelForTest(Path projectRoot) public static ApplicationModel enableGradleAppModel(Path projectRoot, String mode, List jvmArgs, String... tasks) throws IOException, AppModelResolverException { if (isGradleProject(projectRoot)) { + log.infof("Loading Quarkus Gradle application model for %s", projectRoot); final ApplicationModel model = QuarkusGradleModelFactory.create( getBuildFile(projectRoot, BuildTool.GRADLE).toFile(), mode, jvmArgs, tasks); diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsQuarkusExt.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsQuarkusExt.java index cce22dcf20460..29fef873ab273 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsQuarkusExt.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsQuarkusExt.java @@ -2,11 +2,15 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; -import java.util.Properties; +import java.util.Map; +import java.util.StringJoiner; import java.util.stream.Collectors; import io.quarkus.bootstrap.BootstrapConstants; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.DependencyFlags; public class TsQuarkusExt { @@ -16,6 +20,7 @@ public class TsQuarkusExt { protected final TsJar rtContent; protected final PropsBuilder rtDescr = PropsBuilder.newInstance(); private boolean installed; + private Map> flags = new HashMap<>(); public TsQuarkusExt(String artifactId) { this(artifactId, TsArtifact.DEFAULT_VERSION); @@ -30,6 +35,11 @@ public TsQuarkusExt(String artifactId, String version) { rtDescr.set(BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT, deployment.toString()); } + public TsQuarkusExt setDependencyFlag(ArtifactKey dep, int flag) { + this.flags.computeIfAbsent(flag, k -> new ArrayList<>()).add(dep); + return this; + } + public TsQuarkusExt setConditionalDeps(TsQuarkusExt... exts) { final StringBuilder buf = new StringBuilder(); int i = 0; @@ -83,15 +93,25 @@ public void install(TsRepoBuilder repo) { } installed = true; - rtContent.addEntry(rtDescr.build(), BootstrapConstants.DESCRIPTOR_PATH); - if (!extDeps.isEmpty()) { for (TsQuarkusExt e : extDeps) { e.install(repo); } } - Properties props = rtDescr.build(); - rtContent.addEntry(props, BootstrapConstants.DESCRIPTOR_PATH); + for (Map.Entry> e : flags.entrySet()) { + switch (e.getKey()) { + case DependencyFlags.CLASSLOADER_PARENT_FIRST: + final StringJoiner sj = new StringJoiner(","); + for (ArtifactKey k : e.getValue()) { + sj.add(k.toString()); + } + rtDescr.set(BootstrapConstants.PARENT_FIRST_ARTIFACTS, sj.toString()); + break; + default: + throw new RuntimeException("Not yet supported flag " + e.getKey()); + } + } + rtContent.addEntry(rtDescr.build(), BootstrapConstants.DESCRIPTOR_PATH); deployment.install(repo); runtime.install(repo); } diff --git a/independent-projects/bootstrap/gradle-resolver/src/main/java/io/quarkus/bootstrap/resolver/QuarkusGradleModelFactory.java b/independent-projects/bootstrap/gradle-resolver/src/main/java/io/quarkus/bootstrap/resolver/QuarkusGradleModelFactory.java index 6177ef610942f..1a0fce2b7f81b 100644 --- a/independent-projects/bootstrap/gradle-resolver/src/main/java/io/quarkus/bootstrap/resolver/QuarkusGradleModelFactory.java +++ b/independent-projects/bootstrap/gradle-resolver/src/main/java/io/quarkus/bootstrap/resolver/QuarkusGradleModelFactory.java @@ -1,7 +1,6 @@ package io.quarkus.bootstrap.resolver; import java.io.File; -import java.util.Collections; import java.util.List; import org.gradle.tooling.GradleConnector; @@ -14,7 +13,7 @@ public class QuarkusGradleModelFactory { public static ApplicationModel create(File projectDir, String mode, String... tasks) { - return create(projectDir, mode, Collections.emptyList(), tasks); + return create(projectDir, mode, List.of(), tasks); } public static ApplicationModel create(File projectDir, String mode, List jvmArgs, String... tasks) { diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java index 21d10db1cec82..728cddf764235 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java @@ -54,6 +54,7 @@ import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.DependencyFlags; +import io.quarkus.maven.dependency.GACT; import io.quarkus.maven.dependency.ResolvedDependencyBuilder; import io.quarkus.paths.PathList; import io.quarkus.paths.PathTree; @@ -209,21 +210,42 @@ public void resolve(CollectRequest collectRtDepsRequest) throws AppModelResolver buildTreeConsumer); buildDepsVisitor.visit(root); - if (!CONVERGED_TREE_ONLY && collectReloadableModules) { - for (ResolvedDependencyBuilder db : appBuilder.getDependencies()) { - if (db.isFlagSet(DependencyFlags.RELOADABLE | DependencyFlags.VISITED)) { - continue; + for (ExtensionInfo e : allExtensions.values()) { + if (!e.activated) { + continue; + } + e.handleExtensionProperties(); + } + + if (!CONVERGED_TREE_ONLY) { + if (collectReloadableModules) { + for (ResolvedDependencyBuilder db : appBuilder.getDependencies()) { + if (!db.isFlagSet(DependencyFlags.RELOADABLE | DependencyFlags.VISITED)) { + propagateFlags(db, DependencyFlags.RELOADABLE, 0); + } } - clearReloadableFlag(db); + clearVisitedFlag(); } + propagateFlag(DependencyFlags.CLASSLOADER_PARENT_FIRST); + propagateFlag(DependencyFlags.CLASSLOADER_RUNNER_PARENT_FIRST); } + collectPlatformProperties(); + } + + private void propagateFlag(int flag) { for (ResolvedDependencyBuilder db : appBuilder.getDependencies()) { - db.clearFlag(DependencyFlags.VISITED); - appBuilder.addDependency(db); + if (db.isFlagSet(flag) && !db.isFlagSet(DependencyFlags.VISITED)) { + propagateFlags(db, 0, flag); + } } + clearVisitedFlag(); + } - collectPlatformProperties(); + private void clearVisitedFlag() { + for (ResolvedDependencyBuilder db : appBuilder.getDependencies()) { + db.clearFlag(DependencyFlags.VISITED); + } } private void collectPlatformProperties() throws AppModelResolverException { @@ -245,7 +267,7 @@ private void collectPlatformProperties() throws AppModelResolverException { appBuilder.setPlatformImports(platformReleases); } - private void clearReloadableFlag(ResolvedDependencyBuilder db) { + private void propagateFlags(ResolvedDependencyBuilder db, int toClear, int toSet) { final Set deps = artifactDeps.get(db.getArtifactCoords()); if (deps == null || deps.isEmpty()) { return; @@ -255,9 +277,9 @@ private void clearReloadableFlag(ResolvedDependencyBuilder db) { if (dep == null || dep.isFlagSet(DependencyFlags.VISITED)) { continue; } - dep.setFlags(DependencyFlags.VISITED); - dep.clearFlag(DependencyFlags.RELOADABLE); - clearReloadableFlag(dep); + dep.setFlags(DependencyFlags.VISITED | toSet); + dep.clearFlag(toClear); + propagateFlags(dep, toClear, toSet); } } @@ -673,13 +695,79 @@ private class ExtensionInfo { .parseDependencyCondition(props.getProperty(BootstrapConstants.DEPENDENCY_CONDITION)); } + public void handleExtensionProperties() { + for (Map.Entry prop : props.entrySet()) { + if (prop.getValue() == null) { + continue; + } + final String value = prop.getValue().toString(); + if (value.isBlank()) { + continue; + } + final String name = prop.getKey().toString(); + switch (name) { + case ApplicationModelBuilder.PARENT_FIRST_ARTIFACTS: + for (String artifact : value.split(",")) { + final ResolvedDependencyBuilder d = appBuilder.getDependency(new GACT(artifact.split(":"))); + if (d != null) { + d.setFlags(DependencyFlags.CLASSLOADER_PARENT_FIRST); + } + } + break; + case ApplicationModelBuilder.RUNNER_PARENT_FIRST_ARTIFACTS: + for (String artifact : value.split(",")) { + final ResolvedDependencyBuilder d = appBuilder.getDependency(new GACT(artifact.split(":"))); + if (d != null) { + d.setFlags(DependencyFlags.CLASSLOADER_RUNNER_PARENT_FIRST); + } + } + break; + case ApplicationModelBuilder.LESSER_PRIORITY_ARTIFACTS: + String[] artifacts = value.split(","); + for (String artifact : artifacts) { + final ResolvedDependencyBuilder d = appBuilder.getDependency(new GACT(artifact.split(":"))); + if (d != null) { + log.debugf("Extension %s is making %s a lesser priority artifact", runtimeArtifact, artifact); + d.setFlags(DependencyFlags.CLASSLOADER_LESSER_PRIORITY); + } + } + break; + case ApplicationModelBuilder.EXCLUDED_ARTIFACTS: + for (String artifact : value.split(",")) { + appBuilder.addExcludedArtifact(new GACT(artifact.split(":"))); + log.debugf("Extension %s is excluding %s", runtimeArtifact, artifact); + } + break; + default: + if (name.startsWith(ApplicationModelBuilder.REMOVED_RESOURCES_DOT)) { + final String keyStr = name.substring(ApplicationModelBuilder.REMOVED_RESOURCES_DOT.length()); + if (!keyStr.isBlank()) { + ArtifactKey key = null; + try { + key = ArtifactKey.fromString(keyStr); + } catch (IllegalArgumentException e) { + log.warnf("Failed to parse artifact key %s in %s from descriptor of extension %s", keyStr, + name, + runtimeArtifact); + } + if (key != null) { + final Set resources = Set.of(value.split(",")); + appBuilder.addRemovedResources(key, resources); + log.debugf("Extension %s is excluding resources %s from artifact %s", runtimeArtifact, + resources, + key); + } + } + } + } + } + } + void ensureActivated() { if (activated) { return; } activated = true; - appBuilder.handleExtensionProperties(props, runtimeArtifact.toString()); - final String providesCapabilities = props.getProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES); final String requiresCapabilities = props.getProperty(BootstrapConstants.PROP_REQUIRES_CAPABILITIES); if (providesCapabilities != null || requiresCapabilities != null) { diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java index a30aad09d2f53..9e2587ee65a3b 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java @@ -153,13 +153,11 @@ private LocalProject loadAndCacheProject(Path pomFile) throws BootstrapMavenExce } private Model rawModel(Path pomFile) throws BootstrapMavenException { - final Model rawModel = rawModelCache.getOrDefault(pomFile.getParent(), + Model rawModel = rawModelCache.getOrDefault(pomFile.getParent(), modelProvider == null ? null : modelProvider.apply(pomFile.getParent())); - return rawModel == null ? loadAndCacheRawModel(pomFile) : rawModel; - } - - private Model loadAndCacheRawModel(Path pomFile) throws BootstrapMavenException { - final Model rawModel = readModel(pomFile); + if (rawModel == null) { + rawModel = readModel(pomFile); + } rawModelCache.put(pomFile.getParent(), rawModel); return rawModel; } diff --git a/independent-projects/bootstrap/pom.xml b/independent-projects/bootstrap/pom.xml index bf693209f566c..05ffd4aa556e1 100644 --- a/independent-projects/bootstrap/pom.xml +++ b/independent-projects/bootstrap/pom.xml @@ -36,7 +36,7 @@ 11 3.0.0-M7 1.6.8 - 3.0.4 + 3.0.5 3.23.1 @@ -49,7 +49,7 @@ 1.6.3 3.3.4 3.5.1 - 4.4.15 + 4.4.16 1.0.0.Final 2.14.1 1.3.5 diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/Timing.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/Timing.java index cfb18a1a09c74..93f32284fb9d5 100644 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/Timing.java +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/Timing.java @@ -2,6 +2,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.List; import java.util.logging.Handler; import org.jboss.logging.Logger; @@ -88,8 +89,8 @@ public static void restart(ClassLoader cl) { } } - public static void printStartupTime(String name, String version, String quarkusVersion, String features, String profile, - boolean liveCoding, boolean anc) { + public static void printStartupTime(String name, String version, String quarkusVersion, String features, + List profiles, boolean liveCoding, boolean anc) { Timing t = get(anc); final long bootTimeNanoSeconds = System.nanoTime() - t.bootStartTime; final Logger logger = Logger.getLogger("io.quarkus"); @@ -105,7 +106,8 @@ public static void printStartupTime(String name, String version, String quarkusV logger.infof("%s %s %s (powered by Quarkus %s) started in %ss. %s", name, version, nativeOrJvm, quarkusVersion, secondsRepresentation, t.httpServerInfo); } - logger.infof("Profile %s activated. %s", profile, liveCoding ? "Live Coding activated." : ""); + logger.infof("Profile%s %s activated. %s", profiles.size() > 1 ? "s" : "", String.join(",", profiles), + liveCoding ? "Live Coding activated." : ""); logger.infof("Installed features: [%s]", features); t.bootStartTime = -1; } diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java index b0826982da99e..57799bf42d767 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java @@ -27,8 +27,6 @@ private Descriptors() { boolean.class, Class.class); public static final MethodDescriptor GET_CLASS = MethodDescriptor.ofMethod(Object.class, "getClass", Class.class); public static final MethodDescriptor COLLECTION_SIZE = MethodDescriptor.ofMethod(Collection.class, "size", int.class); - public static final MethodDescriptor EQUALS = MethodDescriptor.ofMethod(Object.class, "equals", boolean.class, - Object.class); public static final MethodDescriptor GET_NAME = MethodDescriptor.ofMethod(EvalContext.class, "getName", String.class); public static final MethodDescriptor GET_BASE = MethodDescriptor.ofMethod(EvalContext.class, "getBase", Object.class); public static final MethodDescriptor GET_PARAMS = MethodDescriptor.ofMethod(EvalContext.class, "getParams", List.class); diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java index e6da69c6ca21f..145514ca1dd66 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java @@ -42,6 +42,7 @@ import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.FunctionCreator; +import io.quarkus.gizmo.Gizmo; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; @@ -393,14 +394,12 @@ private void implementAppliesTo(ClassCreator valueResolver, MethodInfo method, S // Any of the name matches BytecodeCreator namesMatch = appliesTo.createScope(); for (String match : matchNames) { - ResultHandle nameTest = namesMatch.invokeVirtualMethod(Descriptors.EQUALS, name, - namesMatch.load(match)); + ResultHandle nameTest = Gizmo.equals(namesMatch, name, namesMatch.load(match)); namesMatch.ifTrue(nameTest).trueBranch().breakScope(namesMatch); } namesMatch.returnValue(namesMatch.load(false)); } else { - ResultHandle nameTest = appliesTo.invokeVirtualMethod(Descriptors.EQUALS, name, - appliesTo.load(matchName)); + ResultHandle nameTest = Gizmo.equals(appliesTo, name, appliesTo.load(matchName)); BytecodeCreator nameNotMatched = appliesTo.ifFalse(nameTest).trueBranch(); nameNotMatched.returnValue(nameNotMatched.load(false)); } @@ -634,15 +633,12 @@ private BytecodeCreator createNamespaceExtensionMatchScope(BytecodeCreator bytec // Any of the name matches BytecodeCreator namesMatch = matchScope.createScope(); for (String match : matchNames) { - ResultHandle nameTest = namesMatch.invokeVirtualMethod(Descriptors.EQUALS, name, - namesMatch.load(match)); + ResultHandle nameTest = Gizmo.equals(namesMatch, name, namesMatch.load(match)); namesMatch.ifTrue(nameTest).trueBranch().breakScope(namesMatch); } namesMatch.breakScope(matchScope); } else { - matchScope.ifTrue(matchScope.invokeVirtualMethod(Descriptors.EQUALS, - matchScope.load(matchName), - name)) + matchScope.ifTrue(Gizmo.equals(matchScope, matchScope.load(matchName), name)) .falseBranch().breakScope(matchScope); } } diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java index 8a2573b58ca7e..a07b3f30a1a92 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ValueResolverGenerator.java @@ -44,6 +44,7 @@ import io.quarkus.gizmo.DescriptorUtils; import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.FunctionCreator; +import io.quarkus.gizmo.Gizmo; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; @@ -171,7 +172,7 @@ private void generate(DotName className, int priority) { AnnotationInstance templateData = nameToTemplateData.get(className); if (templateData == null) { // @TemplateData declared on the class - for (AnnotationInstance annotation : clazz.classAnnotations()) { + for (AnnotationInstance annotation : clazz.declaredAnnotations()) { if (annotation.name().equals(TEMPLATE_DATA)) { AnnotationValue targetValue = annotation.value(TARGET); if (targetValue == null || targetValue.asClass().name().equals(className)) { @@ -306,136 +307,136 @@ private boolean implementResolve(ClassCreator valueResolver, String clazzName, C fields.add(field); } } - if (!fields.isEmpty()) { - BytecodeCreator zeroParamsBranch = resolve.ifNonZero(paramsCount).falseBranch(); - for (FieldInfo field : fields) { - String getterName = fieldToGetterFun != null ? fieldToGetterFun.apply(field) : null; - if (getterName != null && noneMethodMatches(methods, getterName)) { - LOGGER.debugf("Forced getter added: %s", field); - BytecodeCreator getterMatch = zeroParamsBranch.createScope(); - // Match the getter name - BytecodeCreator notMatched = getterMatch.ifNonZero(getterMatch.invokeVirtualMethod(Descriptors.EQUALS, - getterMatch.load(getterName), - name)) - .falseBranch(); - // Match the property name - notMatched.ifNonZero(notMatched.invokeVirtualMethod(Descriptors.EQUALS, - notMatched.load(field.name()), - name)).falseBranch().breakScope(getterMatch); - ResultHandle value = getterMatch.invokeVirtualMethod( - MethodDescriptor.ofMethod(clazz.name().toString(), getterName, - DescriptorUtils.typeToString(field.type())), - base); - getterMatch.returnValue(getterMatch.invokeStaticMethod(Descriptors.COMPLETED_STAGE, value)); - } else { - LOGGER.debugf("Field added: %s", field); - // Match field name - BytecodeCreator fieldMatch = zeroParamsBranch - .ifNonZero( - zeroParamsBranch.invokeVirtualMethod(Descriptors.EQUALS, - resolve.load(field.name()), name)) - .trueBranch(); - ResultHandle value = fieldMatch - .readInstanceField(FieldDescriptor.of(clazzName, field.name(), field.type().name().toString()), - base); - fieldMatch.returnValue(fieldMatch.invokeStaticMethod(Descriptors.COMPLETED_STAGE, value)); - } - } - } if (methods.isEmpty() && fields.isEmpty()) { + // No members return false; } - if (!methods.isEmpty()) { - // name, number of params -> list of methods - Map> matches = new HashMap<>(); - Map> varargsMatches = new HashMap<>(); + // Collect methods + List noParamMethods = new ArrayList<>(); + // name, number of params -> list of methods + Map> matches = new HashMap<>(); + Map> varargsMatches = new HashMap<>(); - for (MethodKey methodKey : methods) { - MethodInfo method = methodKey.method; - List methodParams = method.parameterTypes(); - if (methodParams.isEmpty()) { - // No params - just invoke the method - LOGGER.debugf("Method added %s", method); - try (BytecodeCreator matchScope = createMatchScope(resolve, method.name(), 0, method.returnType(), name, - params, paramsCount)) { - ResultHandle ret; - boolean hasCompletionStage = !skipMemberType(method.returnType()) - && hasCompletionStageInTypeClosure(index.getClassByName(method.returnType().name()), index); - ResultHandle invokeRet; - if (Modifier.isInterface(clazz.flags())) { - invokeRet = matchScope.invokeInterfaceMethod(MethodDescriptor.of(method), base); - } else { - invokeRet = matchScope.invokeVirtualMethod(MethodDescriptor.of(method), base); - } - if (hasCompletionStage) { - ret = invokeRet; - } else { - ret = matchScope.invokeStaticMethod(Descriptors.COMPLETED_STAGE, invokeRet); - } - matchScope.returnValue(ret); - } - } else { - // Collect methods with params - Match match = new Match(method.name(), method.parametersCount()); - List methodMatches = matches.get(match); + for (MethodKey methodKey : methods) { + MethodInfo method = methodKey.method; + if (method.parametersCount() == 0) { + noParamMethods.add(methodKey); + } else { + Match match = new Match(method.name(), method.parametersCount()); + List methodMatches = matches.get(match); + if (methodMatches == null) { + methodMatches = new ArrayList<>(); + matches.put(match, methodMatches); + } + methodMatches.add(method); + + if (isVarArgs(method)) { + // The last argument is a sequence of arguments -> match name and min number of params + // getList(int age, String... names) -> "getList", 1 + match = new Match(method.name(), method.parametersCount() - 1); + methodMatches = varargsMatches.get(match); if (methodMatches == null) { methodMatches = new ArrayList<>(); - matches.put(match, methodMatches); + varargsMatches.put(match, methodMatches); } methodMatches.add(method); - - if (isVarArgs(method)) { - // The last argument is a sequence of arguments -> match name and min number of params - // getList(int age, String... names) -> "getList", 1 - match = new Match(method.name(), method.parametersCount() - 1); - methodMatches = varargsMatches.get(match); - if (methodMatches == null) { - methodMatches = new ArrayList<>(); - varargsMatches.put(match, methodMatches); - } - methodMatches.add(method); - } } } + } - // Match methods by name and number of params - for (Entry> entry : matches.entrySet()) { - Match match = entry.getKey(); - - // The set of matching methods is made up of the methods matching the name and number of params + varargs methods matching the name and minimal number of params - // For example both the methods getList(int age, String... names) and getList(int age) match "getList" and 1 param - Set methodMatches = new HashSet<>(entry.getValue()); - varargsMatches.entrySet().stream() - .filter(e -> e.getKey().name.equals(match.name) && e.getKey().paramsCount >= match.paramsCount) - .forEach(e -> methodMatches.addAll(e.getValue())); + BytecodeCreator zeroParamsBranch = resolve.ifZero(paramsCount).trueBranch(); + + for (FieldInfo field : fields) { + String getterName = fieldToGetterFun != null ? fieldToGetterFun.apply(field) : null; + if (getterName != null && noneMethodMatches(methods, getterName)) { + LOGGER.debugf("Forced getter added: %s", field); + BytecodeCreator getterMatch = zeroParamsBranch.createScope(); + // Match the getter name + BytecodeCreator notMatched = getterMatch.ifTrue(Gizmo.equals(getterMatch, getterMatch.load(getterName), + name)).falseBranch(); + // Match the property name + notMatched.ifTrue(Gizmo.equals(notMatched, notMatched.load(field.name()), + name)).falseBranch().breakScope(getterMatch); + ResultHandle value = getterMatch.invokeVirtualMethod( + MethodDescriptor.ofMethod(clazz.name().toString(), getterName, + DescriptorUtils.typeToString(field.type())), + base); + getterMatch.returnValue(getterMatch.invokeStaticMethod(Descriptors.COMPLETED_STAGE, value)); + } else { + LOGGER.debugf("Field added: %s", field); + // Match field name + BytecodeCreator fieldMatch = zeroParamsBranch + .ifTrue(Gizmo.equals(zeroParamsBranch, resolve.load(field.name()), name)) + .trueBranch(); + ResultHandle value = fieldMatch + .readInstanceField(FieldDescriptor.of(clazzName, field.name(), field.type().name().toString()), + base); + fieldMatch.returnValue(fieldMatch.invokeStaticMethod(Descriptors.COMPLETED_STAGE, value)); + } + } - if (methodMatches.size() == 1) { - // Single method matches the name and number of params - matchMethod(methodMatches.iterator().next(), clazz, resolve, base, name, params, paramsCount, evalContext); + for (MethodKey methodKey : noParamMethods) { + // No params - just invoke the method if the name matches + MethodInfo method = methodKey.method; + LOGGER.debugf("No-args method added %s", method); + try (BytecodeCreator matchScope = createMatchScope(zeroParamsBranch, method.name(), -1, method.returnType(), name, + params, paramsCount)) { + ResultHandle ret; + boolean hasCompletionStage = !skipMemberType(method.returnType()) + && hasCompletionStageInTypeClosure(index.getClassByName(method.returnType().name()), index); + ResultHandle invokeRet; + if (Modifier.isInterface(clazz.flags())) { + invokeRet = matchScope.invokeInterfaceMethod(MethodDescriptor.of(method), base); } else { - // Multiple methods match the name and number of params - matchMethods(match.name, match.paramsCount, methodMatches, clazz, resolve, base, name, - params, paramsCount, evalContext); + invokeRet = matchScope.invokeVirtualMethod(MethodDescriptor.of(method), base); + } + if (hasCompletionStage) { + ret = invokeRet; + } else { + ret = matchScope.invokeStaticMethod(Descriptors.COMPLETED_STAGE, invokeRet); } + matchScope.returnValue(ret); } + } - // For varargs methods we also need to match name and any number of params - Map> varargsMap = new HashMap<>(); - for (Entry> entry : varargsMatches.entrySet()) { - List list = varargsMap.get(entry.getKey().name); - if (list == null) { - list = new ArrayList<>(); - varargsMap.put(entry.getKey().name, list); - } - list.addAll(entry.getValue()); + // Match methods by name and number of params + for (Entry> entry : matches.entrySet()) { + Match match = entry.getKey(); + + // The set of matching methods is made up of the methods matching the name and number of params + varargs methods matching the name and minimal number of params + // For example both the methods getList(int age, String... names) and getList(int age) match "getList" and 1 param + Set methodMatches = new HashSet<>(entry.getValue()); + varargsMatches.entrySet().stream() + .filter(e -> e.getKey().name.equals(match.name) && e.getKey().paramsCount >= match.paramsCount) + .forEach(e -> methodMatches.addAll(e.getValue())); + + if (methodMatches.size() == 1) { + // Single method matches the name and number of params + matchMethod(methodMatches.iterator().next(), clazz, resolve, base, name, params, paramsCount, evalContext); + } else { + // Multiple methods match the name and number of params + matchMethods(match.name, match.paramsCount, methodMatches, clazz, resolve, base, name, + params, paramsCount, evalContext); } - for (Entry> entry : varargsMap.entrySet()) { - matchMethods(entry.getKey(), Integer.MIN_VALUE, entry.getValue(), clazz, resolve, base, name, params, - paramsCount, evalContext); + } + + // For varargs methods we also need to match name and any number of params + Map> varargsMap = new HashMap<>(); + for (Entry> entry : varargsMatches.entrySet()) { + List list = varargsMap.get(entry.getKey().name); + if (list == null) { + list = new ArrayList<>(); + varargsMap.put(entry.getKey().name, list); } + list.addAll(entry.getValue()); + } + for (Entry> entry : varargsMap.entrySet()) { + matchMethods(entry.getKey(), Integer.MIN_VALUE, entry.getValue(), clazz, resolve, base, name, params, + paramsCount, evalContext); } + resolve.returnValue(resolve.invokeStaticMethod(Descriptors.RESULTS_NOT_FOUND_EC, evalContext)); return true; } @@ -477,9 +478,7 @@ private boolean implementNamespaceResolve(ClassCreator valueResolver, String cla LOGGER.debugf("Static field added: %s", field); // Match field name BytecodeCreator fieldMatch = zeroParamsBranch - .ifNonZero( - zeroParamsBranch.invokeVirtualMethod(Descriptors.EQUALS, - resolve.load(field.name()), name)) + .ifTrue(Gizmo.equals(zeroParamsBranch, resolve.load(field.name()), name)) .trueBranch(); ResultHandle value = fieldMatch .readStaticField(FieldDescriptor.of(clazzName, field.name(), field.type().name().toString())); @@ -899,19 +898,21 @@ private BytecodeCreator createMatchScope(BytecodeCreator bytecodeCreator, String Type returnType, ResultHandle name, ResultHandle params, ResultHandle paramsCount) { BytecodeCreator matchScope = bytecodeCreator.createScope(); + // Match name - BytecodeCreator notMatched = matchScope.ifTrue(matchScope.invokeVirtualMethod(Descriptors.EQUALS, - matchScope.load(methodName), - name)) - .falseBranch(); - // Match the property name for getters, ie. "foo" for "getFoo" - if (methodParams == 0 && isGetterName(methodName, returnType)) { - notMatched.ifNonZero(notMatched.invokeVirtualMethod(Descriptors.EQUALS, - notMatched.load(getPropertyName(methodName)), - name)).falseBranch().breakScope(matchScope); + if (methodParams <= 0 && isGetterName(methodName, returnType)) { + // Getter found - match the property name first + BytecodeCreator notMatched = matchScope + .ifTrue(Gizmo.equals(matchScope, matchScope.load(getPropertyName(methodName)), name)) + .falseBranch(); + // If the property does not match then use the exact method name + notMatched.ifTrue(Gizmo.equals(notMatched, matchScope.load(methodName), name)).falseBranch().breakScope(matchScope); } else { - notMatched.breakScope(matchScope); + // No getter - only match the exact method name + matchScope.ifTrue(Gizmo.equals(matchScope, matchScope.load(methodName), name)) + .falseBranch().breakScope(matchScope); } + // Match number of params if (methodParams >= 0) { matchScope.ifIntegerEqual(matchScope.load(methodParams), paramsCount).falseBranch().breakScope(matchScope); diff --git a/independent-projects/qute/pom.xml b/independent-projects/qute/pom.xml index 856eefad28561..9deb1e5dff1aa 100644 --- a/independent-projects/qute/pom.xml +++ b/independent-projects/qute/pom.xml @@ -42,7 +42,7 @@ 11 5.9.1 3.23.1 - 3.0.4 + 3.0.5 1.4.0.Final 3.5.0.Final 3.0.0-M7 diff --git a/independent-projects/resteasy-reactive/build-support/src/main/java/org/jboss/resteasy/reactive/build/support/AllWriteableMessageBodyWriterByteBuddyPlugin.java b/independent-projects/resteasy-reactive/build-support/src/main/java/org/jboss/resteasy/reactive/build/support/AllWriteableMessageBodyWriterByteBuddyPlugin.java index 00da666a6c451..40495c4ac1100 100644 --- a/independent-projects/resteasy-reactive/build-support/src/main/java/org/jboss/resteasy/reactive/build/support/AllWriteableMessageBodyWriterByteBuddyPlugin.java +++ b/independent-projects/resteasy-reactive/build-support/src/main/java/org/jboss/resteasy/reactive/build/support/AllWriteableMessageBodyWriterByteBuddyPlugin.java @@ -9,6 +9,7 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; +import javax.ws.rs.core.MediaType; import javax.ws.rs.ext.MessageBodyWriter; import org.jboss.resteasy.reactive.common.types.AllWriteableMarker; @@ -84,15 +85,15 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str MethodVisitor superMethodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); if (name.equals("isWriteable")) { // RR isWriteable - if ("(Ljava/lang/Class;Ljava/lang/reflect/Type;Lorg/jboss/resteasy/reactive/server/spi/ResteasyReactiveResourceInfo;Ljavax/ws/rs/core/MediaType;)Z" - .equals(descriptor)) { + if (("(Ljava/lang/Class;Ljava/lang/reflect/Type;Lorg/jboss/resteasy/reactive/server/spi/ResteasyReactiveResourceInfo;L" + + MediaType.class.getName().replace('.', '/') + ";)Z").equals(descriptor)) { AtomicBoolean rrResult = new AtomicBoolean(false); rrIsWritableResult = Optional.of(rrResult); return new MessageBodyWriterIsWriteableMethodVisitor(new CodeSizeEvaluator(superMethodVisitor), rrResult); } // JAX-RS isWriteable - else if ("(Ljava/lang/Class;Ljava/lang/reflect/Type;[Ljava/lang/annotation/Annotation;Ljavax/ws/rs/core/MediaType;)Z" - .equals(descriptor)) { + else if (("(Ljava/lang/Class;Ljava/lang/reflect/Type;[Ljava/lang/annotation/Annotation;L" + + MediaType.class.getName().replace('.', '/') + ";)Z").equals(descriptor)) { AtomicBoolean standardResult = new AtomicBoolean(false); jaxRSIsWritableResult = Optional.of(standardResult); return new MessageBodyWriterIsWriteableMethodVisitor(new CodeSizeEvaluator(superMethodVisitor), diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java index ba58ddbc17f5b..6d7951ed1f80d 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java @@ -11,6 +11,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import javax.ws.rs.core.MediaType; @@ -115,7 +116,8 @@ protected ResourceMethod createResourceMethod(MethodInfo info, ClassInfo actualE } @Override - protected boolean handleBeanParam(ClassInfo actualEndpointInfo, Type paramType, MethodParameter[] methodParameters, int i) { + protected boolean handleBeanParam(ClassInfo actualEndpointInfo, Type paramType, MethodParameter[] methodParameters, int i, + Set fileFormNames) { ClassInfo beanParamClassInfo = index.getClassByName(paramType.name()); methodParameters[i] = parseClientBeanParam(beanParamClassInfo, index); @@ -127,15 +129,18 @@ private MethodParameter parseClientBeanParam(ClassInfo beanParamClassInfo, Index return new ClientBeanParamInfo(items, beanParamClassInfo.name().toString()); } + @Override protected InjectableBean scanInjectableBean(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo, Map existingConverters, AdditionalReaders additionalReaders, Map injectableBeans, boolean hasRuntimeConverters) { throw new RuntimeException("Injectable beans not supported in client"); } + @Override protected MethodParameter createMethodParameter(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo, boolean encoded, Type paramType, ClientIndexedParam parameterResult, String name, String defaultValue, ParameterType type, - String elementType, boolean single, String signature) { + String elementType, boolean single, String signature, + Set fileFormNames) { DeclaredTypes declaredTypes = getDeclaredTypes(paramType, currentClassInfo, actualEndpointInfo); String mimePart = getPartMime(parameterResult.getAnns()); String partFileName = getPartFileName(parameterResult.getAnns()); @@ -143,7 +148,7 @@ protected MethodParameter createMethodParameter(ClassInfo currentClassInfo, Clas elementType, declaredTypes.getDeclaredType(), declaredTypes.getDeclaredUnresolvedType(), signature, type, single, defaultValue, parameterResult.isObtainedAsCollection(), parameterResult.isOptional(), encoded, - mimePart, partFileName); + mimePart, partFileName, null); } private String getPartFileName(Map annotations) { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java index 2bf3f434ecb41..8d1d7d62c3cbb 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java @@ -20,7 +20,6 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; import javax.ws.rs.core.Variant; import org.jboss.logging.Logger; @@ -244,11 +243,6 @@ public void handle(HttpClientResponse clientResponse) { } } - if (Response.Status.Family.familyOf(status) != Response.Status.Family.SUCCESSFUL) { - httpClientRequest.connection().close(); - requestContext.resume(); - } - if (isResponseMultipart(requestContext)) { QuarkusMultipartResponseDecoder multipartDecoder = new QuarkusMultipartResponseDecoder( clientResponse); @@ -372,16 +366,17 @@ public void handle(Buffer buffer) { requestContext.resume(t); } } - }).onFailure(new Handler<>() { - @Override - public void handle(Throwable failure) { - if (failure instanceof IOException) { - requestContext.resume(new ProcessingException(failure)); - } else { - requestContext.resume(failure); - } - } - }); + }) + .onFailure(new Handler<>() { + @Override + public void handle(Throwable failure) { + if (failure instanceof IOException) { + requestContext.resume(new ProcessingException(failure)); + } else { + requestContext.resume(failure); + } + } + }); } private boolean isResponseMultipart(RestClientRequestContext requestContext) { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java index 9f6e50abe605d..9d323b012a410 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java @@ -59,6 +59,7 @@ public class ClientBuilderImpl extends ClientBuilder { private boolean followRedirects; private boolean trustAll; + private boolean verifyHost; private LoggingScope loggingScope; private Integer loggingBodySize = 100; @@ -178,6 +179,7 @@ public ClientImpl build() { HttpClientOptions options = Optional.ofNullable(configuration.getFromContext(HttpClientOptions.class)) .orElseGet(HttpClientOptions::new); + options.setVerifyHost(verifyHost); if (trustAll) { options.setTrustAll(true); options.setVerifyHost(false); @@ -347,6 +349,11 @@ public ClientBuilderImpl trustAll(boolean trustAll) { return this; } + public ClientBuilderImpl verifyHost(boolean verifyHost) { + this.verifyHost = verifyHost; + return this; + } + public ClientBuilderImpl setUserAgent(String userAgent) { this.userAgent = userAgent; return this; diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java index 9d30b7b60041e..b279df0c8784b 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java @@ -94,6 +94,7 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; import javax.enterprise.inject.spi.DeploymentException; import javax.ws.rs.container.AsyncResponse; @@ -232,6 +233,8 @@ public abstract class EndpointIndexer> isDisabledCreator; + protected EndpointIndexer(Builder builder) { this.index = builder.index; this.applicationIndex = builder.applicationIndex; @@ -253,6 +256,7 @@ protected EndpointIndexer(Builder builder) { this.parameterContainerTypes = builder.parameterContainerTypes; this.multipartReturnTypeIndexerExtension = builder.multipartReturnTypeIndexerExtension; this.targetJavaVersion = builder.targetJavaVersion; + this.isDisabledCreator = builder.isDisabledCreator; } public Optional createEndpoints(ClassInfo classInfo, boolean considerApplication) { @@ -304,6 +308,10 @@ public Optional createEndpoints(ClassInfo classInfo, boolean cons clazz.setPerRequestResource(true); } + if (isDisabledCreator != null) { + clazz.setIsDisabled(isDisabledCreator.apply(classInfo)); + } + return Optional.of(clazz); } catch (Exception e) { if (Modifier.isInterface(classInfo.flags()) || Modifier.isAbstract(classInfo.flags())) { @@ -570,7 +578,9 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf basicResourceClassInfo.getConsumes()); boolean suspended = false; boolean sse = false; - boolean formParamRequired = false; + boolean formParamRequired = getAnnotationStore().getAnnotation(currentMethodInfo, + ResteasyReactiveDotNames.WITH_FORM_READ) != null; + Set fileFormNames = new HashSet<>(); Type bodyParamType = null; TypeArgMapper typeArgMapper = new TypeArgMapper(currentMethodInfo.declaringClass(), index); for (int i = 0; i < methodParameters.length; ++i) { @@ -604,12 +614,12 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf } methodParameters[i] = createMethodParameter(currentClassInfo, actualEndpointInfo, encoded, paramType, parameterResult, name, defaultValue, type, elementType, single, - AsmUtil.getSignature(paramType, typeArgMapper)); + AsmUtil.getSignature(paramType, typeArgMapper), fileFormNames); if (type == ParameterType.BEAN || type == ParameterType.MULTI_PART_FORM) { // transform the bean param - formParamRequired |= handleBeanParam(actualEndpointInfo, paramType, methodParameters, i); + formParamRequired |= handleBeanParam(actualEndpointInfo, paramType, methodParameters, i, fileFormNames); } else if (type == ParameterType.FORM) { formParamRequired = true; } @@ -731,6 +741,7 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf .setSse(sse) .setStreamElementType(streamElementType) .setFormParamRequired(formParamRequired) + .setFileFormNames(fileFormNames) .setParameters(methodParameters) .setSimpleReturnType( toClassName(currentMethodInfo.returnType(), currentClassInfo, actualEndpointInfo, index)) @@ -885,7 +896,7 @@ private String determineReturnType(Type returnType, TypeArgMapper typeArgMapper, } protected abstract boolean handleBeanParam(ClassInfo actualEndpointInfo, Type paramType, MethodParameter[] methodParameters, - int i); + int i, Set fileFormNames); protected void handleAdditionalMethodProcessing(METHOD method, ClassInfo currentClassInfo, MethodInfo info, AnnotationStore annotationStore) { @@ -901,7 +912,8 @@ protected abstract InjectableBean scanInjectableBean(ClassInfo currentClassInfo, protected abstract MethodParameter createMethodParameter(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo, boolean encoded, Type paramType, PARAM parameterResult, String name, String defaultValue, - ParameterType type, String elementType, boolean single, String signature); + ParameterType type, String elementType, boolean single, String signature, + Set fileFormNames); private String[] applyDefaultProduces(String[] produces, Type nonAsyncReturnType, DotName httpMethod) { @@ -1500,6 +1512,15 @@ protected String getPartMime(Map annotations) { return mimeType; } + protected String getSeparator(Map annotations) { + AnnotationInstance separator = annotations.get(ResteasyReactiveDotNames.SEPARATOR); + String result = null; + if (separator != null && separator.value() != null) { + result = separator.value().asString(); + } + return result; + } + @SuppressWarnings({ "unchecked", "rawtypes" }) public static abstract class Builder, B extends Builder, METHOD extends ResourceMethod> { private Function> factoryCreator; @@ -1529,6 +1550,8 @@ public boolean handleMultipartForReturnType(AdditionalWriters additionalWriters, }; private TargetJavaVersion targetJavaVersion = new TargetJavaVersion.Unknown(); + private Function> isDisabledCreator = null; + public B setMultipartReturnTypeIndexerExtension(MultipartReturnTypeIndexerExtension multipartReturnTypeHandler) { this.multipartReturnTypeIndexerExtension = multipartReturnTypeHandler; return (B) this; @@ -1639,6 +1662,12 @@ public B setTargetJavaVersion(TargetJavaVersion targetJavaVersion) { return (B) this; } + public B setIsDisabledCreator( + Function> isDisabledCreator) { + this.isDisabledCreator = isDisabledCreator; + return (B) this; + } + public abstract T build(); } diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java index c86e07b6d04ac..ab185a22ab89f 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java @@ -92,6 +92,7 @@ import org.jboss.resteasy.reactive.RestResponse; import org.jboss.resteasy.reactive.RestSseElementType; import org.jboss.resteasy.reactive.RestStreamElementType; +import org.jboss.resteasy.reactive.Separator; import org.reactivestreams.Publisher; import io.smallrye.common.annotation.Blocking; @@ -136,6 +137,7 @@ public final class ResteasyReactiveDotNames { public static final DotName MULTI_PART_FORM_PARAM = DotName.createSimple(MultipartForm.class.getName()); public static final DotName PART_TYPE_NAME = DotName.createSimple(PartType.class.getName()); public static final DotName PART_FILE_NAME = DotName.createSimple(PartFilename.class.getName()); + public static final DotName SEPARATOR = DotName.createSimple(Separator.class.getName()); public static final DotName REST_MATRIX_PARAM = DotName.createSimple(RestMatrix.class.getName()); public static final DotName REST_COOKIE_PARAM = DotName.createSimple(RestCookie.class.getName()); public static final DotName GET = DotName.createSimple(javax.ws.rs.GET.class.getName()); @@ -259,6 +261,9 @@ public final class ResteasyReactiveDotNames { public static final DotName RESTEASY_REACTIVE_CONTAINER_REQUEST_CONTEXT = DotName .createSimple("org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext"); + public static final DotName WITH_FORM_READ = DotName + .createSimple("org.jboss.resteasy.reactive.server.WithFormRead"); + public static final DotName OBJECT = DotName.createSimple(Object.class.getName()); public static final DotName CONTINUATION = DotName.createSimple("kotlin.coroutines.Continuation"); diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/Separator.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/Separator.java new file mode 100644 index 0000000000000..bee058ff3d280 --- /dev/null +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/Separator.java @@ -0,0 +1,18 @@ +package org.jboss.resteasy.reactive; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; + +/** + * When used on a {@link List} parameter annotated with {@link RestQuery}, RESTEasy Reactive will split the value of the + * parameter (using the value of the annotation) and populate the list with those values. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.PARAMETER, ElementType.FIELD }) +public @interface Separator { + + String value(); +} diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/BeanParamInfo.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/BeanParamInfo.java index e9dcd4d9103c0..0040b7f9f304c 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/BeanParamInfo.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/BeanParamInfo.java @@ -1,9 +1,13 @@ package org.jboss.resteasy.reactive.common.model; +import java.util.HashSet; +import java.util.Set; + public class BeanParamInfo implements InjectableBean { private boolean isFormParamRequired; private boolean isInjectionRequired; private int fieldExtractorsCount; + private Set fileFormNames = new HashSet<>(); @Override public boolean isFormParamRequired() { @@ -36,4 +40,14 @@ public int getFieldExtractorsCount() { public void setFieldExtractorsCount(int fieldExtractorsCount) { this.fieldExtractorsCount = fieldExtractorsCount; } + + @Override + public Set getFileFormNames() { + return fileFormNames; + } + + @Override + public void setFileFormNames(Set fileFormNames) { + this.fileFormNames = fileFormNames; + } } diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/InjectableBean.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/InjectableBean.java index aea871820d09c..e659e6bf50608 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/InjectableBean.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/InjectableBean.java @@ -1,5 +1,7 @@ package org.jboss.resteasy.reactive.common.model; +import java.util.Set; + /** * Class that represents information about injectable beans as we scan them, such as * resource endpoint beans, or BeanParam classes. @@ -25,4 +27,8 @@ public interface InjectableBean { int getFieldExtractorsCount(); void setFieldExtractorsCount(int fieldExtractorsCount); + + Set getFileFormNames(); + + void setFileFormNames(Set fileFormNames); } diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java index 3490ee77bde90..cf86ba52d4261 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java @@ -25,6 +25,7 @@ public class MethodParameter { private boolean isObtainedAsCollection; public String mimeType; public String partFileName; + public String separator; public MethodParameter() { } @@ -33,7 +34,7 @@ public MethodParameter(String name, String type, String declaredType, String dec ParameterType parameterType, boolean single, String defaultValue, boolean isObtainedAsCollection, boolean optional, boolean encoded, - String mimeType, String partFileName) { + String mimeType, String partFileName, String separator) { this.name = name; this.type = type; this.declaredType = declaredType; @@ -47,6 +48,7 @@ public MethodParameter(String name, String type, String declaredType, String dec this.encoded = encoded; this.mimeType = mimeType; this.partFileName = partFileName; + this.separator = separator; } public String getName() { diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceClass.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceClass.java index 957b6ca61b168..aaf593e8ac8b3 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceClass.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceClass.java @@ -45,6 +45,8 @@ public class ResourceClass { */ private Map classLevelExceptionMappers = new HashMap<>(); + private Supplier isDisabled; + public String getClassName() { return className; } @@ -114,6 +116,14 @@ public void setClassLevelExceptionMappers(Map classLevelExceptio this.classLevelExceptionMappers = classLevelExceptionMappers; } + public Supplier getIsDisabled() { + return isDisabled; + } + + public void setIsDisabled(Supplier isDisabled) { + this.isDisabled = isDisabled; + } + @SuppressWarnings({ "rawtypes", "unchecked" }) public Map, ResourceExceptionMapper> resourceExceptionMapper() { if (classLevelExceptionMappers.isEmpty()) { diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceExceptionMapper.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceExceptionMapper.java index 2d695d3c645ab..705e560e66530 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceExceptionMapper.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceExceptionMapper.java @@ -1,16 +1,20 @@ package org.jboss.resteasy.reactive.common.model; +import java.util.function.Supplier; + import javax.ws.rs.Priorities; import javax.ws.rs.ext.ExceptionMapper; import org.jboss.resteasy.reactive.spi.BeanFactory; -public class ResourceExceptionMapper { +public class ResourceExceptionMapper implements Comparable> { private BeanFactory> factory; private int priority = Priorities.USER; private String className; + private Supplier discardAtRuntime; + public void setFactory(BeanFactory> factory) { this.factory = factory; } @@ -35,4 +39,34 @@ public ResourceExceptionMapper setClassName(String className) { this.className = className; return this; } + + public Supplier getDiscardAtRuntime() { + return discardAtRuntime; + } + + public void setDiscardAtRuntime(Supplier discardAtRuntime) { + this.discardAtRuntime = discardAtRuntime; + } + + @Override + public int compareTo(ResourceExceptionMapper o) { + return Integer.compare(this.priority, o.priority); + } + + public static final class DiscardAtRuntimeIfBeanIsUnavailable implements Supplier { + private final String beanClass; + + public DiscardAtRuntimeIfBeanIsUnavailable(String beanClass) { + this.beanClass = beanClass; + } + + @Override + public Boolean get() { + throw new IllegalStateException("should never be called"); + } + + public String getBeanClass() { + return beanClass; + } + } } diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceInterceptor.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceInterceptor.java index 1092d32cc6dca..146b956b24fee 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceInterceptor.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceInterceptor.java @@ -16,7 +16,7 @@ public class ResourceInterceptor private BeanFactory factory; private int priority = Priorities.USER; // default priority as defined by spec private boolean nonBlockingRequired; // whether or not @NonBlocking was specified on the class - private boolean readBody; // whether or not 'readBody' was set true for this filter + private boolean withFormRead; // whether or not '@WithFormRead' was set on this filter /** * The class names of the {@code @NameBinding} annotations that the method is annotated with. @@ -76,12 +76,12 @@ public void setNonBlockingRequired(boolean nonBlockingRequired) { this.nonBlockingRequired = nonBlockingRequired; } - public boolean isReadBody() { - return readBody; + public boolean isWithFormRead() { + return withFormRead; } - public void setReadBody(boolean readBody) { - this.readBody = readBody; + public void setWithFormRead(boolean withFormRead) { + this.withFormRead = withFormRead; } // spec says that writer interceptors are sorted in ascending order diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceMethod.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceMethod.java index 7725306fefc1b..54463c2a191fa 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceMethod.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceMethod.java @@ -68,6 +68,8 @@ public class ResourceMethod { private boolean isFormParamRequired; + private Set fileFormNames; + private List subResourceMethods; public ResourceMethod() { @@ -224,6 +226,15 @@ public ResourceMethod setFormParamRequired(boolean isFormParamRequired) { return this; } + public Set getFileFormNames() { + return fileFormNames; + } + + public ResourceMethod setFileFormNames(Set fileFormNames) { + this.fileFormNames = fileFormNames; + return this; + } + public ResourceMethod setStreamElementType(String streamElementType) { this.streamElementType = streamElementType; return this; diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index a3655066d920b..e86c773738f2c 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -41,7 +41,7 @@ 11 2.0.2 - 3.0.4 + 3.0.5 1.12.12 5.9.1 3.8.6 diff --git a/independent-projects/resteasy-reactive/server/jackson/src/main/java/org/jboss/resteasy/reactive/server/jackson/JacksonBasicMessageBodyReader.java b/independent-projects/resteasy-reactive/server/jackson/src/main/java/org/jboss/resteasy/reactive/server/jackson/JacksonBasicMessageBodyReader.java index f20abb80ac7e4..dbf2cd8b8b91c 100644 --- a/independent-projects/resteasy-reactive/server/jackson/src/main/java/org/jboss/resteasy/reactive/server/jackson/JacksonBasicMessageBodyReader.java +++ b/independent-projects/resteasy-reactive/server/jackson/src/main/java/org/jboss/resteasy/reactive/server/jackson/JacksonBasicMessageBodyReader.java @@ -9,14 +9,12 @@ import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; import org.jboss.resteasy.reactive.common.providers.serialisers.AbstractJsonMessageBodyReader; import org.jboss.resteasy.reactive.common.util.EmptyInputStream; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; -import com.fasterxml.jackson.databind.exc.MismatchedInputException; public class JacksonBasicMessageBodyReader extends AbstractJsonMessageBodyReader { @@ -30,11 +28,7 @@ public JacksonBasicMessageBodyReader(ObjectMapper mapper) { @Override public Object readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { - try { - return doReadFrom(type, genericType, entityStream); - } catch (MismatchedInputException e) { - throw new WebApplicationException(e, Response.Status.BAD_REQUEST); - } + return doReadFrom(type, genericType, entityStream); } protected ObjectReader getEffectiveReader() { diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java index 247375f997f6b..cae5bc7227def 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java @@ -95,6 +95,13 @@ public class ServerEndpointIndexer extends EndpointIndexer { + + private static final DotName FILE_DOT_NAME = DotName.createSimple(File.class.getName()); + private static final DotName PATH_DOT_NAME = DotName.createSimple(Path.class.getName()); + private static final DotName FILEUPLOAD_DOT_NAME = DotName.createSimple(FileUpload.class.getName()); + + private static final Set SUPPORTED_MULTIPART_FILE_TYPES = Set.of(FILE_DOT_NAME, PATH_DOT_NAME, + FILEUPLOAD_DOT_NAME); protected final EndpointInvokerFactory endpointInvokerFactory; protected final List methodScanners; protected final FieldInjectionIndexerExtension fieldInjectionHandler; @@ -176,7 +183,8 @@ protected ServerResourceMethod createResourceMethod(MethodInfo methodInfo, Class } @Override - protected boolean handleBeanParam(ClassInfo actualEndpointInfo, Type paramType, MethodParameter[] methodParameters, int i) { + protected boolean handleBeanParam(ClassInfo actualEndpointInfo, Type paramType, MethodParameter[] methodParameters, int i, + Set fileFormNames) { ClassInfo beanParamClassInfo = index.getClassByName(paramType.name()); InjectableBean injectableBean = scanInjectableBean(beanParamClassInfo, actualEndpointInfo, @@ -186,7 +194,7 @@ protected boolean handleBeanParam(ClassInfo actualEndpointInfo, Type paramType, + "Annotations like `@QueryParam` should be used in fields, not in methods.", beanParamClassInfo.name())); } - + fileFormNames.addAll(injectableBean.getFileFormNames()); return injectableBean.isFormParamRequired(); } @@ -233,6 +241,7 @@ private void validateMethodPath(ServerResourceMethod method, ClassInfo currentCl } } + @Override protected InjectableBean scanInjectableBean(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo, Map existingConverters, AdditionalReaders additionalReaders, Map injectableBeans, boolean hasRuntimeConverters) { @@ -280,6 +289,24 @@ protected InjectableBean scanInjectableBean(ClassInfo currentClassInfo, ClassInf } else if (result.getType() == ParameterType.FORM) { // direct form param requirement currentInjectableBean.setFormParamRequired(true); + + if (SUPPORTED_MULTIPART_FILE_TYPES.contains(field.type().name())) { + String name = field.name(); + AnnotationInstance restForm = field.annotation(ResteasyReactiveDotNames.REST_FORM_PARAM); + AnnotationInstance formParam = field.annotation(ResteasyReactiveDotNames.FORM_PARAM); + if (restForm != null) { + AnnotationValue value = restForm.value(); + if (value != null) { + name = value.asString(); + } + } else if (formParam != null) { + AnnotationValue value = formParam.value(); + if (value != null) { + name = value.asString(); + } + } + currentInjectableBean.getFileFormNames().add(name); + } } } // the TCK expects that fields annotated with @BeanParam are handled last @@ -309,17 +336,25 @@ protected InjectableBean scanInjectableBean(ClassInfo currentClassInfo, ClassInf return currentInjectableBean; } + @Override protected MethodParameter createMethodParameter(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo, boolean encoded, Type paramType, ServerIndexedParameter parameterResult, String name, String defaultValue, ParameterType type, - String elementType, boolean single, String signature) { + String elementType, boolean single, String signature, + Set fileFormNames) { ParameterConverterSupplier converter = parameterResult.getConverter(); DeclaredTypes declaredTypes = getDeclaredTypes(paramType, currentClassInfo, actualEndpointInfo); String mimeType = getPartMime(parameterResult.getAnns()); + String separator = getSeparator(parameterResult.getAnns()); + String declaredType = declaredTypes.getDeclaredType(); + + if (SUPPORTED_MULTIPART_FILE_TYPES.contains(DotName.createSimple(declaredType))) { + fileFormNames.add(name); + } return new ServerMethodParameter(name, - elementType, declaredTypes.getDeclaredType(), declaredTypes.getDeclaredUnresolvedType(), + elementType, declaredType, declaredTypes.getDeclaredUnresolvedType(), type, single, signature, converter, defaultValue, parameterResult.isObtainedAsCollection(), parameterResult.isOptional(), encoded, - parameterResult.getCustomParameterExtractor(), mimeType); + parameterResult.getCustomParameterExtractor(), mimeType, separator); } protected void handleOtherParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMapperGenerator.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMapperGenerator.java index c5ac16bb7d8c6..60ddd874a0b23 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMapperGenerator.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMapperGenerator.java @@ -24,7 +24,9 @@ import java.util.Map; import java.util.Set; import java.util.function.BiFunction; +import java.util.function.Predicate; +import javax.enterprise.inject.Instance; import javax.inject.Inject; import javax.inject.Singleton; import javax.ws.rs.container.ResourceInfo; @@ -240,7 +242,7 @@ public static Map generatePerClassMapper(MethodInfo targetMethod * Returns a map containing the handled exception name as the key and the generated class name as the value */ public static Map generateGlobalMapper(MethodInfo targetMethod, ClassOutput classOutput, - Set unwrappableTypes, Set additionalBeanAnnotations) { + Set unwrappableTypes, Set additionalBeanAnnotations, Predicate isOptionalMapper) { ReturnType returnType = determineReturnType(targetMethod); checkModifiers(targetMethod); @@ -264,14 +266,32 @@ public static Map generateGlobalMapper(MethodInfo targetMethod, .setModifiers(Modifier.PRIVATE | Modifier.FINAL) .getFieldDescriptor(); - // generate a constructor that takes the target class as an argument - this class is a CDI bean so Arc will be able to inject into the generated class - MethodCreator ctor = cc.getMethodCreator("", void.class, targetClass.name().toString()); + MethodCreator ctor; + + boolean isOptional = isOptionalMapper.test(targetMethod); + if (isOptional) { + // generate a constructor that takes the Instance as an argument in order to avoid missing bean issues if the target has been conditionally disabled + // the body can freely read the instance value because if the target has been conditionally disabled, the generated class will not be instantiated + ctor = cc.getMethodCreator("", void.class, Instance.class).setSignature( + String.format("(L%s;)V", + Instance.class.getName().replace('.', '/'), + targetClass.name().toString().replace('.', '/'))); + } else { + // generate a constructor that takes the target class as an argument - this class is a CDI bean so Arc will be able to inject into the generated class + ctor = cc.getMethodCreator("", void.class, targetClass.name().toString()); + } + ctor.setModifiers(Modifier.PUBLIC); ctor.addAnnotation(Inject.class); ctor.invokeSpecialMethod(ofConstructor(Object.class), ctor.getThis()); ResultHandle self = ctor.getThis(); - ResultHandle config = ctor.getMethodParam(0); - ctor.writeInstanceField(delegateField, self, config); + ResultHandle param = ctor.getMethodParam(0); + if (isOptional) { + ctor.writeInstanceField(delegateField, self, ctor + .invokeInterfaceMethod(MethodDescriptor.ofMethod(Instance.class, "get", Object.class), param)); + } else { + ctor.writeInstanceField(delegateField, self, param); + } ctor.returnValue(null); if (returnType == ReturnType.RESPONSE || returnType == ReturnType.REST_RESPONSE) { diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMappingFeature.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMappingFeature.java index ef2e8ce4c1ae3..7c6e8e9c73710 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMappingFeature.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMappingFeature.java @@ -76,7 +76,7 @@ public FeatureScanResult integrate(IndexView index, ScannedApplication scannedAp // the user class itself is made to be a bean as we want the user to be able to declare dependencies //additionalBeans.addBeanClass(methodInfo.declaringClass().name().toString()); Map generatedClassNames = ServerExceptionMapperGenerator.generateGlobalMapper(methodInfo, - classOutput, unwrappableTypes, additionalBeanAnnotations); + classOutput, unwrappableTypes, additionalBeanAnnotations, (m) -> false); for (Map.Entry entry : generatedClassNames.entrySet()) { ResourceExceptionMapper mapper = new ResourceExceptionMapper<>().setClassName(entry.getValue()); scannedApplication.getExceptionMappers().addExceptionMapper(entry.getKey(), mapper); diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/CustomFilterGenerator.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/CustomFilterGenerator.java index bb08d63067ee3..2c99a7eaac714 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/CustomFilterGenerator.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/CustomFilterGenerator.java @@ -522,7 +522,9 @@ private FieldDescriptor generateConstructorAndDelegateField(ClassCreator cc, Cla if (checkForOptionalBean) { // generate a constructor that takes the Instance as an argument ctor = cc.getMethodCreator("", void.class, Instance.class).setSignature( - String.format("(Ljavax/enterprise/inject/Instance;)V", declaringClassName.replace('.', '/'))); + String.format("(L%s;)V", + Instance.class.getName().replace('.', '/'), + declaringClassName.replace('.', '/'))); } else { // generate a constructor that takes the target class as an argument - this class is a CDI bean so Arc will be able to inject into the generated class ctor = cc.getMethodCreator("", void.class, declaringClassName); diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/FilterGeneration.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/FilterGeneration.java index 9d07114902225..7b6508ecc7713 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/FilterGeneration.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/FilterGeneration.java @@ -19,6 +19,7 @@ import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; import org.jboss.resteasy.reactive.server.processor.util.GeneratedClass; import org.jboss.resteasy.reactive.server.processor.util.GeneratedClassOutput; +import org.jboss.resteasy.reactive.server.processor.util.ResteasyReactiveServerDotNames; public class FilterGeneration { public static List generate(IndexView index, Set unwrappableTypes, @@ -37,6 +38,7 @@ public static List generate(IndexView index, Set unwra boolean preMatching = false; boolean nonBlockingRequired = false; boolean readBody = false; + boolean withFormRead = methodInfo.hasAnnotation(ResteasyReactiveServerDotNames.WITH_FORM_READ); Set nameBindingNames = new HashSet<>(); AnnotationValue priorityValue = instance.value("priority"); @@ -56,10 +58,17 @@ public static List generate(IndexView index, Set unwra readBody = readBodyValue.asBoolean(); } - if (readBody && preMatching) { - throw new IllegalStateException( - "Setting both 'readBody' and 'preMatching' to 'true' on '@ServerRequestFilter' is invalid. Offending method is '" - + methodInfo.name() + "' of class '" + methodInfo.declaringClass().name() + "'"); + if (preMatching) { + if (readBody) { + throw new IllegalStateException( + "Setting both 'readBody' and 'preMatching' to 'true' on '@ServerRequestFilter' is invalid. Offending method is '" + + methodInfo.name() + "' of class '" + methodInfo.declaringClass().name() + "'"); + } + if (withFormRead) { + throw new IllegalStateException( + "Setting both '@WithFormRead' and 'preMatching' to 'true' on '@ServerRequestFilter' is invalid. Offending method is '" + + methodInfo.name() + "' of class '" + methodInfo.declaringClass().name() + "'"); + } } List annotations = methodInfo.annotations(); @@ -78,7 +87,7 @@ public static List generate(IndexView index, Set unwra } ret.add(new GeneratedFilter(output.getOutput(), generatedClassName, methodInfo.declaringClass().name().toString(), - true, priority, preMatching, nonBlockingRequired, nameBindingNames, readBody, methodInfo)); + true, priority, preMatching, nonBlockingRequired, nameBindingNames, withFormRead || readBody, methodInfo)); } for (AnnotationInstance instance : index .getAnnotations(SERVER_RESPONSE_FILTER)) { @@ -128,14 +137,14 @@ public static class GeneratedFilter { final boolean preMatching; final boolean nonBlocking; final Set nameBindingNames; - final boolean readBody; + final boolean withFormRead; final MethodInfo filterSourceMethod; public GeneratedFilter(List generatedClasses, String generatedClassName, String declaringClassName, boolean requestFilter, Integer priority, boolean preMatching, boolean nonBlocking, - Set nameBindingNames, boolean readBody, MethodInfo filterSourceMethod) { + Set nameBindingNames, boolean withFormRead, MethodInfo filterSourceMethod) { this.generatedClasses = generatedClasses; this.generatedClassName = generatedClassName; this.declaringClassName = declaringClassName; @@ -144,7 +153,7 @@ public GeneratedFilter(List generatedClasses, String generatedCl this.preMatching = preMatching; this.nonBlocking = nonBlocking; this.nameBindingNames = nameBindingNames; - this.readBody = readBody; + this.withFormRead = withFormRead; this.filterSourceMethod = filterSourceMethod; } @@ -180,8 +189,8 @@ public Set getNameBindingNames() { return nameBindingNames; } - public boolean isReadBody() { - return readBody; + public boolean isWithFormRead() { + return withFormRead; } public MethodInfo getFilterSourceMethod() { diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/DotNames.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/DotNames.java deleted file mode 100644 index d1fdb5c0ed9ba..0000000000000 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/DotNames.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.jboss.resteasy.reactive.server.processor.generation.multipart; - -import java.io.File; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.file.Path; - -import org.jboss.jandex.DotName; -import org.jboss.resteasy.reactive.multipart.FileUpload; - -final class DotNames { - - static final String POPULATE_METHOD_NAME = "populate"; - static final DotName OBJECT_NAME = DotName.createSimple(Object.class.getName()); - static final DotName STRING_NAME = DotName.createSimple(String.class.getName()); - static final DotName BYTE_NAME = DotName.createSimple(byte.class.getName()); - static final DotName INPUT_STREAM_NAME = DotName.createSimple(InputStream.class.getName()); - static final DotName INPUT_STREAM_READER_NAME = DotName.createSimple(InputStreamReader.class.getName()); - static final DotName FIELD_UPLOAD_NAME = DotName.createSimple(FileUpload.class.getName()); - static final DotName PATH_NAME = DotName.createSimple(Path.class.getName()); - static final DotName FILE_NAME = DotName.createSimple(File.class.getName()); - - private DotNames() { - } -} diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/util/ResteasyReactiveServerDotNames.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/util/ResteasyReactiveServerDotNames.java index 4fbe147aae4f3..bf6706327c8fb 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/util/ResteasyReactiveServerDotNames.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/util/ResteasyReactiveServerDotNames.java @@ -6,6 +6,7 @@ import org.jboss.resteasy.reactive.server.ServerRequestFilter; import org.jboss.resteasy.reactive.server.ServerResponseFilter; import org.jboss.resteasy.reactive.server.SimpleResourceInfo; +import org.jboss.resteasy.reactive.server.WithFormRead; import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext; import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyReader; import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter; @@ -25,5 +26,6 @@ public class ResteasyReactiveServerDotNames { public static final DotName QUARKUS_REST_CONTAINER_REQUEST_CONTEXT = DotName .createSimple(ResteasyReactiveContainerRequestContext.class.getName()); public static final DotName SIMPLIFIED_RESOURCE_INFO = DotName.createSimple(SimpleResourceInfo.class.getName()); - + public static final DotName WITH_FORM_READ = DotName + .createSimple(WithFormRead.class.getName()); } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerRequestFilter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerRequestFilter.java index bb8a6c2379d24..8e60ebb805a6d 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerRequestFilter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerRequestFilter.java @@ -113,6 +113,9 @@ * Resource Methods that the filter applies to, it will be executed in normal fashion. * * Also note that this setting and {@link ServerRequestFilter#preMatching()} cannot be both set to true. + * + * @deprecated use {@link WithFormRead} on your filter to force reading the form values before your filter is invoked. */ + @Deprecated boolean readBody() default false; } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/WithFormRead.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/WithFormRead.java new file mode 100644 index 0000000000000..3eef6995eee2c --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/WithFormRead.java @@ -0,0 +1,18 @@ +package org.jboss.resteasy.reactive.server; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Forces the form body to be read and parsed before filters and the endpoint are invoked. This is only + * useful if your endpoint does not contain any declared form parameter, which would otherwise force + * the form body being read anyway. + * You can place this annotation on request filters as well as endpoints. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface WithFormRead { + +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ExceptionMapping.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ExceptionMapping.java index 680cd22425ae2..cbf952beb1f2f 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ExceptionMapping.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ExceptionMapping.java @@ -1,6 +1,7 @@ package org.jboss.resteasy.reactive.server.core; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -8,13 +9,28 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import org.jboss.resteasy.reactive.common.model.ResourceExceptionMapper; import org.jboss.resteasy.reactive.spi.BeanFactory; +@SuppressWarnings({ "unchecked", "unused" }) public class ExceptionMapping { + /** + * The idea behind having two different maps is the following: + * Under normal circumstances, mappers are added to the first map, + * and we don't need to track multiple mappings because the priority is enough + * to distinguish. + * + * However, in the case where ExceptionMapper classes may not be active at runtime + * (due to the presence of conditional bean annotations), then we need to track + * all the possible mappings and at runtime determine the one with the lowest priority + * value that is active. + */ final Map> mappers = new HashMap<>(); + // this is going to be used when there are mappers that are removable at runtime + final Map>> runtimeCheckMappers = new HashMap<>(); /** * Exceptions that indicate an blocking operation was performed on an IO thread. @@ -61,17 +77,44 @@ public void addExceptionMapper(String exceptionClass, Reso ResourceExceptionMapper existing = mappers.get(exceptionClass); if (existing != null) { if (existing.getPriority() < mapper.getPriority()) { - //already a higher priority mapper + // we already have a lower priority mapper registered return; + } else { + mappers.remove(exceptionClass); + List> list = new ArrayList<>(2); + list.add(mapper); + list.add(existing); + runtimeCheckMappers.put(exceptionClass, list); + } + } else { + var list = runtimeCheckMappers.get(exceptionClass); + if (list == null) { + if (mapper.getDiscardAtRuntime() == null) { + mappers.put(exceptionClass, mapper); + } else { + list = new ArrayList<>(1); + list.add(mapper); + runtimeCheckMappers.put(exceptionClass, list); + } + } else { + list.add(mapper); + Collections.sort(list); } } - mappers.put(exceptionClass, mapper); } public void initializeDefaultFactories(Function> factoryCreator) { - for (Map.Entry> entry : mappers.entrySet()) { - if (entry.getValue().getFactory() == null) { - entry.getValue().setFactory((BeanFactory) factoryCreator.apply(entry.getValue().getClassName())); + for (var resourceExceptionMapper : mappers.values()) { + if (resourceExceptionMapper.getFactory() == null) { + resourceExceptionMapper.setFactory((BeanFactory) factoryCreator.apply(resourceExceptionMapper.getClassName())); + } + } + for (var list : runtimeCheckMappers.values()) { + for (var resourceExceptionMapper : list) { + if (resourceExceptionMapper.getFactory() == null) { + resourceExceptionMapper + .setFactory((BeanFactory) factoryCreator.apply(resourceExceptionMapper.getClassName())); + } } } } @@ -80,6 +123,51 @@ public Map> getMappers() { return mappers; } + public Map>> getRuntimeCheckMappers() { + return runtimeCheckMappers; + } + + public Map> effectiveMappers() { + if (runtimeCheckMappers.isEmpty()) { + return mappers; + } + Map> result = new HashMap<>(); + for (var entry : runtimeCheckMappers.entrySet()) { + String exceptionClass = entry.getKey(); + var list = entry.getValue(); + for (var resourceExceptionMapper : list) { + if (resourceExceptionMapper.getDiscardAtRuntime() == null) { + result.put(exceptionClass, resourceExceptionMapper); + break; + } else { + if (!resourceExceptionMapper.getDiscardAtRuntime().get()) { + result.put(exceptionClass, resourceExceptionMapper); + break; + } + } + } + } + result.putAll(mappers); + return result; + } + + public void replaceDiscardAtRuntimeIfBeanIsUnavailable(Function> function) { + if (runtimeCheckMappers.isEmpty()) { + return; + } + for (var list : runtimeCheckMappers.values()) { + for (var resourceExceptionMapper : list) { + if (resourceExceptionMapper + .getDiscardAtRuntime() instanceof ResourceExceptionMapper.DiscardAtRuntimeIfBeanIsUnavailable) { + var discardAtRuntimeIfBeanIsUnavailable = (ResourceExceptionMapper.DiscardAtRuntimeIfBeanIsUnavailable) resourceExceptionMapper + .getDiscardAtRuntime(); + resourceExceptionMapper + .setDiscardAtRuntime(function.apply(discardAtRuntimeIfBeanIsUnavailable.getBeanClass())); + } + } + } + } + public static class ExceptionTypePredicate implements Predicate { private Class throwable; diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/RuntimeExceptionMapper.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/RuntimeExceptionMapper.java index 04be9002bf53b..a72022cea3288 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/RuntimeExceptionMapper.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/RuntimeExceptionMapper.java @@ -45,7 +45,7 @@ public class RuntimeExceptionMapper { public RuntimeExceptionMapper(ExceptionMapping mapping, ClassLoader classLoader) { try { mappers = new HashMap<>(); - for (var i : mapping.mappers.entrySet()) { + for (var i : mapping.effectiveMappers().entrySet()) { mappers.put((Class) Class.forName(i.getKey(), false, classLoader), i.getValue()); } blockingProblemPredicates = new ArrayList<>(mapping.blockingProblemPredicates); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormEncodedDataDefinition.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormEncodedDataDefinition.java index 4829909c40610..39e499454e13d 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormEncodedDataDefinition.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormEncodedDataDefinition.java @@ -5,6 +5,7 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.Set; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.HttpHeaders; @@ -37,7 +38,7 @@ public FormEncodedDataDefinition() { } @Override - public FormDataParser create(final ResteasyReactiveRequestContext exchange) { + public FormDataParser create(final ResteasyReactiveRequestContext exchange, Set fileFormNames) { String mimeType = exchange.serverRequest().getRequestHeader(HttpHeaders.CONTENT_TYPE); if (forceCreation || (mimeType != null && mimeType.startsWith(APPLICATION_X_WWW_FORM_URLENCODED))) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormParserFactory.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormParserFactory.java index 7fa20a50e5584..8d382ed75a4b5 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormParserFactory.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormParserFactory.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Supplier; @@ -28,11 +29,12 @@ public class FormParserFactory { * Creates a form data parser for this request. * * @param exchange The exchange + * @param fileFormNames * @return A form data parser, or null if there is no parser registered for the request content type */ - public FormDataParser createParser(final ResteasyReactiveRequestContext exchange) { + public FormDataParser createParser(final ResteasyReactiveRequestContext exchange, Set fileFormNames) { for (int i = 0; i < parserDefinitions.length; ++i) { - FormDataParser parser = parserDefinitions[i].create(exchange); + FormDataParser parser = parserDefinitions[i].create(exchange, fileFormNames); if (parser != null) { return parser; } @@ -42,7 +44,7 @@ public FormDataParser createParser(final ResteasyReactiveRequestContext exchange public interface ParserDefinition { - FormDataParser create(final ResteasyReactiveRequestContext exchange); + FormDataParser create(final ResteasyReactiveRequestContext exchange, Set fileFormNames); T setDefaultCharset(String charset); } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultiPartParserDefinition.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultiPartParserDefinition.java index 216da2f1965f6..9d3465317a90d 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultiPartParserDefinition.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultiPartParserDefinition.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Supplier; @@ -53,6 +54,7 @@ public class MultiPartParserDefinition implements FormParserFactory.ParserDefini private long maxAttributeSize = 2048; private long maxEntitySize = -1; + private List fileContentTypes; public MultiPartParserDefinition(Supplier executorSupplier) { this.executorSupplier = executorSupplier; @@ -65,7 +67,7 @@ public MultiPartParserDefinition(Supplier executorSupplier, final Path } @Override - public FormDataParser create(final ResteasyReactiveRequestContext exchange) { + public FormDataParser create(final ResteasyReactiveRequestContext exchange, Set fileFormNames) { String mimeType = exchange.serverRequest().getRequestHeader(HttpHeaders.CONTENT_TYPE); if (mimeType != null && mimeType.startsWith(MULTIPART_FORM_DATA)) { String boundary = HeaderUtil.extractQuotedValueFromHeader(mimeType, "boundary"); @@ -76,7 +78,7 @@ public FormDataParser create(final ResteasyReactiveRequestContext exchange) { return null; } final MultiPartUploadHandler parser = new MultiPartUploadHandler(exchange, boundary, maxIndividualFileSize, - fileSizeThreshold, defaultCharset, mimeType, maxAttributeSize, maxEntitySize); + fileSizeThreshold, defaultCharset, mimeType, maxAttributeSize, maxEntitySize, fileFormNames); exchange.registerCompletionCallback(new CompletionCallback() { @Override public void onComplete(Throwable throwable) { @@ -152,6 +154,15 @@ public MultiPartParserDefinition setMaxEntitySize(long maxEntitySize) { return this; } + public List getFileContentTypes() { + return fileContentTypes; + } + + public MultiPartParserDefinition setFileContentTypes(List fileContentTypes) { + this.fileContentTypes = fileContentTypes; + return this; + } + private final class MultiPartUploadHandler implements FormDataParser, MultipartParser.PartHandler { private final ResteasyReactiveRequestContext exchange; @@ -161,6 +172,7 @@ private final class MultiPartUploadHandler implements FormDataParser, MultipartP private final long fileSizeThreshold; private final long maxAttributeSize; private final long maxEntitySize; + private final Set fileFormNames; private String defaultEncoding; private final ByteArrayOutputStream contentBytes = new ByteArrayOutputStream(); @@ -175,13 +187,15 @@ private final class MultiPartUploadHandler implements FormDataParser, MultipartP private MultiPartUploadHandler(final ResteasyReactiveRequestContext exchange, final String boundary, final long maxIndividualFileSize, final long fileSizeThreshold, final String defaultEncoding, - String contentType, long maxAttributeSize, long maxEntitySize) { + String contentType, long maxAttributeSize, long maxEntitySize, + Set fileFormNames) { this.exchange = exchange; this.maxIndividualFileSize = maxIndividualFileSize; this.defaultEncoding = defaultEncoding; this.fileSizeThreshold = fileSizeThreshold; this.maxAttributeSize = maxAttributeSize; this.maxEntitySize = maxEntitySize; + this.fileFormNames = fileFormNames; int maxParameters = 1000; this.data = new FormData(maxParameters); String charset = defaultEncoding; @@ -236,7 +250,9 @@ public void beginPart(final CaseInsensitiveMap headers) { if (disposition.startsWith("form-data")) { currentName = HeaderUtil.extractQuotedValueFromHeader(disposition, "name"); fileName = HeaderUtil.extractQuotedValueFromHeaderWithEncoding(disposition, "filename"); - if (fileName != null && fileSizeThreshold == 0) { + String contentType = headers.getFirst(HttpHeaders.CONTENT_TYPE); + if (((fileName != null) || isFileContentType(contentType) || fileFormNames.contains(currentName)) + && fileSizeThreshold == 0) { try { if (tempFileLocation != null) { Files.createDirectories(tempFileLocation); @@ -254,6 +270,14 @@ public void beginPart(final CaseInsensitiveMap headers) { } } + private boolean isFileContentType(String contentType) { + if (contentType == null || fileContentTypes == null) { + return false; + } + + return fileContentTypes.contains(contentType); + } + @Override public void data(final ByteBuffer buffer) throws IOException { this.currentFileSize += buffer.remaining(); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java index a7915b23b9349..39b21a7a58bff 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java @@ -1,5 +1,7 @@ package org.jboss.resteasy.reactive.server.core.multipart; +import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; + import java.io.File; import java.io.IOException; import java.io.OutputStream; @@ -136,7 +138,7 @@ private void writeHeaders(String partName, Object partValue, PartItem part, Char throws IOException { part.getHeaders().put(HttpHeaders.CONTENT_DISPOSITION, List.of("form-data; name=\"" + partName + "\"" + getFileNameIfFile(partValue, part.getFilename()))); - part.getHeaders().put(HttpHeaders.CONTENT_TYPE, List.of(part.getMediaType())); + part.getHeaders().put(CONTENT_TYPE, List.of(part.getMediaType())); for (Map.Entry> entry : part.getHeaders().entrySet()) { writeLine(outputStream, entry.getKey() + ": " + entry.getValue().stream().map(String::valueOf) .collect(Collectors.joining("; ")), charset); @@ -206,8 +208,15 @@ private String generateBoundary() { private void appendBoundaryIntoMediaType(ResteasyReactiveRequestContext requestContext, String boundary, MediaType mediaType) { - requestContext.setResponseContentType(new MediaType(mediaType.getType(), mediaType.getSubtype(), - Collections.singletonMap(BOUNDARY_PARAM, boundary))); + MediaType mediaTypeWithBoundary = new MediaType(mediaType.getType(), mediaType.getSubtype(), + Collections.singletonMap(BOUNDARY_PARAM, boundary)); + requestContext.setResponseContentType(mediaTypeWithBoundary); + + // this is a total hack, but it's needed to make RestResponse work properly + requestContext.serverResponse().setResponseHeader(CONTENT_TYPE, mediaTypeWithBoundary.toString()); + if (requestContext.getResponse().isCreated()) { + requestContext.getResponse().get().getHeaders().remove(CONTENT_TYPE); + } } private boolean isNotEmpty(String str) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java index 5506f976f1d92..0c44ebb2b0941 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java @@ -283,9 +283,9 @@ public static DefaultFileUpload getFileUpload(String formName, ResteasyReactiveR public static List getFileUploads(String formName, ResteasyReactiveRequestContext context) { List result = new ArrayList<>(); - FormData fileUploads = context.getFormData(); - if (fileUploads != null) { - Collection fileUploadsForName = fileUploads.get(formName); + FormData formData = context.getFormData(); + if (formData != null) { + Collection fileUploadsForName = formData.get(formName); if (fileUploadsForName != null) { for (FormData.FormValue fileUpload : fileUploadsForName) { if (fileUpload.isFileItem()) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/QueryParamExtractor.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/QueryParamExtractor.java index ba9efb34ea930..f6d31259de9bb 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/QueryParamExtractor.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/QueryParamExtractor.java @@ -1,5 +1,9 @@ package org.jboss.resteasy.reactive.server.core.parameters; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; public class QueryParamExtractor implements ParameterExtractor { @@ -7,15 +11,37 @@ public class QueryParamExtractor implements ParameterExtractor { private final String name; private final boolean single; private final boolean encoded; + private final String separator; - public QueryParamExtractor(String name, boolean single, boolean encoded) { + public QueryParamExtractor(String name, boolean single, boolean encoded, String separator) { this.name = name; this.single = single; this.encoded = encoded; + this.separator = separator; } @Override + @SuppressWarnings("unchecked") public Object extractParameter(ResteasyReactiveRequestContext context) { - return context.getQueryParameter(name, single, encoded); + Object queryParameter = context.getQueryParameter(name, single, encoded); + if (separator != null) { + if (queryParameter instanceof List) { // it's List + List list = (List) queryParameter; + List result = new ArrayList<>(list.size()); + for (int i = 0; i < list.size(); i++) { + String[] parts = list.get(i).split(separator); + result.addAll(Arrays.asList(parts)); + } + queryParameter = result; + } else if (queryParameter instanceof String) { + List result = new ArrayList<>(1); + String[] parts = ((String) queryParameter).split(separator); + result.addAll(Arrays.asList(parts)); + queryParameter = result; + } else { + // can't really happen + } + } + return queryParameter; } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java index e2966a4152ae9..3ef5ad8f2d11c 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java @@ -111,6 +111,9 @@ public BeanFactory.BeanInstance apply(Class aClass) { possibleSubResource.addAll(resourceClasses); //the TCK uses normal resources also as sub resources for (int i = 0; i < possibleSubResource.size(); i++) { ResourceClass clazz = possibleSubResource.get(i); + if ((clazz.getIsDisabled() != null) && clazz.getIsDisabled().get()) { + continue; + } Map>>> templates = new HashMap<>(); URITemplate classPathTemplate = clazz.getPath() == null ? null : new URITemplate(clazz.getPath(), true); for (int j = 0; j < clazz.getMethods().size(); j++) { @@ -132,6 +135,9 @@ public BeanFactory.BeanInstance apply(Class aClass) { for (int i = 0; i < resourceClasses.size(); i++) { ResourceClass clazz = resourceClasses.get(i); + if ((clazz.getIsDisabled() != null) && clazz.getIsDisabled().get()) { + continue; + } URITemplate classTemplate = new URITemplate(clazz.getPath(), true); var perClassMappers = mappers.get(classTemplate); if (perClassMappers == null) { @@ -187,7 +193,7 @@ public BeanFactory.BeanInstance apply(Class aClass) { .entrySet()) { preMatchHandlers .add(new ResourceRequestFilterHandler(entry.getValue(), true, entry.getKey().isNonBlockingRequired(), - entry.getKey().isReadBody())); + entry.getKey().isWithFormRead())); } } for (int i = 0; i < info.getGlobalHandlerCustomizers().size(); i++) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeInterceptorDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeInterceptorDeployment.java index 719a87bbc61e5..89775a73b4620 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeInterceptorDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeInterceptorDeployment.java @@ -98,7 +98,7 @@ public RuntimeInterceptorDeployment(DeploymentInfo info, ConfigurationImpl confi .entrySet()) { globalRequestInterceptorHandlers .add(new ResourceRequestFilterHandler(entry.getValue(), false, entry.getKey().isNonBlockingRequired(), - entry.getKey().isReadBody())); + entry.getKey().isWithFormRead())); } InterceptorHandler globalInterceptorHandler = null; @@ -330,7 +330,7 @@ public List setupRequestFilterHandler() { .entrySet()) { handlers.add( new ResourceRequestFilterHandler(entry.getValue(), false, entry.getKey().isNonBlockingRequired(), - entry.getKey().isNonBlockingRequired())); + entry.getKey().isWithFormRead())); } } return handlers; diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java index 3c2710fef8cf4..08494b3d68ce1 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java @@ -242,7 +242,7 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, //spec doesn't seem to test this, but RESTEasy does not run request filters for both root and sub resources (which makes sense) //so only run request filters for methods that are leaf resources - i.e. have a HTTP method annotation so we ensure only one will run - boolean hasReadBodyRequestFilters = false; + boolean hasWithFormReadRequestFilters = false; if (method.getHttpMethod() != null) { List containerRequestFilterHandlers = interceptorDeployment .setupRequestFilterHandler(); @@ -257,13 +257,15 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, } else { handlers.add(handler); } - if (handler.isReadBody()) { - hasReadBodyRequestFilters = true; - } } } else { handlers.addAll(containerRequestFilterHandlers); } + for (ResourceRequestFilterHandler handler : containerRequestFilterHandlers) { + if (handler.isWithFormRead()) { + hasWithFormReadRequestFilters = true; + } + } } // some parameters need the body to be read @@ -280,28 +282,28 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, } } // form params can be everywhere (field, beanparam, param) - boolean checkReadBodyRequestFilters = false; - if (method.isFormParamRequired()) { + boolean checkWithFormReadRequestFilters = false; + if (method.isFormParamRequired() || hasWithFormReadRequestFilters) { // read the body as multipart in one go - handlers.add(new FormBodyHandler(bodyParameter != null, executorSupplier)); - checkReadBodyRequestFilters = true; + handlers.add(new FormBodyHandler(bodyParameter != null, executorSupplier, method.getFileFormNames())); + checkWithFormReadRequestFilters = true; } else if (bodyParameter != null) { if (!defaultBlocking) { if (!method.isBlocking()) { // allow the body to be read by chunks handlers.add(new InputHandler(resteasyReactiveConfig.getInputBufferSize(), executorSupplier)); - checkReadBodyRequestFilters = true; + checkWithFormReadRequestFilters = true; } } } - if (checkReadBodyRequestFilters && hasReadBodyRequestFilters) { + if (checkWithFormReadRequestFilters && hasWithFormReadRequestFilters) { // we need to remove the corresponding filters from the handlers list and add them to its end in the same order List readBodyRequestFilters = new ArrayList<>(1); for (int i = handlers.size() - 2; i >= 0; i--) { var serverRestHandler = handlers.get(i); if (serverRestHandler instanceof ResourceRequestFilterHandler) { ResourceRequestFilterHandler resourceRequestFilterHandler = (ResourceRequestFilterHandler) serverRestHandler; - if (resourceRequestFilterHandler.isReadBody()) { + if (resourceRequestFilterHandler.isWithFormRead()) { readBodyRequestFilters.add(handlers.remove(i)); } } @@ -676,7 +678,7 @@ public ParameterExtractor parameterExtractor(Map pathParameterI case ASYNC_RESPONSE: return new AsyncResponseExtractor(); case QUERY: - extractor = new QueryParamExtractor(param.name, param.isSingle(), param.encoded); + extractor = new QueryParamExtractor(param.name, param.isSingle(), param.encoded, param.separator); return extractor; case BODY: return new BodyParamExtractor(); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FormBodyHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FormBodyHandler.java index b7f12b58df269..42c7ada9b433e 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FormBodyHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FormBodyHandler.java @@ -8,6 +8,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Supplier; @@ -20,17 +21,18 @@ import org.jboss.resteasy.reactive.server.core.multipart.MultiPartParserDefinition; import org.jboss.resteasy.reactive.server.spi.GenericRuntimeConfigurableServerRestHandler; import org.jboss.resteasy.reactive.server.spi.RuntimeConfiguration; -import org.jboss.resteasy.reactive.server.spi.ServerHttpRequest; public class FormBodyHandler implements GenericRuntimeConfigurableServerRestHandler { private final boolean alsoSetInputStream; private final Supplier executorSupplier; + private final Set fileFormNames; private volatile FormParserFactory formParserFactory; - public FormBodyHandler(boolean alsoSetInputStream, Supplier executorSupplier) { + public FormBodyHandler(boolean alsoSetInputStream, Supplier executorSupplier, Set fileFormNames) { this.alsoSetInputStream = alsoSetInputStream; this.executorSupplier = executorSupplier; + this.fileFormNames = fileFormNames; } @Override @@ -46,6 +48,7 @@ public void configure(RuntimeConfiguration configuration) { .setMaxAttributeSize(configuration.limits().maxFormAttributeSize()) .setMaxEntitySize(configuration.limits().maxBodySize().orElse(-1L)) .setDeleteUploadsOnEnd(configuration.body().deleteUploadedFilesOnEnd()) + .setFileContentTypes(configuration.body().multiPart().fileContentTypes()) .setDefaultCharset(configuration.body().defaultCharset().name()) .setTempFileLocation(Path.of(configuration.body().uploadsDirectory()))) @@ -74,14 +77,12 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti requestContext.setFormData(existingParsedForm); return; } - ServerHttpRequest serverHttpRequest = requestContext.serverRequest(); + FormDataParser factory = formParserFactory.createParser(requestContext, fileFormNames); + if (factory == null) { + return; + } if (BlockingOperationSupport.isBlockingAllowed()) { //blocking IO approach - - FormDataParser factory = formParserFactory.createParser(requestContext); - if (factory == null) { - return; - } CapturingInputStream cis = null; if (alsoSetInputStream) { // the TCK allows the body to be read as a form param and also as a body param @@ -95,10 +96,6 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti requestContext.setInputStream(new ByteArrayInputStream(cis.baos.toByteArray())); } } else if (alsoSetInputStream) { - FormDataParser factory = formParserFactory.createParser(requestContext); - if (factory == null) { - return; - } requestContext.suspend(); executorSupplier.get().execute(new Runnable() { @Override @@ -115,10 +112,6 @@ public void run() { } }); } else { - FormDataParser factory = formParserFactory.createParser(requestContext); - if (factory == null) { - return; - } //parse will auto resume factory.parse(); } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/MediaTypeMapper.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/MediaTypeMapper.java index 480d342920cbb..383b95b3931b6 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/MediaTypeMapper.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/MediaTypeMapper.java @@ -28,6 +28,9 @@ */ public class MediaTypeMapper implements ServerRestHandler { + private static final MediaType[] DEFAULT_MEDIA_TYPES = new MediaType[] { MediaType.WILDCARD_TYPE }; + private static final List DEFAULT_MEDIA_TYPES_LIST = List.of(DEFAULT_MEDIA_TYPES); + final Map resourcesByConsumes; final List consumesTypes; @@ -35,20 +38,17 @@ public MediaTypeMapper(List runtimeResources) { resourcesByConsumes = new HashMap<>(); consumesTypes = new ArrayList<>(); for (RuntimeResource runtimeResource : runtimeResources) { - MediaType consumesMT = runtimeResource.getConsumes().isEmpty() ? MediaType.WILDCARD_TYPE - : runtimeResource.getConsumes().get(0); - if (!resourcesByConsumes.containsKey(consumesMT)) { - consumesTypes.add(consumesMT); - resourcesByConsumes.put(consumesMT, new Holder()); - } - MediaType[] produces = runtimeResource.getProduces() != null - ? runtimeResource.getProduces().getSortedOriginalMediaTypes() - : null; - if (produces == null) { - produces = new MediaType[] { MediaType.WILDCARD_TYPE }; + List consumesMediaTypes = getConsumesMediaTypes(runtimeResource); + for (MediaType consumedMediaType : consumesMediaTypes) { + if (!resourcesByConsumes.containsKey(consumedMediaType)) { + consumesTypes.add(consumedMediaType); + resourcesByConsumes.put(consumedMediaType, new Holder()); + } } - for (MediaType producesMT : produces) { - resourcesByConsumes.get(consumesMT).setResource(runtimeResource, producesMT); + for (MediaType producesMT : getProducesMediaTypes(runtimeResource)) { + for (MediaType consumedMediaType : consumesMediaTypes) { + resourcesByConsumes.get(consumedMediaType).setResource(runtimeResource, producesMT); + } } } for (Holder holder : resourcesByConsumes.values()) { @@ -116,6 +116,17 @@ public MediaType selectMediaType(ResteasyReactiveRequestContext requestContext, return selected; } + private MediaType[] getProducesMediaTypes(RuntimeResource runtimeResource) { + return runtimeResource.getProduces() == null + ? DEFAULT_MEDIA_TYPES + : runtimeResource.getProduces().getSortedOriginalMediaTypes(); + } + + private List getConsumesMediaTypes(RuntimeResource runtimeResource) { + return runtimeResource.getConsumes().isEmpty() ? DEFAULT_MEDIA_TYPES_LIST + : runtimeResource.getConsumes(); + } + private static final class Holder { private final Map mtWithoutParamsToResource = new HashMap<>(); 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 3ac1f35a6deb3..45389d6c2ca0e 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 @@ -11,6 +11,7 @@ import java.util.function.Consumer; import javax.ws.rs.core.MediaType; +import javax.ws.rs.sse.OutboundSseEvent; import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.common.util.RestMediaType; @@ -49,7 +50,12 @@ private static class SseMultiSubscriber extends AbstractMultiSubscriber { @Override public void onNext(Object item) { - OutboundSseEventImpl event = new OutboundSseEventImpl.BuilderImpl().data(item).build(); + OutboundSseEvent event; + if (item instanceof OutboundSseEvent) { + event = (OutboundSseEvent) item; + } else { + event = new OutboundSseEventImpl.BuilderImpl().data(item).build(); + } SseUtil.send(requestContext, event, customizers).whenComplete(new BiConsumer() { @Override public void accept(Object v, Throwable t) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResourceRequestFilterHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResourceRequestFilterHandler.java index 7fbdca4e10962..6e19c70d8ae4a 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResourceRequestFilterHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResourceRequestFilterHandler.java @@ -11,14 +11,14 @@ public class ResourceRequestFilterHandler implements ServerRestHandler { private final ContainerRequestFilter filter; private final boolean preMatch; private final boolean nonBlockingRequired; - private final boolean readBody; + private final boolean withFormRead; public ResourceRequestFilterHandler(ContainerRequestFilter filter, boolean preMatch, boolean nonBlockingRequired, - boolean readBody) { + boolean withFormRead) { this.filter = filter; this.preMatch = preMatch; this.nonBlockingRequired = nonBlockingRequired; - this.readBody = readBody; + this.withFormRead = withFormRead; } public ContainerRequestFilter getFilter() { @@ -33,8 +33,8 @@ public boolean isNonBlockingRequired() { return nonBlockingRequired; } - public boolean isReadBody() { - return readBody; + public boolean isWithFormRead() { + return withFormRead; } @Override diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java index 6d90dfd86bc0d..3ba853eda3962 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java @@ -116,8 +116,11 @@ public RequestMatch map(String path) { boolean doPrefixMatch = false; if (!fullMatch) { //according to the spec every template ends with (/.*)? - doPrefixMatch = (matchPos == 1 || path.charAt(matchPos) == '/') //matchPos == 1 corresponds to '/' as a root level match - && (prefixAllowed || matchPos == pathLength - 1); //if prefix is allowed, or the remainder is only a trailing / + if (matchPos == 1) { //matchPos == 1 corresponds to '/' as a root level match + doPrefixMatch = prefixAllowed || pathLength == 1; //if prefix is allowed, or we've matched the whole thing + } else if (path.charAt(matchPos) == '/') { + doPrefixMatch = prefixAllowed || matchPos == pathLength - 1; //if prefix is allowed, or the remainder is only a trailing / + } } if (fullMatch || doPrefixMatch) { String remaining; diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java index 9a24d48043bba..d866d393ea817 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java @@ -21,10 +21,10 @@ public ServerMethodParameter(String name, String type, String declaredType, Stri ParameterConverterSupplier converter, String defaultValue, boolean obtainedAsCollection, boolean optional, boolean encoded, ParameterExtractor customParameterExtractor, - String mimeType) { + String mimeType, String separator) { super(name, type, declaredType, declaredUnresolvedType, signature, parameterType, single, defaultValue, obtainedAsCollection, optional, - encoded, mimeType, null /* not useful for server params */); + encoded, mimeType, null /* not useful for server params */, separator); this.converter = converter; this.customParameterExtractor = customParameterExtractor; } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java index 6fe9c4a290ab1..958c6e55b7f8f 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java @@ -2,6 +2,7 @@ import java.nio.charset.Charset; import java.time.Duration; +import java.util.List; import java.util.Optional; public class DefaultRuntimeConfiguration implements RuntimeConfiguration { @@ -10,9 +11,16 @@ public class DefaultRuntimeConfiguration implements RuntimeConfiguration { private final Limits limits; public DefaultRuntimeConfiguration(Duration readTimeout, boolean deleteUploadedFilesOnEnd, String uploadsDirectory, - Charset defaultCharset, Optional maxBodySize, long maxFormAttributeSize) { + List fileContentTypes, Charset defaultCharset, Optional maxBodySize, long maxFormAttributeSize) { this.readTimeout = readTimeout; body = new Body() { + Body.MultiPart multiPart = new Body.MultiPart() { + @Override + public List fileContentTypes() { + return fileContentTypes; + } + }; + @Override public boolean deleteUploadedFilesOnEnd() { return deleteUploadedFilesOnEnd; @@ -27,6 +35,11 @@ public String uploadsDirectory() { public Charset defaultCharset() { return defaultCharset; } + + @Override + public MultiPart multiPart() { + return multiPart; + } }; limits = new Limits() { @Override diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java index 5bdd06534a537..6ccfdcee9dd50 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java @@ -2,6 +2,7 @@ import java.nio.charset.Charset; import java.time.Duration; +import java.util.List; import java.util.Optional; public interface RuntimeConfiguration { @@ -19,6 +20,12 @@ interface Body { String uploadsDirectory(); Charset defaultCharset(); + + MultiPart multiPart(); + + interface MultiPart { + List fileContentTypes(); + } } interface Limits { diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java index 92d770aeb910a..b996daf921ddc 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java @@ -121,6 +121,7 @@ public boolean isBlockingAllowed() { static Vertx vertx; static ExecutorService executor; boolean deleteUploadedFilesOnEnd = true; + List fileContentTypes; Path uploadPath; private List> scanCustomizers = new ArrayList<>(); @@ -149,6 +150,11 @@ public ResteasyReactiveUnitTest setDeleteUploadedFilesOnEnd(boolean deleteUpload return this; } + public ResteasyReactiveUnitTest setFileContentTypes(List fileContentTypes) { + this.fileContentTypes = fileContentTypes; + return this; + } + public ResteasyReactiveUnitTest setUploadPath(Path uploadPath) { this.uploadPath = uploadPath; return this; @@ -391,7 +397,7 @@ public Thread newThread(Runnable r) { DefaultRuntimeConfiguration runtimeConfiguration = new DefaultRuntimeConfiguration(Duration.ofMinutes(1), deleteUploadedFilesOnEnd, uploadPath != null ? uploadPath.toAbsolutePath().toString() : System.getProperty("java.io.tmpdir"), - defaultCharset, Optional.empty(), maxFormAttributeSize); + fileContentTypes, defaultCharset, Optional.empty(), maxFormAttributeSize); ResteasyReactiveDeploymentManager.RunnableApplication application = prepared.createApplication(runtimeConfiguration, new VertxRequestContextFactory(), executor); fieldInjectionSupport.runtimeInit(testClassLoader, application.getDeployment()); diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/EndingSlashTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/EndingSlashTest.java index 268d38265e89d..640d58e9a4704 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/EndingSlashTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/EndingSlashTest.java @@ -39,6 +39,14 @@ public void test() { .then() .statusCode(200) .body(equalTo("Hello World!")); + + get("/hello/world/1") + .then() + .statusCode(404); + + get("/hello/world/22") + .then() + .statusCode(404); } @Path("/hello") diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartFileContentTypeTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartFileContentTypeTest.java new file mode 100644 index 0000000000000..93c4771f78959 --- /dev/null +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartFileContentTypeTest.java @@ -0,0 +1,72 @@ +package org.jboss.resteasy.reactive.server.vertx.test.multipart; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.function.Supplier; + +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.server.vertx.test.framework.ResteasyReactiveUnitTest; +import org.jboss.resteasy.reactive.server.vertx.test.multipart.other.OtherPackageFormDataBase; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.restassured.RestAssured; + +public class MultipartFileContentTypeTest extends AbstractMultipartTest { + + private static final Path uploadDir = Paths.get("file-uploads"); + + @RegisterExtension + static ResteasyReactiveUnitTest test = new ResteasyReactiveUnitTest() + .setDeleteUploadedFilesOnEnd(false) + .setUploadPath(uploadDir) + .setFileContentTypes(List.of(MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_SVG_XML)) + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(FormDataBase.class, OtherPackageFormDataBase.class, FormData.class, Status.class, + OtherFormData.class, FormDataSameFileName.class, + OtherFormDataBase.class, + MultipartResource.class, OtherMultipartResource.class); + } + + }); + + private final File FILE = new File("./src/test/resources/test.html"); + + @BeforeEach + public void assertEmptyUploads() { + Assertions.assertTrue(isDirectoryEmpty(uploadDir)); + } + + @AfterEach + public void clearDirectory() { + clearDirectory(uploadDir); + } + + @Test + public void testFilePartWithExpectedContentType() throws IOException { + RestAssured.given() + .multiPart("octetStream", null, Files.readAllBytes(FILE.toPath()), MediaType.APPLICATION_OCTET_STREAM) + .multiPart("svgXml", null, Files.readAllBytes(FILE.toPath()), MediaType.APPLICATION_SVG_XML) + .accept("text/plain") + .when() + .post("/multipart/optional") + .then() + .statusCode(200); + + // ensure that the 2 uploaded files where created on disk + Assertions.assertEquals(2, uploadDir.toFile().listFiles().length); + } +} diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java index 14c35bfdf92a7..fc27042f39599 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java @@ -14,6 +14,7 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; public class MultipartOutputUsingBlockingEndpointsTest extends AbstractMultipartTest { @@ -46,18 +47,20 @@ public void testSimple() { @Test public void testWithFormData() { - String response = RestAssured.get("/multipart/output/with-form-data") + ExtractableResponse extractable = RestAssured.get("/multipart/output/with-form-data") .then() - .log().all() .contentType(ContentType.MULTIPART) .statusCode(200) - .extract().asString(); + .extract(); - assertContainsValue(response, "name", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_NAME); - assertContainsValue(response, "custom-surname", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_SURNAME); - assertContainsValue(response, "custom-status", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_STATUS); - assertContainsValue(response, "active", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_ACTIVE); - assertContainsValue(response, "values", MediaType.TEXT_PLAIN, "[one, two]"); + String body = extractable.asString(); + assertContainsValue(body, "name", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_NAME); + assertContainsValue(body, "custom-surname", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_SURNAME); + assertContainsValue(body, "custom-status", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_STATUS); + assertContainsValue(body, "active", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_ACTIVE); + assertContainsValue(body, "values", MediaType.TEXT_PLAIN, "[one, two]"); + + assertThat(extractable.header("Content-Type")).contains("boundary="); } @Test diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/DefaultMediaTypeTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/DefaultMediaTypeTest.java index 0dcc843024768..f930b447f4c70 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/DefaultMediaTypeTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/DefaultMediaTypeTest.java @@ -216,4 +216,28 @@ public void postInteger() throws Exception { String responseContent = response.readEntity(String.class); LOG.debug(String.format("Response: %s", responseContent)); } + + @Test + @DisplayName("Post Multi Media Type Consumer") + public void testConsumesMultiMediaType() { + WebTarget target = client.target(generateURL("/postMultiMediaTypeConsumer")); + Response response = target.request().post(Entity.entity("payload", "application/soap+xml")); + Assertions.assertEquals(Response.Status.OK.getStatusCode(), + response.getStatus()); + Assertions.assertEquals("postMultiMediaTypeConsumer", response.readEntity(String.class)); + + response = target.request().post(Entity.entity("payload", MediaType.TEXT_XML)); + Assertions.assertEquals(Response.Status.OK.getStatusCode(), + response.getStatus()); + Assertions.assertEquals("postMultiMediaTypeConsumer", response.readEntity(String.class)); + + response = target.request().post(Entity.entity("payload", "any/media-type")); + Assertions.assertEquals(Response.Status.OK.getStatusCode(), + response.getStatus()); + Assertions.assertEquals("any/media-type", response.readEntity(String.class)); + + response = target.request().post(Entity.entity("payload", "unexpected/media-type")); + Assertions.assertEquals(Response.Status.UNSUPPORTED_MEDIA_TYPE.getStatusCode(), + response.getStatus()); + } } diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/resource/DefaultMediaTypeResource.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/resource/DefaultMediaTypeResource.java index 993cb9f9a6eff..ba8f67206e065 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/resource/DefaultMediaTypeResource.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/resource/DefaultMediaTypeResource.java @@ -3,9 +3,11 @@ import java.util.Date; import javax.ws.rs.Consumes; +import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -75,4 +77,18 @@ public Response postIntegerProduce(String source) throws Exception { public Response postInteger(String source) throws Exception { return Response.ok().entity(5).build(); } + + @Path("postMultiMediaTypeConsumer") + @Consumes({ "application/soap+xml", MediaType.TEXT_XML }) + @POST + public Response postMultiMediaTypeConsumer() { + return Response.ok("postMultiMediaTypeConsumer").build(); + } + + @Path("postMultiMediaTypeConsumer") + @Consumes({ "any/media-type" }) + @POST + public Response postMultiMediaTypeConsumerAnyContentType(@HeaderParam(HttpHeaders.CONTENT_TYPE) String contentType) { + return Response.ok(contentType).build(); + } } diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.yml index 03a05295ef8ad..b42ba3bdf2f71 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.yml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.yml @@ -45,4 +45,7 @@ jobs: cache: 'maven' - name: Build with Maven - run: mvn -B formatter:validate clean install --file pom.xml + run: mvn -B clean verify -Dno-format + + - name: Build with Maven (Native) + run: mvn -B verify -Dnative -Dquarkus.native.container-build -Dnative.surefire.skip diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.yml index 9bd97b523e449..32bbddd82a3b5 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.yml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: - name: Maven release ${{steps.metadata.outputs.current-version}} run: | git checkout -b release - mvn -B release:prepare -Prelease -DpreparationGoals="clean install" -DreleaseVersion=${{steps.metadata.outputs.current-version}} -DdevelopmentVersion=${{steps.metadata.outputs.next-version}} + mvn -B release:prepare -Prelease -DreleaseVersion=${{steps.metadata.outputs.current-version}} -DdevelopmentVersion=${{steps.metadata.outputs.next-version}} if ! git diff --quiet docs/modules/ROOT/pages/includes/attributes.adoc; then git add docs/modules/ROOT/pages/includes/attributes.adoc git commit -m "Update stable version for documentation" @@ -68,19 +68,11 @@ jobs: git commit -m "Update stable version for documentation" # Move the tag after inclusion of documentation adjustments git tag -f ${{steps.metadata.outputs.current-version}} + # Push modified tag + git push origin refs/tags/${{steps.metadata.outputs.current-version}} -f fi # Go back to base branch git checkout ${{github.base_ref}} - - name: Push changes to ${{github.base_ref}} - uses: ad-m/github-push-action@v0.6.0 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - branch: ${{github.base_ref}} - - - name: Push tags - uses: ad-m/github-push-action@v0.6.0 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - tags: true - branch: ${{github.base_ref}} + - name: Push changes to ${{github.base_ref}} branch + run: git push diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/base/README.tpl.qute.md b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/base/README.tpl.qute.md index 1ee0f8fef47f3..d5bf52926285a 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/base/README.tpl.qute.md +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/base/README.tpl.qute.md @@ -1,4 +1,13 @@ -# {project.artifact-id} Project +{#if readme.include-default-content} +{#if project.name} +# {project.name} +{#else} +# {project.artifact-id} +{/if} +{#if project.description} + +> {project.description} +{/if} This project uses Quarkus, the Supersonic Subatomic Java Framework. @@ -60,3 +69,4 @@ If you want to learn more about building native executables, please consult {bui ## Provided Code {/if} +{/if} diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/codestart.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/codestart.yml index 180f54758c227..2822b07c03a85 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/codestart.yml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/codestart.yml @@ -25,5 +25,7 @@ language: artifact-id: quarkus-project version: 1.0.0-SNAPSHOT package-name: org.acme + readme: + include-default-content: true dependencies: - io.quarkus:quarkus-arc diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/github-action/base/.github/workflows/ci.tpl.qute.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/github-action/base/.github/workflows/ci.tpl.qute.yml index 59956fe437203..a5fe09ec507b8 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/github-action/base/.github/workflows/ci.tpl.qute.yml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/github-action/base/.github/workflows/ci.tpl.qute.yml @@ -1,6 +1,6 @@ -## This is basic continuous integration build for your Quarkus application. +## A basic GitHub Actions workflow for your Quarkus application. -name: Quarkus Codestart CI +name: CI build on: push: @@ -12,11 +12,17 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up JDK {java.version} - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: java-version: {java.version} + distribution: temurin + {#if buildtool.cli == 'gradle'} + cache: gradle + {#else} + cache: maven + {/if} - name: Build {#if buildtool.cli == 'gradle'} uses: eskatos/gradle-command-action@v1 diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java index 44bd3c7a8c5f2..e7976251f7915 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java @@ -55,12 +55,12 @@ public enum LayoutType { public static final String DEFAULT_QUARKIVERSE_PARENT_GROUP_ID = "io.quarkiverse"; public static final String DEFAULT_QUARKIVERSE_PARENT_ARTIFACT_ID = "quarkiverse-parent"; - public static final String DEFAULT_QUARKIVERSE_PARENT_VERSION = "10"; + public static final String DEFAULT_QUARKIVERSE_PARENT_VERSION = "12"; public static final String DEFAULT_QUARKIVERSE_NAMESPACE_ID = "quarkus-"; public static final String DEFAULT_QUARKIVERSE_GUIDE_URL = "https://quarkiverse.github.io/quarkiverse-docs/%s/dev/"; private static final String DEFAULT_SUREFIRE_PLUGIN_VERSION = "3.0.0-M7"; - private static final String DEFAULT_COMPILER_PLUGIN_VERSION = "3.8.1"; + private static final String DEFAULT_COMPILER_PLUGIN_VERSION = "3.10.1"; private final QuarkusExtensionCodestartProjectInputBuilder builder = QuarkusExtensionCodestartProjectInput.builder(); private final Path baseDir; diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateProject.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateProject.java index 7240d81cdce68..72a53d5c95ce6 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateProject.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateProject.java @@ -48,6 +48,8 @@ public interface CreateProjectKey { String NO_CODE = "codegen.no-code"; String EXAMPLE = "codegen.example"; String EXTRA_CODESTARTS = "codegen.extra-codestarts"; + + String DATA = "data"; } private QuarkusProject quarkusProject; @@ -195,6 +197,13 @@ public CreateProject noDockerfiles() { return noDockerfiles(true); } + public CreateProject data(String dataAsString) { + setValue(DATA, StringUtils.isNoneBlank(dataAsString) ? ToolsUtils.stringToMap(dataAsString, ",", "=") + : Collections.emptyMap()); + + return this; + } + public CreateProject setValue(String name, Object value) { if (value != null) { values.put(name, value); diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/CreateProjectCommandHandler.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/CreateProjectCommandHandler.java index ee77d4f09ba05..9fa13dd32274d 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/CreateProjectCommandHandler.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/CreateProjectCommandHandler.java @@ -1,5 +1,6 @@ package io.quarkus.devtools.commands.handlers; +import static io.quarkus.devtools.commands.CreateProject.CreateProjectKey.DATA; import static io.quarkus.devtools.commands.CreateProject.CreateProjectKey.EXAMPLE; import static io.quarkus.devtools.commands.CreateProject.CreateProjectKey.EXTENSIONS; import static io.quarkus.devtools.commands.CreateProject.CreateProjectKey.EXTRA_CODESTARTS; @@ -149,6 +150,7 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws .noDockerfiles(invocation.getValue(NO_DOCKERFILES, false)) .addData(platformData) .addData(toCodestartData(invocation.getValues())) + .addData(invocation.getValue(DATA, Collections.emptyMap())) .messageWriter(invocation.log()) .build(); invocation.log().info("-----------"); diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/AbstractGradleBuildFile.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/AbstractGradleBuildFile.java index dba8d5bd5a341..dd5ea2931ce1e 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/AbstractGradleBuildFile.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/AbstractGradleBuildFile.java @@ -46,16 +46,20 @@ public AbstractGradleBuildFile(final Path projectDirPath, final ExtensionCatalog public void writeToDisk() throws IOException { if (rootProjectPath != null) { Files.write(rootProjectPath.resolve(getSettingsGradlePath()), getModel().getRootSettingsContent().getBytes()); - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - getModel().getRootPropertiesContent().store(out, "Gradle properties"); - Files.write(rootProjectPath.resolve(GRADLE_PROPERTIES_PATH), - out.toByteArray()); + if (hasRootProjectFile(GRADLE_PROPERTIES_PATH)) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + getModel().getRootPropertiesContent().store(out, "Gradle properties"); + Files.write(rootProjectPath.resolve(GRADLE_PROPERTIES_PATH), + out.toByteArray()); + } } } else { writeToProjectFile(getSettingsGradlePath(), getModel().getSettingsContent().getBytes()); - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - getModel().getPropertiesContent().store(out, "Gradle properties"); - writeToProjectFile(GRADLE_PROPERTIES_PATH, out.toByteArray()); + if (hasProjectFile(GRADLE_PROPERTIES_PATH)) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + getModel().getPropertiesContent().store(out, "Gradle properties"); + writeToProjectFile(GRADLE_PROPERTIES_PATH, out.toByteArray()); + } } } writeToProjectFile(getBuildGradlePath(), getModel().getBuildContent().getBytes()); diff --git a/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/SnapshotTesting.java b/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/SnapshotTesting.java index bea70ac28e81c..d0a4578536456 100644 --- a/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/SnapshotTesting.java +++ b/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/SnapshotTesting.java @@ -174,7 +174,7 @@ public static AbstractPathAssert assertThatMatchSnapshot(Path fileToCheck, St final String snapshotNotFoundDescription = "corresponding snapshot file not found for " + snapshotIdentifier + " (Use -Dsnap to create it automatically)"; - final String description = "Snapshot is not matching (use -Dsnap to udpate it automatically): " + final String description = "Snapshot is not matching (use -Dsnap to update it automatically): " + snapshotIdentifier; if (isUTF8File(fileToCheck)) { assertThat(snapshotFile).as(snapshotNotFoundDescription).isRegularFile(); @@ -264,7 +264,7 @@ public static ListAssert assertThatDirectoryTreeMatchSnapshots(String sn .collect(toList()); return assertThat(tree) - .as("Snapshot is not matching (use -Dsnap to udpate it automatically):" + snapshotName) + .as("Snapshot is not matching (use -Dsnap to update it automatically):" + snapshotName) .containsExactlyInAnyOrderElementsOf(content); }); } diff --git a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/README.md b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/README.md index 6142e7fc97079..e088c8db5b9c5 100644 --- a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/README.md +++ b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/README.md @@ -1,4 +1,4 @@ -# test-codestart Project +# test-codestart This project uses Quarkus, the Supersonic Subatomic Java Framework. diff --git a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleNoWrapperGithubAction/.github_workflows_ci.yml b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleNoWrapperGithubAction/.github_workflows_ci.yml index f26a18ff19293..b91dae4af7558 100644 --- a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleNoWrapperGithubAction/.github_workflows_ci.yml +++ b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleNoWrapperGithubAction/.github_workflows_ci.yml @@ -1,6 +1,6 @@ -## This is basic continuous integration build for your Quarkus application. +## A basic GitHub Actions workflow for your Quarkus application. -name: Quarkus Codestart CI +name: CI build on: push: @@ -12,11 +12,13 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up JDK 11 - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: java-version: 11 + distribution: temurin + cache: gradle - name: Build uses: eskatos/gradle-command-action@v1 with: diff --git a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleWrapperGithubAction/.github_workflows_ci.yml b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleWrapperGithubAction/.github_workflows_ci.yml index 522d6dc1151da..214af798d2fbe 100644 --- a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleWrapperGithubAction/.github_workflows_ci.yml +++ b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleWrapperGithubAction/.github_workflows_ci.yml @@ -1,6 +1,6 @@ -## This is basic continuous integration build for your Quarkus application. +## A basic GitHub Actions workflow for your Quarkus application. -name: Quarkus Codestart CI +name: CI build on: push: @@ -12,10 +12,12 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up JDK 11 - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: java-version: 11 + distribution: temurin + cache: maven - name: Build run: ./gradlew build diff --git a/independent-projects/tools/pom.xml b/independent-projects/tools/pom.xml index 444edc532844a..05b53c1c5fa6a 100644 --- a/independent-projects/tools/pom.xml +++ b/independent-projects/tools/pom.xml @@ -42,7 +42,7 @@ - 3.8.1 + 3.10.1 1.6.0 2.12.13 4.4.0 @@ -55,14 +55,14 @@ 1.22 3.5.0.Final 3.8.6 - 4.9.0 + 4.10.0 3.0.0-M7 1.6.8 999-SNAPSHOT 21 2.11.0 1.13.2 - 3.0.4 + 3.0.5 registry-client diff --git a/integration-tests/csrf-reactive/src/main/java/io/quarkus/it/csrf/TestResource.java b/integration-tests/csrf-reactive/src/main/java/io/quarkus/it/csrf/TestResource.java index 9814e1d4fe4ae..d882897fcb82f 100644 --- a/integration-tests/csrf-reactive/src/main/java/io/quarkus/it/csrf/TestResource.java +++ b/integration-tests/csrf-reactive/src/main/java/io/quarkus/it/csrf/TestResource.java @@ -27,6 +27,9 @@ public class TestResource { @Inject Template csrfTokenForm; + @Inject + Template csrfTokenWithFormRead; + @Inject Template csrfTokenMultipart; @@ -40,6 +43,13 @@ public TemplateInstance getCsrfTokenForm() { return csrfTokenForm.instance(); } + @GET + @Path("/csrfTokenWithFormRead") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance getCsrfTokenWithFormRead() { + return csrfTokenWithFormRead.instance(); + } + @POST @Path("/csrfTokenForm") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @@ -48,6 +58,14 @@ public String postCsrfTokenForm(@FormParam("name") String name) { return name + ":" + routingContext.get("csrf_token_verified", false); } + @POST + @Path("/csrfTokenWithFormRead") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.TEXT_PLAIN) + public String postCsrfTokenWithFormRead() { + return "verified:" + routingContext.get("csrf_token_verified", false); + } + @GET @Path("/csrfTokenMultipart") @Produces(MediaType.TEXT_HTML) diff --git a/integration-tests/csrf-reactive/src/main/resources/application.properties b/integration-tests/csrf-reactive/src/main/resources/application.properties index d84f200ebf014..add9b0a990af0 100644 --- a/integration-tests/csrf-reactive/src/main/resources/application.properties +++ b/integration-tests/csrf-reactive/src/main/resources/application.properties @@ -1,4 +1,4 @@ quarkus.csrf-reactive.cookie-name=csrftoken -quarkus.csrf-reactive.create-token-path=/service/csrfTokenForm,/service/csrfTokenMultipart +quarkus.csrf-reactive.create-token-path=/service/csrfTokenForm,/service/csrfTokenWithFormRead,/service/csrfTokenMultipart quarkus.csrf-reactive.token-signature-key=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow diff --git a/integration-tests/csrf-reactive/src/main/resources/templates/csrfTokenWithFormRead.html b/integration-tests/csrf-reactive/src/main/resources/templates/csrfTokenWithFormRead.html new file mode 100644 index 0000000000000..d68a90c3849f3 --- /dev/null +++ b/integration-tests/csrf-reactive/src/main/resources/templates/csrfTokenWithFormRead.html @@ -0,0 +1,17 @@ + + + + +CSRF Token With Form Read Test + + +

    CSRF Test

    + +
    + + +

    Your Name:

    +

    +
    + + diff --git a/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java b/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java index f1976362b4cac..9c0302b75f8da 100644 --- a/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java +++ b/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java @@ -46,6 +46,31 @@ public void testCsrfTokenInForm() throws Exception { } } + @Test + public void testCsrfTokenWithFormRead() throws Exception { + try (final WebClient webClient = createWebClient()) { + + HtmlPage htmlPage = webClient.getPage("http://localhost:8081/service/csrfTokenWithFormRead"); + + assertEquals("CSRF Token With Form Read Test", htmlPage.getTitleText()); + + HtmlForm loginForm = htmlPage.getForms().get(0); + + loginForm.getInputByName("name").setValueAttribute("alice"); + + assertNotNull(webClient.getCookieManager().getCookie("csrftoken")); + + TextPage textPage = loginForm.getInputByName("submit").click(); + + assertEquals("verified:true", textPage.getContent()); + + textPage = webClient.getPage("http://localhost:8081/service/hello"); + assertEquals("hello", textPage.getContent()); + + webClient.getCookieManager().clearCookies(); + } + } + @Test public void testCsrfTokenInFormButNoCookie() throws Exception { try (final WebClient webClient = createWebClient()) { @@ -148,6 +173,25 @@ public void testWrongCsrfTokenFormValue() throws Exception { } } + @Test + public void testWrongCsrfTokenWithFormRead() throws Exception { + try (final WebClient webClient = createWebClient()) { + + HtmlPage htmlPage = webClient.getPage("http://localhost:8081/service/csrfTokenWithFormRead"); + + assertEquals("CSRF Token With Form Read Test", htmlPage.getTitleText()); + + assertNotNull(webClient.getCookieManager().getCookie("csrftoken")); + + RestAssured.given().urlEncodingEnabled(true) + .param("csrf-token", "wrong-value") + .post("/service/csrfTokenWithFormRead") + .then().statusCode(400); + + webClient.getCookieManager().clearCookies(); + } + } + private WebClient createWebClient() { WebClient webClient = new WebClient(); webClient.setCssErrorHandler(new SilentCssErrorHandler()); diff --git a/integration-tests/devmode/pom.xml b/integration-tests/devmode/pom.xml index b6ec10f4092e8..6a6c2b5ce8521 100644 --- a/integration-tests/devmode/pom.xml +++ b/integration-tests/devmode/pom.xml @@ -34,6 +34,11 @@ quarkus-grpc-deployment test + + io.quarkus + quarkus-kafka-client-deployment + test + io.quarkus quarkus-resteasy-reactive-qute-deployment diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/devconsole/DevConsoleKafkaSmokeTest.java b/integration-tests/devmode/src/test/java/io/quarkus/test/devconsole/DevConsoleKafkaSmokeTest.java new file mode 100644 index 0000000000000..bbf82bf28ab1b --- /dev/null +++ b/integration-tests/devmode/src/test/java/io/quarkus/test/devconsole/DevConsoleKafkaSmokeTest.java @@ -0,0 +1,29 @@ +package io.quarkus.test.devconsole; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +/** + * Note that this test cannot be placed under the relevant {@code -deployment} module because then the DEV UI processor would + * not be able to locate the template resources correctly. + */ +public class DevConsoleKafkaSmokeTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .withApplicationRoot( + (jar) -> jar.addAsResource(new StringAsset("quarkus.http.root-path=testing"), "application.properties")); + + @Test + public void testServices() { + RestAssured.get("testing/q/dev/io.quarkus.quarkus-kafka-client/kafka-dev-ui") + .then() + .statusCode(200).body(Matchers.containsString("Kafka Dev UI")); + } + +} diff --git a/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/AmazonLambdaCodestartTest.java b/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/AmazonLambdaCodestartTest.java new file mode 100644 index 0000000000000..5a84c39fa20d8 --- /dev/null +++ b/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/AmazonLambdaCodestartTest.java @@ -0,0 +1,32 @@ +package io.quarkus.devtools.codestarts.quarkus; + +import static io.quarkus.devtools.codestarts.quarkus.QuarkusCodestartCatalog.Language.JAVA; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.devtools.testing.codestarts.QuarkusCodestartTest; + +public class AmazonLambdaCodestartTest { + @RegisterExtension + public static QuarkusCodestartTest codestartTest = QuarkusCodestartTest.builder() + .codestarts("amazon-lambda") + .languages(JAVA) + .build(); + + @Test + void testContent() throws Throwable { + codestartTest.checkGeneratedSource("org.acme.lambda.GreetingLambda"); + codestartTest.checkGeneratedSource("org.acme.lambda.Person"); + + codestartTest.checkGeneratedTestSource("org.acme.lambda.LambdaHandlerTest"); + codestartTest.checkGeneratedTestSource("org.acme.lambda.LambdaHandlerTestIT"); + } + + @Test + @EnabledIfSystemProperty(named = "build-projects", matches = "true") + void buildAllProjectsForLocalUse() throws Throwable { + codestartTest.buildAllProjects(); + } +} diff --git a/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/AzureFunctionsHttpCodestartTest.java b/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/AzureFunctionsHttpCodestartTest.java new file mode 100644 index 0000000000000..934838f8ddfdc --- /dev/null +++ b/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/AzureFunctionsHttpCodestartTest.java @@ -0,0 +1,30 @@ +package io.quarkus.devtools.codestarts.quarkus; + +import static io.quarkus.devtools.codestarts.quarkus.QuarkusCodestartCatalog.Language.JAVA; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.devtools.testing.codestarts.QuarkusCodestartTest; + +public class AzureFunctionsHttpCodestartTest { + @RegisterExtension + public static QuarkusCodestartTest codestartTest = QuarkusCodestartTest.builder() + .codestarts("azure-functions-http") + .languages(JAVA) + .build(); + + @Test + void testContent() throws Throwable { + codestartTest.assertThatGeneratedFileMatchSnapshot(JAVA, "azure-config/function.json"); + codestartTest.assertThatGeneratedFileMatchSnapshot(JAVA, "azure-config/host.json"); + codestartTest.assertThatGeneratedFileMatchSnapshot(JAVA, "azure-config/local.settings.json"); + } + + @Test + @EnabledIfSystemProperty(named = "build-projects", matches = "true") + void buildAllProjectsForLocalUse() throws Throwable { + codestartTest.buildAllProjects(); + } +} diff --git a/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/FunqyAmazonLambdaCodestartTest.java b/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/FunqyAmazonLambdaCodestartTest.java new file mode 100644 index 0000000000000..208be34cf3ffd --- /dev/null +++ b/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/FunqyAmazonLambdaCodestartTest.java @@ -0,0 +1,32 @@ +package io.quarkus.devtools.codestarts.quarkus; + +import static io.quarkus.devtools.codestarts.quarkus.QuarkusCodestartCatalog.Language.JAVA; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.devtools.testing.codestarts.QuarkusCodestartTest; + +public class FunqyAmazonLambdaCodestartTest { + @RegisterExtension + public static QuarkusCodestartTest codestartTest = QuarkusCodestartTest.builder() + .codestarts("funqy-amazon-lambda") + .languages(JAVA) + .build(); + + @Test + void testContent() throws Throwable { + codestartTest.checkGeneratedSource("org.acme.funqy.GreetingFunction"); + codestartTest.checkGeneratedSource("org.acme.funqy.Person"); + + codestartTest.checkGeneratedTestSource("org.acme.funqy.FunqyTest"); + codestartTest.checkGeneratedTestSource("org.acme.funqy.FunqyIT"); + } + + @Test + @EnabledIfSystemProperty(named = "build-projects", matches = "true") + void buildAllProjectsForLocalUse() throws Throwable { + codestartTest.buildAllProjects(); + } +} diff --git a/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/FunqyKnativeEventsCodestartTest.java b/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/FunqyKnativeEventsCodestartTest.java new file mode 100644 index 0000000000000..712aef26d525c --- /dev/null +++ b/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/FunqyKnativeEventsCodestartTest.java @@ -0,0 +1,32 @@ +package io.quarkus.devtools.codestarts.quarkus; + +import static io.quarkus.devtools.codestarts.quarkus.QuarkusCodestartCatalog.Language.JAVA; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.devtools.testing.codestarts.QuarkusCodestartTest; + +public class FunqyKnativeEventsCodestartTest { + @RegisterExtension + public static QuarkusCodestartTest codestartTest = QuarkusCodestartTest.builder() + .codestarts("funqy-knative-events") + .languages(JAVA) + .build(); + + @Test + void testContent() throws Throwable { + codestartTest.checkGeneratedSource("org.acme.funqy.cloudevent.CloudEventGreeting"); + codestartTest.checkGeneratedSource("org.acme.funqy.cloudevent.Person"); + + codestartTest.checkGeneratedTestSource("org.acme.funqy.cloudevent.FunqyTest"); + codestartTest.checkGeneratedTestSource("org.acme.funqy.cloudevent.FunqyIT"); + } + + @Test + @EnabledIfSystemProperty(named = "build-projects", matches = "true") + void buildAllProjectsForLocalUse() throws Throwable { + codestartTest.buildAllProjects(); + } +} diff --git a/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/HibernateOrmPanacheCodestartTest.java b/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/HibernateOrmPanacheCodestartTest.java new file mode 100644 index 0000000000000..0c9c49c2e2d1e --- /dev/null +++ b/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/HibernateOrmPanacheCodestartTest.java @@ -0,0 +1,30 @@ +package io.quarkus.devtools.codestarts.quarkus; + +import static io.quarkus.devtools.codestarts.quarkus.QuarkusCodestartCatalog.Language.JAVA; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.devtools.testing.codestarts.QuarkusCodestartTest; +import io.quarkus.maven.ArtifactKey; + +public class HibernateOrmPanacheCodestartTest { + + @RegisterExtension + public static QuarkusCodestartTest codestartTest = QuarkusCodestartTest.builder() + .codestarts("hibernate-orm") + .extension(new ArtifactKey("io.quarkus", "quarkus-jdbc-h2")) + .extension(new ArtifactKey("io.quarkus", "quarkus-hibernate-orm-panache")) + .languages(JAVA) + .build(); + + @Test + void testContent() throws Throwable { + codestartTest.checkGeneratedSource("org.acme.MyEntity"); + } + + @Test + void buildAllProjectsForLocalUse() throws Throwable { + codestartTest.buildAllProjects(); + } +} diff --git a/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/HibernateOrmPanacheKotlinCodestartTest.java b/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/HibernateOrmPanacheKotlinCodestartTest.java new file mode 100644 index 0000000000000..7fe535c799a6e --- /dev/null +++ b/integration-tests/devtools/src/test/java/io/quarkus/devtools/codestarts/quarkus/HibernateOrmPanacheKotlinCodestartTest.java @@ -0,0 +1,30 @@ +package io.quarkus.devtools.codestarts.quarkus; + +import static io.quarkus.devtools.codestarts.quarkus.QuarkusCodestartCatalog.Language.KOTLIN; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.devtools.testing.codestarts.QuarkusCodestartTest; +import io.quarkus.maven.ArtifactKey; + +public class HibernateOrmPanacheKotlinCodestartTest { + + @RegisterExtension + public static QuarkusCodestartTest codestartTest = QuarkusCodestartTest.builder() + .codestarts("hibernate-orm") + .extension(new ArtifactKey("io.quarkus", "quarkus-jdbc-h2")) + .extension(new ArtifactKey("io.quarkus", "quarkus-hibernate-orm-panache-kotlin")) + .languages(KOTLIN) + .build(); + + @Test + void testContent() throws Throwable { + codestartTest.checkGeneratedSource("org.acme.MyKotlinEntity"); + } + + @Test + void buildAllProjectsForLocalUse() throws Throwable { + codestartTest.buildAllProjects(); + } +} diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/AmazonLambdaCodestartTest/testContent/src_main_java_ilove_quark_us_lambda_GreetingLambda.java b/integration-tests/devtools/src/test/resources/__snapshots__/AmazonLambdaCodestartTest/testContent/src_main_java_ilove_quark_us_lambda_GreetingLambda.java new file mode 100644 index 0000000000000..9792c2cad234f --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/AmazonLambdaCodestartTest/testContent/src_main_java_ilove_quark_us_lambda_GreetingLambda.java @@ -0,0 +1,12 @@ +package ilove.quark.us.lambda; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +public class GreetingLambda implements RequestHandler { + + @Override + public String handleRequest(Person input, Context context) { + return "Hello " + input.getName(); + } +} diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/AmazonLambdaCodestartTest/testContent/src_main_java_ilove_quark_us_lambda_Person.java b/integration-tests/devtools/src/test/resources/__snapshots__/AmazonLambdaCodestartTest/testContent/src_main_java_ilove_quark_us_lambda_Person.java new file mode 100644 index 0000000000000..b1e0887460116 --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/AmazonLambdaCodestartTest/testContent/src_main_java_ilove_quark_us_lambda_Person.java @@ -0,0 +1,15 @@ +package ilove.quark.us.lambda; + +public class Person { + + private String name; + + public String getName() { + return name; + } + + public Person setName(String name) { + this.name = name; + return this; + } +} diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/AmazonLambdaCodestartTest/testContent/src_test_java_ilove_quark_us_lambda_LambdaHandlerTest.java b/integration-tests/devtools/src/test/resources/__snapshots__/AmazonLambdaCodestartTest/testContent/src_test_java_ilove_quark_us_lambda_LambdaHandlerTest.java new file mode 100644 index 0000000000000..a4cd06203b932 --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/AmazonLambdaCodestartTest/testContent/src_test_java_ilove_quark_us_lambda_LambdaHandlerTest.java @@ -0,0 +1,31 @@ +package ilove.quark.us.lambda; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.containsString; + +@QuarkusTest +public class LambdaHandlerTest { + + @Test + public void testSimpleLambdaSuccess() throws Exception { + // you test your lambdas by invoking on http://localhost:8081 + // this works in dev mode too + + Person in = new Person(); + in.setName("Stu"); + given() + .contentType("application/json") + .accept("application/json") + .body(in) + .when() + .post() + .then() + .statusCode(200) + .body(containsString("Hello Stu")); + } + +} diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/AmazonLambdaCodestartTest/testContent/src_test_java_ilove_quark_us_lambda_LambdaHandlerTestIT.java b/integration-tests/devtools/src/test/resources/__snapshots__/AmazonLambdaCodestartTest/testContent/src_test_java_ilove_quark_us_lambda_LambdaHandlerTestIT.java new file mode 100644 index 0000000000000..f090fed3c3bb5 --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/AmazonLambdaCodestartTest/testContent/src_test_java_ilove_quark_us_lambda_LambdaHandlerTestIT.java @@ -0,0 +1,9 @@ +package ilove.quark.us.lambda; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class LambdaHandlerTestIT extends LambdaHandlerTest { + + // Execute the same tests but in native mode. +} \ No newline at end of file diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/AzureFunctionsHttpCodestartTest/testContent/azure-config_function.json b/integration-tests/devtools/src/test/resources/__snapshots__/AzureFunctionsHttpCodestartTest/testContent/azure-config_function.json new file mode 100644 index 0000000000000..c527fe7c0345c --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/AzureFunctionsHttpCodestartTest/testContent/azure-config_function.json @@ -0,0 +1,17 @@ +{ + "scriptFile" : "../test-codestart-1.0.0-codestart.jar", + "entryPoint" : "io.quarkus.azure.functions.resteasy.runtime.Function.run", + "bindings" : [ { + "type" : "httpTrigger", + "direction" : "in", + "name" : "req", + "route" : "{*path}", + "methods" : [ "GET", "POST", "HEAD", "PUT", "OPTIONS", "DELETE" ], + "dataType" : "binary", + "authLevel" : "ANONYMOUS" + }, { + "type" : "http", + "direction" : "out", + "name" : "$return" + } ] +} \ No newline at end of file diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/AzureFunctionsHttpCodestartTest/testContent/azure-config_host.json b/integration-tests/devtools/src/test/resources/__snapshots__/AzureFunctionsHttpCodestartTest/testContent/azure-config_host.json new file mode 100644 index 0000000000000..a8fe8c4c8e513 --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/AzureFunctionsHttpCodestartTest/testContent/azure-config_host.json @@ -0,0 +1,3 @@ +{ + "version": "2.0" +} diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/AzureFunctionsHttpCodestartTest/testContent/azure-config_local.settings.json b/integration-tests/devtools/src/test/resources/__snapshots__/AzureFunctionsHttpCodestartTest/testContent/azure-config_local.settings.json new file mode 100644 index 0000000000000..8804ca029cf22 --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/AzureFunctionsHttpCodestartTest/testContent/azure-config_local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "", + "FUNCTIONS_WORKER_RUNTIME": "java" + } +} diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/FunqyAmazonLambdaCodestartTest/testContent/src_main_java_ilove_quark_us_funqy_GreetingFunction.java b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyAmazonLambdaCodestartTest/testContent/src_main_java_ilove_quark_us_funqy_GreetingFunction.java new file mode 100644 index 0000000000000..78e977a2d3180 --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyAmazonLambdaCodestartTest/testContent/src_main_java_ilove_quark_us_funqy_GreetingFunction.java @@ -0,0 +1,11 @@ +package ilove.quark.us.funqy; + +import io.quarkus.funqy.Funq; + +public class GreetingFunction { + + @Funq + public String myFunqyGreeting(Person friend) { + return "Hello " + friend.getName(); + } +} diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/FunqyAmazonLambdaCodestartTest/testContent/src_main_java_ilove_quark_us_funqy_Person.java b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyAmazonLambdaCodestartTest/testContent/src_main_java_ilove_quark_us_funqy_Person.java new file mode 100644 index 0000000000000..a55a1fde0ff0f --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyAmazonLambdaCodestartTest/testContent/src_main_java_ilove_quark_us_funqy_Person.java @@ -0,0 +1,19 @@ +package ilove.quark.us.funqy; + +public class Person { + private String name; + + public Person() {} + + public Person(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/FunqyAmazonLambdaCodestartTest/testContent/src_test_java_ilove_quark_us_funqy_FunqyIT.java b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyAmazonLambdaCodestartTest/testContent/src_test_java_ilove_quark_us_funqy_FunqyIT.java new file mode 100644 index 0000000000000..832ce6b1bb785 --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyAmazonLambdaCodestartTest/testContent/src_test_java_ilove_quark_us_funqy_FunqyIT.java @@ -0,0 +1,10 @@ +package ilove.quark.us.funqy; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class FunqyIT extends FunqyTest { + + // Run the same tests + +} diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/FunqyAmazonLambdaCodestartTest/testContent/src_test_java_ilove_quark_us_funqy_FunqyTest.java b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyAmazonLambdaCodestartTest/testContent/src_test_java_ilove_quark_us_funqy_FunqyTest.java new file mode 100644 index 0000000000000..b0f17923ae8ce --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyAmazonLambdaCodestartTest/testContent/src_test_java_ilove_quark_us_funqy_FunqyTest.java @@ -0,0 +1,31 @@ +package ilove.quark.us.funqy; + +import org.junit.jupiter.api.Test; +import io.quarkus.test.junit.QuarkusTest; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.containsString; + +@QuarkusTest +public class FunqyTest { + + @Test + public void testFunqyLambda() throws Exception { + // you test your lambdas by invoking on http://localhost:8081 + // this works in dev mode too + + Person in = new Person(); + in.setName("Bill"); + given() + .contentType("application/json") + .accept("application/json") + .body(in) + .when() + .post() + .then() + .statusCode(200) + .body(containsString("Hello Bill")); + } + +} + diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/FunqyKnativeEventsCodestartTest/testContent/src_main_java_ilove_quark_us_funqy_cloudevent_CloudEventGreeting.java b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyKnativeEventsCodestartTest/testContent/src_main_java_ilove_quark_us_funqy_cloudevent_CloudEventGreeting.java new file mode 100644 index 0000000000000..f1d17280fac4e --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyKnativeEventsCodestartTest/testContent/src_main_java_ilove_quark_us_funqy_cloudevent_CloudEventGreeting.java @@ -0,0 +1,13 @@ +package ilove.quark.us.funqy.cloudevent; + +import io.quarkus.funqy.Funq; +import org.jboss.logging.Logger; + +public class CloudEventGreeting { + private static final Logger log = Logger.getLogger(CloudEventGreeting.class); + + @Funq + public void myCloudEventGreeting(Person input) { + log.info("Hello " + input.getName()); + } +} diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/FunqyKnativeEventsCodestartTest/testContent/src_main_java_ilove_quark_us_funqy_cloudevent_Person.java b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyKnativeEventsCodestartTest/testContent/src_main_java_ilove_quark_us_funqy_cloudevent_Person.java new file mode 100644 index 0000000000000..cfcff91771b77 --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyKnativeEventsCodestartTest/testContent/src_main_java_ilove_quark_us_funqy_cloudevent_Person.java @@ -0,0 +1,19 @@ +package ilove.quark.us.funqy.cloudevent; + +public class Person { + private String name; + + public Person() {} + + public Person(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/FunqyKnativeEventsCodestartTest/testContent/src_test_java_ilove_quark_us_funqy_cloudevent_FunqyIT.java b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyKnativeEventsCodestartTest/testContent/src_test_java_ilove_quark_us_funqy_cloudevent_FunqyIT.java new file mode 100644 index 0000000000000..a7d713e8d54f4 --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyKnativeEventsCodestartTest/testContent/src_test_java_ilove_quark_us_funqy_cloudevent_FunqyIT.java @@ -0,0 +1,10 @@ +package ilove.quark.us.funqy.cloudevent; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class FunqyIT extends FunqyTest { + + // Run the same tests + +} diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/FunqyKnativeEventsCodestartTest/testContent/src_test_java_ilove_quark_us_funqy_cloudevent_FunqyTest.java b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyKnativeEventsCodestartTest/testContent/src_test_java_ilove_quark_us_funqy_cloudevent_FunqyTest.java new file mode 100644 index 0000000000000..9eb6515ddcc7f --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/FunqyKnativeEventsCodestartTest/testContent/src_test_java_ilove_quark_us_funqy_cloudevent_FunqyTest.java @@ -0,0 +1,28 @@ +package ilove.quark.us.funqy.cloudevent; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +@QuarkusTest +public class FunqyTest { + + @Test + public void testCloudEvent() { + RestAssured.given().contentType("application/json") + .header("ce-specversion", "1.0") + .header("ce-id", UUID.randomUUID().toString()) + .header("ce-type", "myCloudEventGreeting") + .header("ce-source", "test") + .body("{ \"name\": \"Bill\" }") + .post("/") + .then().statusCode(204); + } +} diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/HibernateOrmCodestartTest/testContent/src_main_java_ilove_quark_us_MyEntity.java b/integration-tests/devtools/src/test/resources/__snapshots__/HibernateOrmCodestartTest/testContent/src_main_java_ilove_quark_us_MyEntity.java index 5e512e9cbdf02..09329e1879b2b 100644 --- a/integration-tests/devtools/src/test/resources/__snapshots__/HibernateOrmCodestartTest/testContent/src_main_java_ilove_quark_us_MyEntity.java +++ b/integration-tests/devtools/src/test/resources/__snapshots__/HibernateOrmCodestartTest/testContent/src_main_java_ilove_quark_us_MyEntity.java @@ -15,7 +15,7 @@ * * public void doSomething() { * MyEntity entity1 = new MyEntity(); - * entity1.setField("field-1"); + * entity1.field = "field-1"; * em.persist(entity1); * * List entities = em.createQuery("from MyEntity", MyEntity.class).getResultList(); @@ -24,24 +24,9 @@ */ @Entity public class MyEntity { - private Long id; - private String field; - @Id @GeneratedValue - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getField() { - return field; - } + public Long id; - public void setField(String field) { - this.field = field; - } + public String field; } diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/HibernateOrmPanacheCodestartTest/testContent/src_main_java_ilove_quark_us_MyEntity.java b/integration-tests/devtools/src/test/resources/__snapshots__/HibernateOrmPanacheCodestartTest/testContent/src_main_java_ilove_quark_us_MyEntity.java new file mode 100644 index 0000000000000..7af15a045441e --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/HibernateOrmPanacheCodestartTest/testContent/src_main_java_ilove_quark_us_MyEntity.java @@ -0,0 +1,29 @@ +package ilove.quark.us; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import javax.persistence.Entity; + + +/** + * Example JPA entity defined as a Panache Entity. + * An ID field of Long type is provided, if you want to define your own ID field extends PanacheEntityBase instead. + * + * This uses the active record pattern, you can also use the repository pattern instead: + * . + * + * Usage (more example on the documentation) + * + * {@code + * public void doSomething() { + * MyEntity entity1 = new MyEntity(); + * entity1.field = "field-1"; + * entity1.persist(); + * + * List entities = MyEntity.listAll(); + * } + * } + */ +@Entity +public class MyEntity extends PanacheEntity { + public String field; +} diff --git a/integration-tests/devtools/src/test/resources/__snapshots__/HibernateOrmPanacheKotlinCodestartTest/testContent/src_main_kotlin_ilove_quark_us_MyKotlinEntity.kt b/integration-tests/devtools/src/test/resources/__snapshots__/HibernateOrmPanacheKotlinCodestartTest/testContent/src_main_kotlin_ilove_quark_us_MyKotlinEntity.kt new file mode 100644 index 0000000000000..1bacaf93ee59b --- /dev/null +++ b/integration-tests/devtools/src/test/resources/__snapshots__/HibernateOrmPanacheKotlinCodestartTest/testContent/src_main_kotlin_ilove_quark_us_MyKotlinEntity.kt @@ -0,0 +1,31 @@ +package ilove.quark.us + +import javax.persistence.Entity +import javax.persistence.GeneratedValue +import javax.persistence.Id + +/** + * Example JPA entity. + * + * To use it, get access to a JPA EntityManager via injection. + * + * ```kotlin + * @Inject + * lateinit var em:EntityManager; + * + * fun doSomething() { + * val entity1 = MyKotlinEntity(); + * entity1.field = "field-1" + * em.persist(entity1); + * + * val entities:List = em.createQuery("from MyEntity", MyKotlinEntity::class.java).getResultList() + * } + * ``` + */ +@Entity +class MyKotlinEntity { + @get:GeneratedValue + @get:Id + var id: Long? = null + var field: String? = null +} \ No newline at end of file diff --git a/integration-tests/elytron-resteasy-reactive/src/main/java/io/quarkus/it/resteasy/reactive/elytron/RootResource.java b/integration-tests/elytron-resteasy-reactive/src/main/java/io/quarkus/it/resteasy/reactive/elytron/RootResource.java index a52c6bf2ee811..5cf108735b179 100644 --- a/integration-tests/elytron-resteasy-reactive/src/main/java/io/quarkus/it/resteasy/reactive/elytron/RootResource.java +++ b/integration-tests/elytron-resteasy-reactive/src/main/java/io/quarkus/it/resteasy/reactive/elytron/RootResource.java @@ -57,6 +57,13 @@ public String user(@Context SecurityContext sec) { return sec.getUserPrincipal().getName(); } + @GET + @Path("/employee") + @RolesAllowed("${employees-config-property}") + public String employee(@Context SecurityContext sec) { + return sec.getUserPrincipal().getName(); + } + @GET @Path("/attributes") @Authenticated diff --git a/integration-tests/elytron-resteasy-reactive/src/main/resources/application.properties b/integration-tests/elytron-resteasy-reactive/src/main/resources/application.properties index 84c4d19bd9f24..578e5f42da902 100644 --- a/integration-tests/elytron-resteasy-reactive/src/main/resources/application.properties +++ b/integration-tests/elytron-resteasy-reactive/src/main/resources/application.properties @@ -7,3 +7,4 @@ quarkus.security.users.embedded.users.poul=poul quarkus.security.users.embedded.roles.poul=interns quarkus.security.users.embedded.plain-text=true quarkus.http.auth.basic=true +employees-config-property=employees \ No newline at end of file diff --git a/integration-tests/elytron-resteasy-reactive/src/test/java/io/quarkus/it/resteasy/reactive/elytron/RootResourceTest.java b/integration-tests/elytron-resteasy-reactive/src/test/java/io/quarkus/it/resteasy/reactive/elytron/RootResourceTest.java index 59b81b5cfe7c3..207cc0b9e255e 100644 --- a/integration-tests/elytron-resteasy-reactive/src/test/java/io/quarkus/it/resteasy/reactive/elytron/RootResourceTest.java +++ b/integration-tests/elytron-resteasy-reactive/src/test/java/io/quarkus/it/resteasy/reactive/elytron/RootResourceTest.java @@ -38,4 +38,15 @@ void testGet() { .body(is("get success")); } + @Test + void testRolesAllowedConfigExpression() { + given() + .auth().preemptive().basic("john", Users.password("john")) + .when() + .get("/employee") + .then() + .statusCode(200) + .body(is("john")); + } + } diff --git a/integration-tests/gradle/src/main/resources/basic-kotlin-application-project/src/main/kotlin/org/acme/MyMainClass.kt b/integration-tests/gradle/src/main/resources/basic-kotlin-application-project/src/main/kotlin/org/acme/MyMainClass.kt new file mode 100644 index 0000000000000..7dc20cae46c69 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/basic-kotlin-application-project/src/main/kotlin/org/acme/MyMainClass.kt @@ -0,0 +1,13 @@ +package `basic-kotlin-application-project`.src.main.kotlin.org.acme + +import io.quarkus.runtime.Quarkus +import io.quarkus.runtime.QuarkusApplication +import io.quarkus.runtime.annotations.QuarkusMain + +@QuarkusMain(name = "my-main") +class MyMainClass : QuarkusApplication { + override fun run(vararg args: String?): Int { + Quarkus.waitForExit() + return 0 + } +} diff --git a/integration-tests/gradle/src/main/resources/builder/simple-module-project/src/test/java/Dummy.java b/integration-tests/gradle/src/main/resources/builder/simple-module-project/src/test/java/Dummy.java new file mode 100644 index 0000000000000..6662fdfe2a5e0 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/builder/simple-module-project/src/test/java/Dummy.java @@ -0,0 +1,2 @@ +public class Dummy { +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/conditional-dependencies-kotlin/build.gradle.kts b/integration-tests/gradle/src/main/resources/conditional-dependencies-kotlin/build.gradle.kts index 656ce8ff7e049..065ace8968752 100644 --- a/integration-tests/gradle/src/main/resources/conditional-dependencies-kotlin/build.gradle.kts +++ b/integration-tests/gradle/src/main/resources/conditional-dependencies-kotlin/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - kotlin("jvm") version "1.7.21" - kotlin("plugin.allopen") version "1.7.21" + kotlin("jvm") version "1.7.22" + kotlin("plugin.allopen") version "1.7.22" id("io.quarkus") } diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/builder/QuarkusModelBuilderTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/builder/QuarkusModelBuilderTest.java index ccb64db50f69c..213ea5df6a14c 100644 --- a/integration-tests/gradle/src/test/java/io/quarkus/gradle/builder/QuarkusModelBuilderTest.java +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/builder/QuarkusModelBuilderTest.java @@ -46,7 +46,7 @@ public void shouldLoadSimpleModuleTestModel() throws URISyntaxException, IOExcep @Test public void shouldLoadSimpleModuleDevModel() throws URISyntaxException, IOException { File projectDir = getResourcesProject("builder/simple-module-project"); - final ApplicationModel quarkusModel = QuarkusGradleModelFactory.create(projectDir, "DEVELOPMENT"); + final ApplicationModel quarkusModel = QuarkusGradleModelFactory.create(projectDir, "DEVELOPMENT", "testClasses"); assertNotNull(quarkusModel); assertNotNull(quarkusModel.getApplicationModule()); @@ -63,7 +63,8 @@ public void shouldLoadSimpleModuleDevModel() throws URISyntaxException, IOExcept public void shouldLoadMultiModuleTestModel() throws URISyntaxException, IOException { File projectDir = getResourcesProject("builder/multi-module-project"); - final ApplicationModel quarkusModel = QuarkusGradleModelFactory.create(new File(projectDir, "application"), "TEST"); + final ApplicationModel quarkusModel = QuarkusGradleModelFactory.create(new File(projectDir, "application"), "TEST", + "testClasses"); assertNotNull(quarkusModel); diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java index 363fd589281d4..8b74ce145ad59 100644 --- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java @@ -45,6 +45,7 @@ @Path("test") public class TestEndpoint { + private static final String LINE_SEPARATOR = System.getProperty("line.separator"); // fake unused injection point to force ArC to not remove this otherwise I can't mock it in the tests @Inject MockablePersonRepository mockablePersonRepository; @@ -1188,6 +1189,13 @@ public String testProjection() { person = Person.find("name = ?1", "2").project(PersonName.class).firstResult(); Assertions.assertEquals("2", person.name); + person = Person.find(String.format( + "select uniqueName, name%sfrom io.quarkus.it.panache.Person%swhere name = ?1", + LINE_SEPARATOR, LINE_SEPARATOR), "2") + .project(PersonName.class) + .firstResult(); + Assertions.assertEquals("2", person.name); + person = Person.find("name = :name", Parameters.with("name", "2")).project(PersonName.class).firstResult(); Assertions.assertEquals("2", person.name); diff --git a/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java b/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java index 7352e9f637d16..c75e6cf594091 100644 --- a/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java +++ b/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java @@ -4,16 +4,19 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.stream.Collectors; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; +import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -25,11 +28,14 @@ import org.infinispan.counter.api.CounterConfiguration; import org.infinispan.counter.api.CounterManager; import org.infinispan.counter.api.CounterType; +import org.infinispan.counter.api.Storage; import org.infinispan.counter.api.StrongCounter; +import org.infinispan.counter.api.WeakCounter; import org.infinispan.query.dsl.Query; import org.infinispan.query.dsl.QueryFactory; import io.quarkus.infinispan.client.Remote; +import io.smallrye.common.annotation.Blocking; @Path("/test") public class TestServlet { @@ -111,16 +117,38 @@ public String ickleQueryAuthorSurname(@PathParam("id") String name) { .collect(Collectors.joining(",", "[", "]")); } + @Path("counter/{id}") + @POST + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public boolean defineCounter(@PathParam("id") String id, @QueryParam("type") String type, + @QueryParam("storage") String storage) { + cacheSetup.ensureStarted(); + CounterConfiguration configuration = counterManager.getConfiguration(id); + if (configuration == null) { + configuration = CounterConfiguration.builder(CounterType.valueOf(type)).storage(Storage.valueOf(storage)).build(); + return counterManager.defineCounter(id, configuration); + } + return true; + } + @Path("incr/{id}") @GET @Produces(MediaType.TEXT_PLAIN) + @Blocking public CompletionStage incrementCounter(@PathParam("id") String id) { cacheSetup.ensureStarted(); CounterConfiguration configuration = counterManager.getConfiguration(id); if (configuration == null) { - configuration = CounterConfiguration.builder(CounterType.BOUNDED_STRONG).build(); - counterManager.defineCounter(id, configuration); + return CompletableFuture.completedFuture(0L); } + + if (configuration.type() == CounterType.WEAK) { + WeakCounter weakCounter = counterManager.getWeakCounter(id); + weakCounter.sync().increment(); + return CompletableFuture.completedFuture(weakCounter.getValue()); + } + StrongCounter strongCounter = counterManager.getStrongCounter(id); return strongCounter.incrementAndGet(); } diff --git a/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java b/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java index fac14fdf755d7..eaddbd5bafb1c 100644 --- a/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java +++ b/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java @@ -4,6 +4,8 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import org.infinispan.counter.api.CounterType; +import org.infinispan.counter.api.Storage; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; @@ -33,8 +35,30 @@ public void testIckleQuery() { @Test public void testCounterIncrement() { - String initialValue = RestAssured.when().get("test/incr/somevalue").body().print(); - String nextValue = RestAssured.when().get("test/incr/somevalue").body().print(); + RestAssured.given() + .queryParam("type", CounterType.BOUNDED_STRONG) + .queryParam("storage", Storage.VOLATILE) + .post("test/counter/strong-1").body().print(); + + RestAssured.given() + .queryParam("type", CounterType.WEAK) + .queryParam("storage", Storage.VOLATILE) + .post("test/counter/weak-1").body().print(); + + RestAssured.given() + .queryParam("type", CounterType.UNBOUNDED_STRONG) + .queryParam("storage", Storage.PERSISTENT) + .post("test/counter/strong-2").body().print(); + + assertCounterIncrement("strong-1"); + assertCounterIncrement("weak-1"); + assertCounterIncrement("strong-2"); + } + + private void assertCounterIncrement(String counterName) { + String initialValue = RestAssured.given() + .get("test/incr/" + counterName).body().print(); + String nextValue = RestAssured.when().get("test/incr/" + counterName).body().print(); assertEquals(Integer.parseInt(initialValue) + 1, Integer.parseInt(nextValue)); } diff --git a/integration-tests/kafka-devservices/src/main/resources/application.properties b/integration-tests/kafka-devservices/src/main/resources/application.properties index 8e83b2b6d39c7..bdd341fbda057 100644 --- a/integration-tests/kafka-devservices/src/main/resources/application.properties +++ b/integration-tests/kafka-devservices/src/main/resources/application.properties @@ -5,6 +5,7 @@ quarkus.log.category.\"org.apache.zookeeper\".level=WARN # enable health check quarkus.kafka.health.enabled=true +quarkus.kafka.devservices.provider=kafka-native quarkus.kafka.devservices.topic-partitions.test=2 quarkus.kafka.devservices.topic-partitions.test-consumer=3 quarkus.kafka.devservices.topic-partitions-timeout=4S diff --git a/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingApplication.kt b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingApplication.kt new file mode 100644 index 0000000000000..e7bd494373b0f --- /dev/null +++ b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingApplication.kt @@ -0,0 +1,11 @@ +package io.quarkus.it.kotser + +import io.quarkus.runtime.Quarkus.run +import io.quarkus.runtime.annotations.QuarkusMain + +@QuarkusMain +class GreetingApplication + +fun main(args: Array) { + run(*args) +} diff --git a/integration-tests/kubernetes-service-binding-jdbc/src/test/resources/k8s-sb/fruit-db/database b/integration-tests/kubernetes-service-binding-jdbc/src/test/resources/k8s-sb/fruit-db/database deleted file mode 100644 index 39cd31f5ffa44..0000000000000 --- a/integration-tests/kubernetes-service-binding-jdbc/src/test/resources/k8s-sb/fruit-db/database +++ /dev/null @@ -1 +0,0 @@ -quarkus_test diff --git a/integration-tests/kubernetes-service-binding-jdbc/src/test/resources/k8s-sb/fruit-db/host b/integration-tests/kubernetes-service-binding-jdbc/src/test/resources/k8s-sb/fruit-db/host deleted file mode 100644 index 2fbb50c4a8dc7..0000000000000 --- a/integration-tests/kubernetes-service-binding-jdbc/src/test/resources/k8s-sb/fruit-db/host +++ /dev/null @@ -1 +0,0 @@ -localhost diff --git a/integration-tests/kubernetes-service-binding-jdbc/src/test/resources/k8s-sb/fruit-db/port b/integration-tests/kubernetes-service-binding-jdbc/src/test/resources/k8s-sb/fruit-db/port deleted file mode 100644 index 7e30bed39582f..0000000000000 --- a/integration-tests/kubernetes-service-binding-jdbc/src/test/resources/k8s-sb/fruit-db/port +++ /dev/null @@ -1 +0,0 @@ -5431 diff --git a/integration-tests/kubernetes-service-binding-jdbc/src/test/resources/k8s-sb/fruit-db/uri b/integration-tests/kubernetes-service-binding-jdbc/src/test/resources/k8s-sb/fruit-db/uri new file mode 100644 index 0000000000000..02dca2d9bc469 --- /dev/null +++ b/integration-tests/kubernetes-service-binding-jdbc/src/test/resources/k8s-sb/fruit-db/uri @@ -0,0 +1 @@ +jdbc:postgresql://localhost:5431/quarkus_test diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KnativeWithExistingDeploymentResourceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KnativeWithExistingDeploymentResourceTest.java new file mode 100644 index 0000000000000..dc1a745e169be --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KnativeWithExistingDeploymentResourceTest.java @@ -0,0 +1,66 @@ + +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +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.quarkus.kubernetes.spi.CustomProjectRootBuildItem; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KnativeWithExistingDeploymentResourceTest { + + private static final String APP_NAME = "knative-with-existing-deployment-resource"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource(APP_NAME + ".properties") + .addCustomResourceEntry(Path.of("src", "main", "kubernetes", "knative.yml"), + "manifests/" + APP_NAME + "/knative.yml") + .addBuildChainCustomizerEntries( + new QuarkusProdModeTest.BuildChainCustomizerEntry( + KubernetesWithCustomResourcesTest.CustomProjectRootBuildItemProducerProdMode.class, + Collections.singletonList(CustomProjectRootBuildItem.class), Collections.emptyList())); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("knative.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("knative.yml")); + + List kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("knative.yml")); + + assertThat(kubernetesList).filteredOn(i -> "Deployment".equals(i.getKind()) + && "example".equals(i.getMetadata().getName())) + .singleElement() + .satisfies(e -> { + assertThat(e).isInstanceOfSatisfying(Deployment.class, deployment -> { + assertThat(deployment.getSpec().getTemplate().getSpec().getContainers()).allSatisfy(container -> { + assertThat(container.getLivenessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8080); + assertThat(container.getReadinessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8080); + + assertThat(container.getResources().getRequests().get("memory").getAmount()).isEqualTo("128"); + assertThat(container.getResources().getLimits().get("memory").getAmount()).isEqualTo("768"); + }); + }); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KnativeWithExtendedPropertiesTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KnativeWithExtendedPropertiesTest.java new file mode 100644 index 0000000000000..33cd3dce48266 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KnativeWithExtendedPropertiesTest.java @@ -0,0 +1,75 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +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.knative.serving.v1.Service; +import io.fabric8.kubernetes.api.model.HasMetadata; +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 KnativeWithExtendedPropertiesTest { + + private static final String APP_NAME = "knative-with-extended-properties"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .overrideConfigKey("quarkus.kubernetes.deployment-target", "knative") + .overrideConfigKey("quarkus.knative.revision-auto-scaling.container-concurrency", "5") + .overrideConfigKey("quarkus.knative.min-scale", "5") + .overrideConfigKey("quarkus.knative.max-scale", "10") + .overrideConfigKey("quarkus.knative.image-pull-policy", "Never") + .setLogFileName("k8s.log") + .setForcedDependencies( + Collections.singletonList(new AppArtifact("io.quarkus", "quarkus-kubernetes", Version.getVersion()))); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("knative.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("knative.yml")); + List kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("knative.yml")); + assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Service.class, s -> { + assertThat(s.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo(APP_NAME); + }); + + assertThat(s.getSpec()).satisfies(serviceSpec -> { + assertThat(serviceSpec.getTemplate()).satisfies(template -> { + assertThat(template.getMetadata()).satisfies(m -> { + assertThat(m.getAnnotations()).contains(entry("autoscaling.knative.dev/minScale", "5")); + assertThat(m.getAnnotations()).contains(entry("autoscaling.knative.dev/maxScale", "10")); + }); + assertThat(template.getSpec()).satisfies(revisionSpec -> { + assertThat(revisionSpec.getContainerConcurrency()).isEqualTo(5); + + assertThat(revisionSpec.getContainers().get(0)).satisfies(c -> { + assertThat(c.getImagePullPolicy()).isEqualTo("Never"); + }); + }); + }); + }); + }); + } +} \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithAutoMongoBindingTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithAutoMongoBindingTest.java index 562c5cedc1172..93c5aafb9b8a0 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithAutoMongoBindingTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithAutoMongoBindingTest.java @@ -20,12 +20,14 @@ public class KubernetesWithAutoMongoBindingTest { + private static final String APP_NAME = "kubernetes-with-auto-mongo-binding"; + @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) - .setApplicationName("kubernetes-with-auto-mogno-binding") + .setApplicationName(APP_NAME) .setApplicationVersion("0.1-SNAPSHOT") - .withConfigurationResource("kubernetes-with-auto-mongo-binding.properties") + .withConfigurationResource(APP_NAME + ".properties") .setLogFileName("k8s.log") .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-mongodb-client", Version.getVersion()), @@ -47,7 +49,7 @@ public void assertGeneratedResources() throws IOException { assertThat(kubernetesList).filteredOn(i -> "Deployment".equals(i.getKind())).singleElement().satisfies(i -> { assertThat(i).isInstanceOfSatisfying(Deployment.class, d -> { assertThat(d.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("kubernetes-with-auto-mogno-binding"); + assertThat(m.getName()).isEqualTo(APP_NAME); }); assertThat(d.getSpec()).satisfies(deploymentSpec -> { assertThat(deploymentSpec.getTemplate()).satisfies(t -> { @@ -61,7 +63,7 @@ public void assertGeneratedResources() throws IOException { assertThat(kubernetesList).filteredOn(i -> "ServiceBinding".equals(i.getKind())).singleElement().satisfies(i -> { assertThat(i).isInstanceOfSatisfying(ServiceBinding.class, s -> { assertThat(s.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("kubernetes-with-auto-mogno-binding-mongodb"); + assertThat(m.getName()).isEqualTo(APP_NAME + "-mongodb"); }); assertThat(s.getSpec()).satisfies(spec -> { assertThat(spec.getApplication()).satisfies(a -> { diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithAutoMysqlBindingTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithAutoMysqlBindingTest.java index 848d754ef98c9..110a747273aee 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithAutoMysqlBindingTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithAutoMysqlBindingTest.java @@ -20,12 +20,14 @@ public class KubernetesWithAutoMysqlBindingTest { + private static final String APP_NAME = "kubernetes-with-auto-mysql-binding"; + @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) - .setApplicationName("kubernetes-with-auto-mysql-binding") + .setApplicationName(APP_NAME) .setApplicationVersion("0.1-SNAPSHOT") - .withConfigurationResource("kubernetes-with-auto-mysql-binding.properties") + .withConfigurationResource(APP_NAME + ".properties") .setLogFileName("k8s.log") .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()), @@ -48,7 +50,7 @@ public void assertGeneratedResources() throws IOException { assertThat(kubernetesList).filteredOn(i -> "Deployment".equals(i.getKind())).singleElement().satisfies(i -> { assertThat(i).isInstanceOfSatisfying(Deployment.class, d -> { assertThat(d.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("kubernetes-with-auto-mysql-binding"); + assertThat(m.getName()).isEqualTo(APP_NAME); }); assertThat(d.getSpec()).satisfies(deploymentSpec -> { assertThat(deploymentSpec.getTemplate()).satisfies(t -> { @@ -62,7 +64,7 @@ public void assertGeneratedResources() throws IOException { assertThat(kubernetesList).filteredOn(i -> "ServiceBinding".equals(i.getKind())).singleElement().satisfies(i -> { assertThat(i).isInstanceOfSatisfying(ServiceBinding.class, s -> { assertThat(s.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("kubernetes-with-auto-mysql-binding-mysql"); + assertThat(m.getName()).isEqualTo(APP_NAME + "-mysql"); }); assertThat(s.getSpec()).satisfies(spec -> { assertThat(spec.getApplication()).satisfies(a -> { diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithAutoPostgresBindingTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithAutoPostgresBindingTest.java index 3b6a8d10adebe..686365ee1df7a 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithAutoPostgresBindingTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithAutoPostgresBindingTest.java @@ -20,12 +20,14 @@ public class KubernetesWithAutoPostgresBindingTest { + private static final String APP_NAME = "kubernetes-with-auto-postgres-binding"; + @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) - .setApplicationName("kubernetes-with-auto-postgres-binding") + .setApplicationName(APP_NAME) .setApplicationVersion("0.1-SNAPSHOT") - .withConfigurationResource("kubernetes-with-auto-postgres-binding.properties") + .withConfigurationResource(APP_NAME + ".properties") .setLogFileName("k8s.log") .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()), @@ -48,7 +50,7 @@ public void assertGeneratedResources() throws IOException { assertThat(kubernetesList).filteredOn(i -> "Deployment".equals(i.getKind())).singleElement().satisfies(i -> { assertThat(i).isInstanceOfSatisfying(Deployment.class, d -> { assertThat(d.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("kubernetes-with-auto-postgres-binding"); + assertThat(m.getName()).isEqualTo(APP_NAME); }); assertThat(d.getSpec()).satisfies(deploymentSpec -> { assertThat(deploymentSpec.getTemplate()).satisfies(t -> { @@ -62,7 +64,7 @@ public void assertGeneratedResources() throws IOException { assertThat(kubernetesList).filteredOn(i -> "ServiceBinding".equals(i.getKind())).singleElement().satisfies(i -> { assertThat(i).isInstanceOfSatisfying(ServiceBinding.class, s -> { assertThat(s.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("kubernetes-with-auto-postgres-binding-postgresql"); + assertThat(m.getName()).isEqualTo(APP_NAME + "-postgresql"); }); assertThat(s.getSpec()).satisfies(spec -> { assertThat(spec.getApplication()).satisfies(a -> { diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithExistingCronJobResourceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithExistingCronJobResourceTest.java index bba1599d46b1c..d55bff64b5dea 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithExistingCronJobResourceTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithExistingCronJobResourceTest.java @@ -37,7 +37,7 @@ public class KubernetesWithExistingCronJobResourceTest { .addBuildChainCustomizerEntries( new QuarkusProdModeTest.BuildChainCustomizerEntry( KubernetesWithCustomResourcesTest.CustomProjectRootBuildItemProducerProdMode.class, - Collections.singletonList(CustomProjectRootBuildItem.class), Collections.emptyList()));; + Collections.singletonList(CustomProjectRootBuildItem.class), Collections.emptyList())); @ProdBuildResults private ProdModeTestResults prodModeTestResults; diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithIngressTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithIngressTest.java index 8fb44b85173f5..0c7ce2cb16c83 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithIngressTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithIngressTest.java @@ -21,12 +21,14 @@ public class KubernetesWithIngressTest { + private static final String APP_NAME = "kubernetes-with-ingress"; + @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) - .setApplicationName("kubernetes-with-ingress") + .setApplicationName(APP_NAME) .setApplicationVersion("0.1-SNAPSHOT") - .withConfigurationResource("kubernetes-with-ingress.properties") + .withConfigurationResource(APP_NAME + ".properties") .setLogFileName("k8s.log") .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()))); @@ -44,15 +46,7 @@ public void assertGeneratedResources() throws IOException { assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Deployment.class, d -> { assertThat(d.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("kubernetes-with-ingress"); - }); - - assertThat(d.getSpec()).satisfies(deploymentSpec -> { - assertThat(deploymentSpec.getTemplate()).satisfies(t -> { - assertThat(t.getSpec()).satisfies(podSpec -> { - - }); - }); + assertThat(m.getName()).isEqualTo(APP_NAME); }); }); @@ -60,7 +54,11 @@ public void assertGeneratedResources() throws IOException { assertThat(item).isInstanceOfSatisfying(Ingress.class, ingress -> { //Check that labels and annotations are also applied to Routes (#10260) assertThat(ingress.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("kubernetes-with-ingress"); + assertThat(m.getName()).isEqualTo(APP_NAME); + }); + + assertThat(ingress.getSpec()).satisfies(spec -> { + assertThat(spec.getIngressClassName()).isEqualTo("Nginx"); }); assertThat(ingress.getSpec().getRules()).allSatisfy(rule -> { diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithSemiAutoPostgresBindingTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithSemiAutoPostgresBindingTest.java index ef435fb183291..42db3f78a2294 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithSemiAutoPostgresBindingTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithSemiAutoPostgresBindingTest.java @@ -20,12 +20,14 @@ public class KubernetesWithSemiAutoPostgresBindingTest { + private static final String APP_NAME = "kubernetes-with-semi-auto-postgres-binding"; + @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) - .setApplicationName("kubernetes-with-semi-auto-postgres-binding") + .setApplicationName(APP_NAME) .setApplicationVersion("0.1-SNAPSHOT") - .withConfigurationResource("kubernetes-with-semi-auto-postgres-binding.properties") + .withConfigurationResource(APP_NAME + ".properties") .setLogFileName("k8s.log") .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()), @@ -48,7 +50,7 @@ public void assertGeneratedResources() throws IOException { assertThat(kubernetesList).filteredOn(i -> "Deployment".equals(i.getKind())).singleElement().satisfies(i -> { assertThat(i).isInstanceOfSatisfying(Deployment.class, d -> { assertThat(d.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("kubernetes-with-semi-auto-postgres-binding"); + assertThat(m.getName()).isEqualTo(APP_NAME); }); assertThat(d.getSpec()).satisfies(deploymentSpec -> { assertThat(deploymentSpec.getTemplate()).satisfies(t -> { @@ -62,7 +64,7 @@ public void assertGeneratedResources() throws IOException { assertThat(kubernetesList).filteredOn(i -> "ServiceBinding".equals(i.getKind())).singleElement().satisfies(i -> { assertThat(i).isInstanceOfSatisfying(ServiceBinding.class, s -> { assertThat(s.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("kubernetes-with-semi-auto-postgres-binding-postgresql"); + assertThat(m.getName()).isEqualTo(APP_NAME + "-postgresql"); }); assertThat(s.getSpec()).satisfies(spec -> { assertThat(spec.getApplication()).satisfies(a -> { diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithServiceBindingTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithServiceBindingTest.java index 70acd7f848af9..ed8defbe17c7a 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithServiceBindingTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithServiceBindingTest.java @@ -21,12 +21,14 @@ public class KubernetesWithServiceBindingTest { + private static final String APP_NAME = "kubernetes-with-service-binding"; + @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) - .setApplicationName("kubernetes-with-service-binding") + .setApplicationName(APP_NAME) .setApplicationVersion("0.1-SNAPSHOT") - .withConfigurationResource("kubernetes-with-service-binding.properties") + .withConfigurationResource(APP_NAME + ".properties") .setLogFileName("k8s.log") .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()), @@ -47,7 +49,7 @@ public void assertGeneratedResources() throws IOException { assertThat(kubernetesList).filteredOn(i -> "Deployment".equals(i.getKind())).singleElement().satisfies(i -> { assertThat(i).isInstanceOfSatisfying(Deployment.class, d -> { assertThat(d.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("kubernetes-with-service-binding"); + assertThat(m.getName()).isEqualTo(APP_NAME); }); assertThat(d.getSpec()).satisfies(deploymentSpec -> { assertThat(deploymentSpec.getTemplate()).satisfies(t -> { @@ -61,7 +63,7 @@ public void assertGeneratedResources() throws IOException { assertThat(kubernetesList).filteredOn(i -> "ServiceBinding".equals(i.getKind())).singleElement().satisfies(i -> { assertThat(i).isInstanceOfSatisfying(ServiceBinding.class, s -> { assertThat(s.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("kubernetes-with-service-binding-my-db"); + assertThat(m.getName()).isEqualTo(APP_NAME + "-my-db"); }); assertThat(s.getSpec()).satisfies(spec -> { assertThat(spec.getApplication()).satisfies(a -> { diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithCustomRouteResourceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithCustomRouteResourceTest.java new file mode 100644 index 0000000000000..93e7595de9b74 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithCustomRouteResourceTest.java @@ -0,0 +1,81 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.openshift.api.model.Route; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.Version; +import io.quarkus.kubernetes.spi.CustomProjectRootBuildItem; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestBuildStep; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class OpenshiftWithCustomRouteResourceTest { + + private static final String APP_NAME = "openshift-with-custom-route-resource"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource(APP_NAME + ".properties") + .addCustomResourceEntry(Path.of("src", "main", "kubernetes", "openshift.yml"), + "manifests/" + APP_NAME + "/openshift.yml") + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-openshift", Version.getVersion()))) + .addBuildChainCustomizerEntries( + new QuarkusProdModeTest.BuildChainCustomizerEntry(CustomProjectRootBuildItemProducerProdMode.class, + Collections.singletonList(CustomProjectRootBuildItem.class), Collections.emptyList())); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("openshift.yml")) + .satisfies(p -> assertThat(p.toFile().listFiles()).hasSize(2)); + List openshiftList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("openshift.yml")); + assertThat(openshiftList).filteredOn(i -> "Route".equals(i.getKind())).singleElement().satisfies(i -> { + assertThat(i).isInstanceOfSatisfying(Route.class, r -> { + assertThat(r.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo(APP_NAME); + assertThat(m.getLabels()).contains(entry("foo", "bar")); + assertThat(m.getAnnotations()).contains(entry("bar", "baz")); + assertThat(m.getAnnotations()).contains(entry("kubernetes.io/tls-acme", "true")); + }); + assertThat(r.getSpec().getPort().getTargetPort().getStrVal()).isEqualTo("http"); + assertThat(r.getSpec().getTo().getKind()).isEqualTo("Service"); + assertThat(r.getSpec().getTo().getName()).isEqualTo(APP_NAME); + }); + }); + } + + public static class CustomProjectRootBuildItemProducerProdMode extends ProdModeTestBuildStep { + + public CustomProjectRootBuildItemProducerProdMode(Map testContext) { + super(testContext); + } + + @Override + public void execute(BuildContext context) { + context.produce(new CustomProjectRootBuildItem( + (Path) getTestContext().get(QuarkusProdModeTest.BUILD_CONTEXT_CUSTOM_SOURCES_PATH_KEY))); + } + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithDeploymentResourceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithDeploymentResourceTest.java index d4e5ae89ea863..4b86271cfdcbf 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithDeploymentResourceTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithDeploymentResourceTest.java @@ -2,6 +2,7 @@ package io.quarkus.it.kubernetes; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; import java.io.IOException; import java.nio.file.Path; @@ -29,10 +30,12 @@ // public class OpenshiftWithDeploymentResourceTest { + private static final String NAME = "openshift-with-deployment-resource"; + @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) - .setApplicationName("openshift-with-deployment-resource") + .setApplicationName(NAME) .setApplicationVersion("0.1-SNAPSHOT") .overrideConfigKey("quarkus.openshift.deployment-kind", "Deployment") .overrideConfigKey("quarkus.openshift.replicas", "3") @@ -55,17 +58,21 @@ public void assertGeneratedResources() throws IOException { assertThat(kubernetesList).filteredOn(h -> "BuildConfig".equals(h.getKind())).hasSize(1); assertThat(kubernetesList).filteredOn(h -> "ImageStream".equals(h.getKind())).hasSize(2); assertThat(kubernetesList).filteredOn(h -> "ImageStream".equals(h.getKind()) - && h.getMetadata().getName().equals("openshift-with-deployment-resource")).hasSize(1); + && h.getMetadata().getName().equals(NAME)).hasSize(1); assertThat(kubernetesList).filteredOn(i -> i instanceof Deployment).singleElement().satisfies(i -> { assertThat(i).isInstanceOfSatisfying(Deployment.class, d -> { assertThat(d.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("openshift-with-deployment-resource"); + assertThat(m.getName()).isEqualTo(NAME); }); assertThat(d.getSpec()).satisfies(deploymentSpec -> { assertThat(deploymentSpec.getReplicas()).isEqualTo(3); assertThat(deploymentSpec.getTemplate()).satisfies(t -> { + assertThat(t.getMetadata()).satisfies(metadata -> assertThat(metadata.getLabels()).containsAnyOf( + entry("app.kubernetes.io/name", NAME), + entry("app.kubernetes.io/version", "0.1-SNAPSHOT"))); + assertThat(t.getSpec()).satisfies(podSpec -> { assertThat(podSpec.getContainers()).singleElement().satisfies(container -> { assertThat(container.getImage()) diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithServiceBindingTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithServiceBindingTest.java index 8168a307a496a..e5a65ebae4cea 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithServiceBindingTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithServiceBindingTest.java @@ -21,12 +21,14 @@ public class OpenshiftWithServiceBindingTest { + private static final String APP_NAME = "openshift-with-service-binding"; + @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) - .setApplicationName("openshift-with-service-binding") + .setApplicationName(APP_NAME) .setApplicationVersion("0.1-SNAPSHOT") - .withConfigurationResource("openshift-with-service-binding.properties") + .withConfigurationResource(APP_NAME + ".properties") .setLogFileName("k8s.log") .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-openshift", Version.getVersion()), @@ -47,7 +49,7 @@ public void assertGeneratedResources() throws IOException { assertThat(kubernetesList).filteredOn(i -> "DeploymentConfig".equals(i.getKind())).singleElement().satisfies(i -> { assertThat(i).isInstanceOfSatisfying(DeploymentConfig.class, d -> { assertThat(d.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("openshift-with-service-binding"); + assertThat(m.getName()).isEqualTo(APP_NAME); }); }); }); @@ -55,7 +57,7 @@ public void assertGeneratedResources() throws IOException { assertThat(kubernetesList).filteredOn(i -> "ServiceBinding".equals(i.getKind())).singleElement().satisfies(i -> { assertThat(i).isInstanceOfSatisfying(ServiceBinding.class, s -> { assertThat(s.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("openshift-with-service-binding-my-db"); + assertThat(m.getName()).isEqualTo(APP_NAME + "-my-db"); }); assertThat(s.getSpec()).satisfies(spec -> { assertThat(spec.getApplication()).satisfies(a -> { diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientAndExistingResourcesTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientAndExistingResourcesTest.java index dd32c26359737..43e6f8ba9639e 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientAndExistingResourcesTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientAndExistingResourcesTest.java @@ -30,7 +30,8 @@ public class WithKubernetesClientAndExistingResourcesTest { .withConfigurationResource("kubernetes-with-" + APPLICATION_NAME + ".properties") .addCustomResourceEntry(Path.of("src", "main", "kubernetes", "kubernetes.yml"), "manifests/kubernetes-with-" + APPLICATION_NAME + "/kubernetes.yml") - .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-kubernetes-client", Version.getVersion()))) + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-kubernetes-client", Version.getVersion()), + Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()))) .addBuildChainCustomizerEntries( new QuarkusProdModeTest.BuildChainCustomizerEntry( KubernetesWithCustomResourcesTest.CustomProjectRootBuildItemProducerProdMode.class, @@ -58,12 +59,17 @@ public void assertGeneratedResources() throws IOException { } }); - assertThat(kubernetesList).filteredOn(h -> "ServiceAccount".equals(h.getKind())).singleElement().satisfies(h -> { - assertThat(h.getMetadata().getName()).isEqualTo(APPLICATION_NAME); - }); + assertThat(kubernetesList).filteredOn(h -> "ServiceAccount".equals(h.getKind())).singleElement() + .satisfies(h -> assertThat(h.getMetadata().getName()).isEqualTo(APPLICATION_NAME)); + + assertThat(kubernetesList).filteredOn(h -> "RoleBinding".equals(h.getKind())).singleElement() + .satisfies(h -> assertThat(h.getMetadata().getName()).isEqualTo(APPLICATION_NAME + "-view")); - assertThat(kubernetesList).filteredOn(h -> "RoleBinding".equals(h.getKind())).singleElement().satisfies(h -> { - assertThat(h.getMetadata().getName()).isEqualTo(APPLICATION_NAME + "-view"); + // check that if quarkus.kubernetes.namespace is set, "manually" set namespaces are not overwritten + assertThat(kubernetesList).filteredOn(h -> "ConfigMap".equals(h.getKind())).singleElement().satisfies(h -> { + final var metadata = h.getMetadata(); + assertThat(metadata.getName()).isEqualTo("foo"); + assertThat(metadata.getNamespace()).isEqualTo("foo"); }); } } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/knative-with-existing-deployment-resource.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/knative-with-existing-deployment-resource.properties new file mode 100644 index 0000000000000..dd506612391bb --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/knative-with-existing-deployment-resource.properties @@ -0,0 +1,5 @@ +# Configuration file +quarkus.kubernetes.deployment-target=knative + +quarkus.knative.resources.limits.memory=768Mi +quarkus.knative.resources.requests.memory=256Mi \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-auto-postgres-binding.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-auto-postgres-binding.properties index 3f62f1265363d..ee2a4ec049780 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-auto-postgres-binding.properties +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-auto-postgres-binding.properties @@ -1 +1 @@ -quarkus.datasource.db-kind=postgresql \ No newline at end of file +quarkus.datasource.db-kind=postgresql diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-client-existing-resources.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-client-existing-resources.properties index e69de29bb2d1d..2dac144175834 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-client-existing-resources.properties +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-client-existing-resources.properties @@ -0,0 +1 @@ +quarkus.kubernetes.namespace=bar \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-ingress.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-ingress.properties index ddc494034f0e4..abbb1ba05102e 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-ingress.properties +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-ingress.properties @@ -1 +1,2 @@ -quarkus.kubernetes.ingress.expose=true \ No newline at end of file +quarkus.kubernetes.ingress.expose=true +quarkus.kubernetes.ingress.ingress-class-name=Nginx \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/knative-with-existing-deployment-resource/knative.yml b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/knative-with-existing-deployment-resource/knative.yml new file mode 100644 index 0000000000000..6e317a1744e9d --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/knative-with-existing-deployment-resource/knative.yml @@ -0,0 +1,49 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: example +spec: + replicas: 1 + selector: + matchLabels: + name: example + template: + metadata: + labels: + name: example + spec: + containers: + - image: docker.io/group/app + name: example + livenessProbe: + failureThreshold: 3 + httpGet: + path: /health/live + port: 8080 + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 10 + readinessProbe: + failureThreshold: 3 + httpGet: + path: /health/ready + port: 8080 + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 10 + ports: + - containerPort: 8080 + name: http + protocol: TCP + env: + - name: REGISTRY_AUTH_ANONYMOUS_READ_ACCESS_ENABLED + value: "true" + resources: + limits: + memory: 768Mi + requests: + memory: 128Mi \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/kubernetes-with-client-existing-resources/kubernetes.yml b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/kubernetes-with-client-existing-resources/kubernetes.yml index 50b0ff7a15371..ac0f2031d2cf3 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/kubernetes-with-client-existing-resources/kubernetes.yml +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/kubernetes-with-client-existing-resources/kubernetes.yml @@ -30,4 +30,11 @@ spec: targetPort: 27017 selector: name: my-service - type: ClusterIP \ No newline at end of file + type: ClusterIP +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: foo + namespace: foo +--- \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/openshift-with-custom-route-resource/openshift.yml b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/openshift-with-custom-route-resource/openshift.yml new file mode 100644 index 0000000000000..d715a98a7353c --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/openshift-with-custom-route-resource/openshift.yml @@ -0,0 +1,8 @@ +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: openshift-with-custom-route-resource +spec: + tls: + termination: edge + insecureEdgeTerminationPolicy: Redirect \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-custom-route-resource.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-custom-route-resource.properties new file mode 100644 index 0000000000000..a7300637a33bf --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-custom-route-resource.properties @@ -0,0 +1,6 @@ +quarkus.kubernetes.deployment-target=openshift +quarkus.openshift.labels.foo=bar +quarkus.openshift.annotations.bar=baz +quarkus.openshift.group=grp + +quarkus.openshift.route.annotations."kubernetes.io/tls-acme"=true \ No newline at end of file diff --git a/integration-tests/main/src/main/java/io/quarkus/it/rest/RBACSecuredResource.java b/integration-tests/main/src/main/java/io/quarkus/it/rest/RBACSecuredResource.java index 3582318588e5d..033c9cbf79319 100644 --- a/integration-tests/main/src/main/java/io/quarkus/it/rest/RBACSecuredResource.java +++ b/integration-tests/main/src/main/java/io/quarkus/it/rest/RBACSecuredResource.java @@ -29,6 +29,13 @@ public String forTesterOnly() { return "forTesterOnly"; } + @GET + @RolesAllowed("${tester-config-exp}") + @Path("forTesterOnlyConfigExp") + public String forTesterOnlyConfigExp() { + return "forTesterOnlyConfigExp"; + } + @GET @RolesAllowed("tester") @Path("forTesterOnlyWithMethodParamAnnotations") diff --git a/integration-tests/main/src/main/resources/application.properties b/integration-tests/main/src/main/resources/application.properties index 3c85f6fb16ebb..1cc81a74d60c0 100644 --- a/integration-tests/main/src/main/resources/application.properties +++ b/integration-tests/main/src/main/resources/application.properties @@ -58,4 +58,7 @@ quarkus.native.additional-build-args =-H:ReflectionConfigurationFiles=reflection quarkus.class-loading.removed-resources."io.quarkus\:quarkus-integration-test-shared-library"=io/quarkus/it/shared/RemovedResource.class quarkus.class-loading.removed-resources."io.quarkus\:quarkus-integration-test-main"=io/quarkus/it/rest/RemovedJaxRsApplication.class # Enable callbacks for integration tests -quarkus.test.enable-callbacks-for-integration-tests=true \ No newline at end of file +quarkus.test.enable-callbacks-for-integration-tests=true + +# @RolesAllowed value is configuration expression +tester-config-exp=tester \ No newline at end of file diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/RBACAccessTest.java b/integration-tests/main/src/test/java/io/quarkus/it/main/RBACAccessTest.java index 1453f5473daf9..68d2fa027fc3b 100644 --- a/integration-tests/main/src/test/java/io/quarkus/it/main/RBACAccessTest.java +++ b/integration-tests/main/src/test/java/io/quarkus/it/main/RBACAccessTest.java @@ -25,6 +25,15 @@ public void shouldRestrictAccessToSpecificRole() { Optional.of("forTesterOnly")); } + @Test + public void shouldRestrictAccessToSpecificRoleConfigExp() { + String path = "/rbac-secured/forTesterOnlyConfigExp"; + assertForAnonymous(path, 401, Optional.empty()); + assertStatusAndContent(RestAssured.given().auth().preemptive().basic("stuart", "test"), path, 403, Optional.empty()); + assertStatusAndContent(RestAssured.given().auth().preemptive().basic("scott", "jb0ss"), path, 200, + Optional.of("forTesterOnlyConfigExp")); + } + @Test public void shouldRestrictAccessToSpecificRoleAndMethodParameterAnnotationsShouldntAffectAnything() { String path = "/rbac-secured/forTesterOnlyWithMethodParamAnnotations"; diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml index 3398446768dad..6a6a9d86e68d7 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml @@ -5,7 +5,7 @@ io.quarkiverse quarkiverse-parent - 10 + 12 io.quarkiverse.my-quarkiverse-ext quarkus-my-quarkiverse-ext-parent @@ -18,7 +18,7 @@ docs
    - 3.8.1 + 3.10.1 11 UTF-8 UTF-8 diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml index 960fb8cc5e9b1..c4e836f7ba286 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml @@ -12,7 +12,7 @@ runtime - 3.8.1 + 3.10.1 ${surefire-plugin.version} 11 UTF-8 diff --git a/integration-tests/mongodb-panache-kotlin/src/main/kotlin/io/quarkus/it/mongodb/panache/test/TestResource.kt b/integration-tests/mongodb-panache-kotlin/src/main/kotlin/io/quarkus/it/mongodb/panache/test/TestResource.kt index 7e751030fdf9a..16faeac99b4a9 100644 --- a/integration-tests/mongodb-panache-kotlin/src/main/kotlin/io/quarkus/it/mongodb/panache/test/TestResource.kt +++ b/integration-tests/mongodb-panache-kotlin/src/main/kotlin/io/quarkus/it/mongodb/panache/test/TestResource.kt @@ -1,5 +1,6 @@ package io.quarkus.it.mongodb.panache.test +import com.mongodb.ReadPreference import com.mongodb.client.model.Collation import com.mongodb.client.model.CollationStrength import io.quarkus.mongodb.panache.kotlin.PanacheQuery @@ -90,6 +91,16 @@ class TestResource { ).size ) + // find with options + Assertions.assertEquals( + 5, + TestImperativeEntity.find("category", "category0") + .withBatchSize(2) + .withReadPreference(ReadPreference.nearest()) + .list() + .size + ) + // regex val entityWithUpperCase = TestImperativeEntity("title11", "upperCaseCategory", "desc") entityWithUpperCase.persist() @@ -260,6 +271,16 @@ class TestResource { ).size ) + // find with options + Assertions.assertEquals( + 5, + testImperativeRepository.find("category", "category0") + .withBatchSize(2) + .withReadPreference(ReadPreference.nearest()) + .list() + .size + ) + // regex val entityWithUpperCase = TestImperativeEntity("title11", "upperCaseCate)gory", "desc") testImperativeRepository.persist(entityWithUpperCase) @@ -504,6 +525,17 @@ class TestResource { ).await().indefinitely().size ) + // find with options + Assertions.assertEquals( + 5, + TestReactiveEntity.find("category", "category0") + .withBatchSize(2) + .withReadPreference(ReadPreference.nearest()) + .list() + .await().indefinitely() + .size + ) + // regex val entityWithUpperCase = TestReactiveEntity("title11", "upperCaseCategory", "desc") entityWithUpperCase.persist().await().indefinitely() @@ -698,6 +730,17 @@ class TestResource { ).await().indefinitely().size ) + // find with options + Assertions.assertEquals( + 5, + testReactiveRepository.find("category", "category0") + .withBatchSize(2) + .withReadPreference(ReadPreference.nearest()) + .list() + .await().indefinitely() + .size + ) + // regex val entityWithUpperCase = TestReactiveEntity("title11", "upperCaseCategory", "desc") testReactiveRepository.persist(entityWithUpperCase).await().indefinitely() diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/test/TestResource.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/test/TestResource.java index 7af7c72c5108e..ffd7fd3040025 100644 --- a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/test/TestResource.java +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/test/TestResource.java @@ -11,6 +11,7 @@ import org.bson.Document; import org.junit.jupiter.api.Assertions; +import com.mongodb.ReadPreference; import com.mongodb.client.model.Collation; import com.mongodb.client.model.CollationStrength; @@ -77,6 +78,13 @@ public Response testImperativeEntity() { Assertions.assertEquals(0, TestImperativeEntity.list("category = :category", Parameters.with("category", null)).size()); + // find with options + Assertions.assertEquals(5, TestImperativeEntity.find("category", "category0") + .withBatchSize(2) + .withReadPreference(ReadPreference.nearest()) + .list() + .size()); + // regex TestImperativeEntity entityWithUpperCase = new TestImperativeEntity("title11", "upperCaseCategory", "desc"); entityWithUpperCase.persist(); @@ -237,6 +245,13 @@ public Response testImperativeRepository() { Assertions.assertEquals(0, testImperativeRepository.list("category = :category", Parameters.with("category", null)).size()); + // find with options + Assertions.assertEquals(5, testImperativeRepository.find("category", "category0") + .withBatchSize(2) + .withReadPreference(ReadPreference.nearest()) + .list() + .size()); + // regex TestImperativeEntity entityWithUpperCase = new TestImperativeEntity("title11", "upperCaseCategory", "desc"); testImperativeRepository.persist(entityWithUpperCase); @@ -474,6 +489,14 @@ public Response testReactiveEntity() { Assertions.assertEquals(0, TestReactiveEntity.list("category = :category", Parameters.with("category", null)).await().indefinitely().size()); + // find with options + Assertions.assertEquals(5, TestReactiveEntity.find("category", "category0") + .withBatchSize(2) + .withReadPreference(ReadPreference.nearest()) + .list() + .await().indefinitely() + .size()); + // regex TestReactiveEntity entityWithUpperCase = new TestReactiveEntity("title11", "upperCaseCategory", "desc"); entityWithUpperCase.persist().await().indefinitely(); @@ -650,6 +673,14 @@ public Response testReactiveRepository() { Assertions.assertEquals(0, testReactiveRepository.list("category = :category", Parameters.with("category", null)).await().indefinitely().size()); + // find with options + Assertions.assertEquals(5, testReactiveRepository.find("category", "category0") + .withBatchSize(2) + .withReadPreference(ReadPreference.nearest()) + .list() + .await().indefinitely() + .size()); + // regex TestReactiveEntity entityWithUpperCase = new TestReactiveEntity("title11", "upperCaseCategory", "desc"); testReactiveRepository.persist(entityWithUpperCase).await().indefinitely(); diff --git a/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/FrontendResource.java b/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/FrontendResource.java index 2475b7cded8ec..835ad9c17691d 100644 --- a/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/FrontendResource.java +++ b/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/FrontendResource.java @@ -19,6 +19,10 @@ public class FrontendResource { @RestClient ProtectedResourceServiceReactiveFilter protectedResourceServiceReactiveFilter; + @Inject + @RestClient + ProtectedResourceServiceNamedFilter protectedResourceServiceNamedFilter; + @GET @Path("userNameCustomFilter") @Produces("text/plain") @@ -32,4 +36,11 @@ public Uni userName() { public Uni userNameReactive() { return protectedResourceServiceReactiveFilter.getUserName(); } + + @GET + @Path("userNameNamedFilter") + @Produces("text/plain") + public Uni userNameNamedFilter() { + return protectedResourceServiceNamedFilter.getUserName(); + } } diff --git a/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/ProtectedResourceServiceNamedFilter.java b/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/ProtectedResourceServiceNamedFilter.java new file mode 100644 index 0000000000000..04d703f5d5f39 --- /dev/null +++ b/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/ProtectedResourceServiceNamedFilter.java @@ -0,0 +1,21 @@ +package io.quarkus.it.keycloak; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.oidc.client.filter.OidcClientFilter; +import io.smallrye.mutiny.Uni; + +@RegisterRestClient +@OidcClientFilter("named-client") +@Path("/") +public interface ProtectedResourceServiceNamedFilter { + + @GET + @Produces("text/plain") + @Path("userNameReactive") + Uni getUserName(); +} diff --git a/integration-tests/oidc-client-reactive/src/main/resources/application.properties b/integration-tests/oidc-client-reactive/src/main/resources/application.properties index 9a214ffeb8d97..26e2d60ffb0e4 100644 --- a/integration-tests/oidc-client-reactive/src/main/resources/application.properties +++ b/integration-tests/oidc-client-reactive/src/main/resources/application.properties @@ -10,8 +10,16 @@ quarkus.oidc-client.grant.type=password quarkus.oidc-client.grant-options.password.username=alice quarkus.oidc-client.grant-options.password.password=alice +quarkus.oidc-client.named-client.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.named-client.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.named-client.credentials.secret=${quarkus.oidc.credentials.secret} +quarkus.oidc-client.named-client.grant.type=password +quarkus.oidc-client.named-client.grant-options.password.username=jdoe +quarkus.oidc-client.named-client.grant-options.password.password=jdoe + io.quarkus.it.keycloak.ProtectedResourceServiceCustomFilter/mp-rest/url=http://localhost:8081/protected io.quarkus.it.keycloak.ProtectedResourceServiceReactiveFilter/mp-rest/url=http://localhost:8081/protected +io.quarkus.it.keycloak.ProtectedResourceServiceNamedFilter/mp-rest/url=http://localhost:8081/protected quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".min-level=TRACE quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".level=TRACE diff --git a/integration-tests/oidc-client-reactive/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java b/integration-tests/oidc-client-reactive/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java index f216770d3c413..ab999b6a55d01 100644 --- a/integration-tests/oidc-client-reactive/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java +++ b/integration-tests/oidc-client-reactive/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java @@ -35,6 +35,15 @@ public void testGetUserNameCustomFilter() { .body(equalTo("alice")); } + @Test + public void testGetUserNameNamedFilter() { + RestAssured.given().header("Accept", "text/plain") + .when().get("/frontend/userNameNamedFilter") + .then() + .statusCode(200) + .body(equalTo("jdoe")); + } + @Test public void testGetUserNameReactive() { RestAssured.given().header("Accept", "text/plain") diff --git a/integration-tests/oidc-client/src/main/java/io/quarkus/it/keycloak/FrontendResource.java b/integration-tests/oidc-client/src/main/java/io/quarkus/it/keycloak/FrontendResource.java index dd163cea232af..de5fe9f39ee15 100644 --- a/integration-tests/oidc-client/src/main/java/io/quarkus/it/keycloak/FrontendResource.java +++ b/integration-tests/oidc-client/src/main/java/io/quarkus/it/keycloak/FrontendResource.java @@ -25,6 +25,10 @@ public class FrontendResource { @RestClient ProtectedResourceServiceNoOidcClient protectedResourceServiceNoOidcClient; + @Inject + @RestClient + ProtectedResourceServiceNonDefaultOidcClient protectedResourceServiceNonDefaultOidcClient; + @Inject ManagedExecutor managedExecutor; @@ -37,6 +41,12 @@ public String userNameOidcClient() { return protectedResourceServiceOidcClient.getUserName(); } + @GET + @Path("userNonDefaultOidcClient") + public String userNameNonDefaultOidcClient() { + return protectedResourceServiceNonDefaultOidcClient.getUserName(); + } + @GET @Path("userOidcClientManagedExecutor") public String userNameOidcClientManagedExecutor() throws Exception { diff --git a/integration-tests/oidc-client/src/main/java/io/quarkus/it/keycloak/ProtectedResourceServiceNonDefaultOidcClient.java b/integration-tests/oidc-client/src/main/java/io/quarkus/it/keycloak/ProtectedResourceServiceNonDefaultOidcClient.java new file mode 100644 index 0000000000000..676632398d3a1 --- /dev/null +++ b/integration-tests/oidc-client/src/main/java/io/quarkus/it/keycloak/ProtectedResourceServiceNonDefaultOidcClient.java @@ -0,0 +1,18 @@ +package io.quarkus.it.keycloak; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.oidc.client.filter.OidcClientFilter; + +@RegisterRestClient +@OidcClientFilter("non-default-client") +@Path("/") +public interface ProtectedResourceServiceNonDefaultOidcClient { + + @GET + String getUserName(); + +} diff --git a/integration-tests/oidc-client/src/main/resources/application.properties b/integration-tests/oidc-client/src/main/resources/application.properties index 6351861a8f3ab..d020d5631dc32 100644 --- a/integration-tests/oidc-client/src/main/resources/application.properties +++ b/integration-tests/oidc-client/src/main/resources/application.properties @@ -16,10 +16,18 @@ quarkus.oidc-client.named.grant.type=password quarkus.oidc-client.named.grant-options.password.username=alice quarkus.oidc-client.named.grant-options.password.password=alice +quarkus.oidc-client.non-default-client.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.non-default-client.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.non-default-client.credentials.secret=${quarkus.oidc.credentials.secret} +quarkus.oidc-client.non-default-client.grant.type=password +quarkus.oidc-client.non-default-client.grant-options.password.username=bob +quarkus.oidc-client.non-default-client.grant-options.password.password=bob + io.quarkus.it.keycloak.ProtectedResourceServiceRegisterProvider/mp-rest/url=http://localhost:8081/protected io.quarkus.it.keycloak.ProtectedResourceServiceOidcClient/mp-rest/url=http://localhost:8081/protected io.quarkus.it.keycloak.ProtectedResourceServiceNamedOidcClient/mp-rest/url=http://localhost:8081/protected io.quarkus.it.keycloak.ProtectedResourceServiceNoOidcClient/mp-rest/url=http://localhost:8081/protected +io.quarkus.it.keycloak.ProtectedResourceServiceNonDefaultOidcClient/mp-rest/url=http://localhost:8081/protected quarkus.tls.trust-all=true quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".min-level=TRACE diff --git a/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java b/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java index a760650e66bbd..a5f70b2f08f75 100644 --- a/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java +++ b/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java @@ -38,6 +38,7 @@ public Map start() { realm.getClients().add(createClient("quarkus-app")); realm.getUsers().add(createUser("alice", "user")); + realm.getUsers().add(createUser("bob", "user")); try { RestAssured diff --git a/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java b/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java index c64965dc44ce0..94c1747f1fc1f 100644 --- a/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java +++ b/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java @@ -36,6 +36,14 @@ public void testGetUserNameOidcClient() { .body(equalTo("alice")); } + @Test + public void testGetUserNameNonDefaultOidcClient() { + RestAssured.when().get("/frontend/userNonDefaultOidcClient") + .then() + .statusCode(200) + .body(equalTo("bob")); + } + @Test public void testGetUserNameOidcClientManagedExecutor() { RestAssured.when().get("/frontend/userOidcClientManagedExecutor") diff --git a/integration-tests/oidc-code-flow/pom.xml b/integration-tests/oidc-code-flow/pom.xml index 4600259665044..dbfd44d2741aa 100644 --- a/integration-tests/oidc-code-flow/pom.xml +++ b/integration-tests/oidc-code-flow/pom.xml @@ -13,10 +13,6 @@ Quarkus - Integration Tests - OpenID Connect Adapter Code Flow Module that contains OpenID Connect Code Flow related tests - - http://localhost:8180/auth - - io.quarkus @@ -50,6 +46,11 @@ quarkus-junit5 test + + io.quarkus + quarkus-test-keycloak-server + test + org.keycloak keycloak-adapter-core @@ -201,18 +202,12 @@ maven-surefire-plugin false - - ${keycloak.url} - maven-failsafe-plugin false - - ${keycloak.url} - @@ -229,89 +224,6 @@ - - - docker-keycloak - - - start-containers - - - - http://localhost:8180/auth - - - - - io.fabric8 - docker-maven-plugin - - - - ${keycloak.docker.legacy.image} - quarkus-test-keycloak - - - 8180:8080 - - - admin - admin - - - Keycloak: - default - cyan - - - - - http://localhost:8180 - - - - - - - true - - - - docker-start - compile - - stop - start - - - - docker-stop - post-integration-test - - stop - - - - - - org.codehaus.mojo - exec-maven-plugin - - - docker-prune - generate-resources - - exec - - - ${docker-prune.location} - - - - - - - jakarta-rewrite diff --git a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java index fbe80108b4407..2bac753ec2c0b 100644 --- a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java +++ b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java @@ -24,6 +24,7 @@ import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.security.Authenticated; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.SecurityIdentityAssociation; import io.vertx.ext.web.RoutingContext; @Path("/web-app") @@ -33,6 +34,9 @@ public class ProtectedResource { @Inject SecurityIdentity identity; + @Inject + SecurityIdentityAssociation securityIdentityAssociation; + @Inject Principal principal; @@ -68,13 +72,15 @@ public class ProtectedResource { @Path("test-security") public String testSecurity() { return securityContext.getUserPrincipal().getName() + ":" + identity.getPrincipal().getName() + ":" - + principal.getName(); + + principal.getName() + ":" + + securityIdentityAssociation.getDeferredIdentity().await().indefinitely().getPrincipal().getName(); } @GET @Path("test-security-oidc") public String testSecurityJwt() { return idToken.getName() + ":" + identity.getPrincipal().getName() + ":" + principal.getName() + + ":" + securityIdentityAssociation.getDeferredIdentity().await().indefinitely().getPrincipal().getName() + ":" + idToken.getGroups().iterator().next() + ":" + idToken.getClaim("email") + ":" + userInfo.getString("sub") diff --git a/integration-tests/oidc-code-flow/src/main/resources/application.properties b/integration-tests/oidc-code-flow/src/main/resources/application.properties index dece21ad567dd..4bc7b95148da4 100644 --- a/integration-tests/oidc-code-flow/src/main/resources/application.properties +++ b/integration-tests/oidc-code-flow/src/main/resources/application.properties @@ -1,5 +1,5 @@ +quarkus.keycloak.devservices.create-realm=false # Default tenant configurationf -quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus quarkus.oidc.client-id=quarkus-app quarkus.oidc.credentials.secret=secret quarkus.oidc.authentication.scopes=profile,email,phone @@ -18,7 +18,7 @@ quarkus.oidc-client.credentials.secret=${quarkus.oidc.credentials.secret} quarkus.oidc-client.grant.type=code # Tenant listener configuration for testing that the login event has been captured -quarkus.oidc.tenant-listener.auth-server-url=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-listener.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-listener.client-id=quarkus-app quarkus.oidc.tenant-listener.credentials.secret=secret # Redirect parameters are dropped by redirecting the authenticated user but this final redirect loses the login event message @@ -27,45 +27,45 @@ quarkus.oidc.tenant-listener.authentication.remove-redirect-parameters=false quarkus.oidc.tenant-listener.application-type=web-app # Tenant which does not need to restore a request path after redirect, client_secret_post method -quarkus.oidc.tenant-1.auth-server-url=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-1.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-1.client-id=quarkus-app quarkus.oidc.tenant-1.credentials.client-secret.value=secret quarkus.oidc.tenant-1.credentials.client-secret.method=post -quarkus.oidc.tenant-1.token.issuer=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-1.token.issuer=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-1.authentication.redirect-path=/web-app/callback-after-redirect quarkus.oidc.tenant-1.application-type=web-app # Tenant with client which needs to use client_secret_jwt method -quarkus.oidc.tenant-jwt.auth-server-url=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-jwt.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-jwt.client-id=quarkus-app-jwt quarkus.oidc.tenant-jwt.credentials.jwt.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow -quarkus.oidc.tenant-jwt.token.issuer=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-jwt.token.issuer=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-jwt.authentication.redirect-path=/web-app/callback-jwt-after-redirect quarkus.oidc.tenant-jwt.application-type=web-app # Tenant with client which needs to use client_secret_jwt but uses client_secret_post -quarkus.oidc.tenant-jwt-not-used.auth-server-url=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-jwt-not-used.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-jwt-not-used.client-id=quarkus-app-jwt quarkus.oidc.tenant-jwt-not-used.credentials.client-secret.value=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow quarkus.oidc.tenant-jwt-not-used.credentials.client-secret.method=post -quarkus.oidc.tenant-jwt-not-used.token.issuer=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-jwt-not-used.token.issuer=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-jwt-not-used.authentication.redirect-path=/web-app/callback-jwt-not-used-after-redirect quarkus.oidc.tenant-jwt-not-used.application-type=web-app # Tenant which does not need to restore a request path after redirect with a different redirect path root -quarkus.oidc.tenant-2.auth-server-url=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-2.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-2.client-id=quarkus-app quarkus.oidc.tenant-2.credentials.client-secret.value=secret -quarkus.oidc.tenant-2.token.issuer=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-2.token.issuer=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-2.authentication.redirect-path=/web-app2/name quarkus.oidc.tenant-2.authentication.cookie-path=/web-app2 quarkus.oidc.tenant-2.application-type=web-app # Tenant which is only used to test that the failed token request will not cause a redirect loop. -quarkus.oidc.tenant-3.auth-server-url=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-3.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-3.client-id=quarkus-app quarkus.oidc.tenant-3.credentials.secret=secret -quarkus.oidc.tenant-3.token.issuer=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-3.token.issuer=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-3.authentication.redirect-path=/web-app3 quarkus.oidc.tenant-3.application-type=web-app @@ -85,7 +85,7 @@ quarkus.oidc.tenant-refresh.authentication.cookie-path=/tenant-refresh quarkus.oidc.tenant-refresh.authentication.session-age-extension=2M quarkus.oidc.tenant-refresh.token.refresh-expired=true -quarkus.oidc.tenant-autorefresh.auth-server-url=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-autorefresh.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-autorefresh.client-id=quarkus-app quarkus.oidc.tenant-autorefresh.credentials.secret=secret quarkus.oidc.tenant-autorefresh.application-type=web-app @@ -95,7 +95,7 @@ quarkus.oidc.tenant-autorefresh.token.refresh-token-time-skew=30S quarkus.oidc.tenant-autorefresh.authentication.remove-redirect-parameters=false # Tenant which is used to test that the redirect_uri https scheme is enforced. -quarkus.oidc.tenant-https.auth-server-url=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-https.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-https.client-id=quarkus-app quarkus.oidc.tenant-https.credentials.secret=secret quarkus.oidc.tenant-https.authentication.scopes=profile,email,phone @@ -108,38 +108,38 @@ quarkus.oidc.tenant-https.authentication.error-path=/tenant-https/error quarkus.oidc.tenant-https.authentication.pkce-required=true quarkus.oidc.tenant-https.authentication.pkce-secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU -quarkus.oidc.tenant-javascript.auth-server-url=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-javascript.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-javascript.client-id=quarkus-app quarkus.oidc.tenant-javascript.credentials.secret=secret quarkus.oidc.tenant-javascript.authentication.java-script-auto-redirect=false quarkus.oidc.tenant-javascript.application-type=web-app -quarkus.oidc.tenant-cookie-path-header.auth-server-url=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-cookie-path-header.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-cookie-path-header.client-id=quarkus-app quarkus.oidc.tenant-cookie-path-header.credentials.secret=secret quarkus.oidc.tenant-cookie-path-header.authentication.cookie-path-header=X-Forwarded-Prefix quarkus.oidc.tenant-cookie-path-header.application-type=web-app -quarkus.oidc.tenant-idtoken-only.auth-server-url=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-idtoken-only.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-idtoken-only.client-id=quarkus-app quarkus.oidc.tenant-idtoken-only.credentials.secret=secret quarkus.oidc.tenant-idtoken-only.token-state-manager.strategy=id-token quarkus.oidc.tenant-idtoken-only.application-type=web-app -quarkus.oidc.tenant-id-refresh-token.auth-server-url=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-id-refresh-token.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-id-refresh-token.client-id=quarkus-app quarkus.oidc.tenant-id-refresh-token.credentials.secret=secret quarkus.oidc.tenant-id-refresh-token.token-state-manager.strategy=id-refresh-tokens quarkus.oidc.tenant-id-refresh-token.application-type=web-app -quarkus.oidc.tenant-split-id-refresh-token.auth-server-url=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-split-id-refresh-token.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-split-id-refresh-token.client-id=quarkus-app quarkus.oidc.tenant-split-id-refresh-token.credentials.secret=secret quarkus.oidc.tenant-split-id-refresh-token.token-state-manager.strategy=id-refresh-tokens quarkus.oidc.tenant-split-id-refresh-token.token-state-manager.split-tokens=true quarkus.oidc.tenant-split-id-refresh-token.application-type=web-app -quarkus.oidc.tenant-split-tokens.auth-server-url=${keycloak.url}/realms/quarkus +quarkus.oidc.tenant-split-tokens.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.tenant-split-tokens.client-id=quarkus-app quarkus.oidc.tenant-split-tokens.credentials.secret=secret quarkus.oidc.tenant-split-tokens.token-state-manager.split-tokens=true @@ -166,6 +166,7 @@ quarkus.http.auth.permission.post-logout.paths=/tenant-logout/post-logout quarkus.http.auth.permission.post-logout.policy=permit quarkus.http.cors=true +quarkus.http.cors.origins=* quarkus.http.auth.proactive=false quarkus.http.proxy.enable-forwarded-prefix=true quarkus.http.proxy.allow-forwarded=true diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java index 7b5450669d750..7ae4555f73101 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java @@ -33,6 +33,7 @@ import io.quarkus.oidc.runtime.OidcUtils; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.keycloak.client.KeycloakTestClient; import io.restassured.RestAssured; import io.smallrye.jwt.util.KeyUtils; import io.vertx.core.json.JsonObject; @@ -44,6 +45,8 @@ @QuarkusTestResource(KeycloakRealmResourceManager.class) public class CodeFlowTest { + KeycloakTestClient client = new KeycloakTestClient(); + @Test public void testCodeFlowNoConsent() throws IOException { try (final WebClient webClient = createWebClient()) { @@ -78,7 +81,7 @@ public void testCodeFlowNoConsent() throws IOException { page = webClient.getPage("http://localhost:8081/web-app/configMetadataIssuer"); assertEquals( - KeycloakRealmResourceManager.KEYCLOAK_SERVER_URL + "/realms/" + KeycloakRealmResourceManager.KEYCLOAK_REALM, + client.getAuthServerUrl(), page.asText()); page = webClient.getPage("http://localhost:8081/web-app/configMetadataScopes"); @@ -346,7 +349,7 @@ private void verifyCodeVerifier(Cookie stateCookie, String keycloakUrl) throws E } private void verifyLocationHeader(WebClient webClient, String loc, String tenant, String path, boolean httpsScheme) { - assertTrue(loc.startsWith("http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/auth")); + assertTrue(loc.startsWith(client.getAuthServerUrl() + "/protocol/openid-connect/auth")); String scheme = httpsScheme ? "https" : "http"; assertTrue(loc.contains("redirect_uri=" + scheme + "%3A%2F%2Flocalhost%3A8081%2F" + path)); assertTrue(loc.contains("state=" + getStateCookieStateParam(webClient, tenant))); diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java index 6d5321ce0a42f..b9c0be214ee86 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java @@ -1,77 +1,47 @@ package io.quarkus.it.keycloak; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; -import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RolesRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.util.JsonSerialization; +import io.quarkus.test.common.DevServicesContext; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; -import io.restassured.RestAssured; +import io.quarkus.test.keycloak.client.KeycloakTestClient; -public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycleManager { +public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware { + + private static final String KEYCLOAK_REALM = "quarkus"; + final KeycloakTestClient client = new KeycloakTestClient(); - public static final String KEYCLOAK_SERVER_URL = System.getProperty("keycloak.url", "http://localhost:8180/auth"); - public static final String KEYCLOAK_REALM = "quarkus"; private List realms = new ArrayList<>(); @Override public Map start() { - try { - - RealmRepresentation realm = createRealm(KEYCLOAK_REALM); - createRealmInKeycloak(realm); - realms.add(realm); - - RealmRepresentation logoutRealm = createRealm("logout-realm"); - // revoke refresh tokens so that they can only be used once - logoutRealm.setRevokeRefreshToken(true); - logoutRealm.setRefreshTokenMaxReuse(0); - logoutRealm.setSsoSessionMaxLifespan(15); - logoutRealm.setAccessTokenLifespan(5); - createRealmInKeycloak(logoutRealm); - realms.add(logoutRealm); - - } catch (IOException e) { - throw new RuntimeException(e); - } + RealmRepresentation realm = createRealm(KEYCLOAK_REALM); + client.createRealm(realm); + realms.add(realm); + + RealmRepresentation logoutRealm = createRealm("logout-realm"); + // revoke refresh tokens so that they can only be used once + logoutRealm.setRevokeRefreshToken(true); + logoutRealm.setRefreshTokenMaxReuse(0); + logoutRealm.setSsoSessionMaxLifespan(15); + logoutRealm.setAccessTokenLifespan(5); + client.createRealm(logoutRealm); + realms.add(logoutRealm); return Collections.emptyMap(); } - private static String getAdminAccessToken() { - return RestAssured - .given() - .param("grant_type", "password") - .param("username", "admin") - .param("password", "admin") - .param("client_id", "admin-cli") - .when() - .post(KEYCLOAK_SERVER_URL + "/realms/master/protocol/openid-connect/token") - .as(AccessTokenResponse.class).getToken(); - } - - private static void createRealmInKeycloak(RealmRepresentation realm) throws IOException { - RestAssured - .given() - .auth().oauth2(getAdminAccessToken()) - .contentType("application/json") - .body(JsonSerialization.writeValueAsBytes(realm)) - .when() - .post(KEYCLOAK_SERVER_URL + "/admin/realms").then() - .statusCode(201); - } - private static RealmRepresentation createRealm(String name) { RealmRepresentation realm = new RealmRepresentation(); @@ -147,11 +117,13 @@ private static UserRepresentation createUser(String username, String... realmRol @Override public void stop() { for (RealmRepresentation realm : realms) { - RestAssured - .given() - .auth().oauth2(getAdminAccessToken()) - .when() - .delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + realm.getRealm()).thenReturn().prettyPrint(); + client.deleteRealm(realm); } } + + @Override + public void setIntegrationTestContext(DevServicesContext context) { + client.setIntegrationTestContext(context); + } + } diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java index a3dda08d86d43..40732c2d319cc 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java @@ -2,6 +2,11 @@ import static org.hamcrest.Matchers.is; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + import org.junit.jupiter.api.Test; import io.quarkus.test.common.http.TestHTTPEndpoint; @@ -21,10 +26,18 @@ public class TestSecurityLazyAuthTest { @TestSecurity(user = "user1", roles = "viewer") public void testWithDummyUser() { RestAssured.when().get("test-security").then() - .body(is("user1:user1:user1")); + .body(is("user1:user1:user1:user1")); } @Test + @TestSecurityMetaAnnotation + public void testJwtWithDummyUser() { + RestAssured.when().get("test-security-oidc").then() + .body(is("userOidc:userOidc:userOidc:userOidc:viewer:user@gmail.com:subject:aud")); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD }) @TestSecurity(user = "userOidc", roles = "viewer") @OidcSecurity(claims = { @Claim(key = "email", value = "user@gmail.com") @@ -33,9 +46,8 @@ public void testWithDummyUser() { }, config = { @ConfigMetadata(key = "audience", value = "aud") }) - public void testJwtWithDummyUser() { - RestAssured.when().get("test-security-oidc").then() - .body(is("userOidc:userOidc:userOidc:viewer:user@gmail.com:subject:aud")); + public @interface TestSecurityMetaAnnotation { + } } diff --git a/integration-tests/oidc-tenancy/pom.xml b/integration-tests/oidc-tenancy/pom.xml index 9a098bfaab69c..787092a4310cf 100644 --- a/integration-tests/oidc-tenancy/pom.xml +++ b/integration-tests/oidc-tenancy/pom.xml @@ -13,10 +13,6 @@ Quarkus - Integration Tests - Multi-Tenant OpenID Connect Adapter Module that contains OpenID Connect Multi-Tenancy related tests - - http://localhost:8180/auth - - io.quarkus @@ -54,6 +50,11 @@ + + io.quarkus + quarkus-test-keycloak-server + test + io.quarkus quarkus-junit5 @@ -182,18 +183,12 @@ maven-surefire-plugin false - - ${keycloak.url} - maven-failsafe-plugin false - - ${keycloak.url} - @@ -210,89 +205,6 @@ - - - docker-keycloak - - - start-containers - - - - http://localhost:8180/auth - - - - - io.fabric8 - docker-maven-plugin - - - - ${keycloak.docker.legacy.image} - quarkus-test-keycloak - - - 8180:8080 - - - admin - admin - - - Keycloak: - default - cyan - - - - - http://localhost:8180 - - - - - - - true - - - - docker-start - compile - - stop - start - - - - docker-stop - post-integration-test - - stop - - - - - - org.codehaus.mojo - exec-maven-plugin - - - docker-prune - generate-resources - - exec - - - ${docker-prune.location} - - - - - - - jakarta-rewrite diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java index 41945e66c614b..d5b4b5ff79818 100644 --- a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java @@ -5,6 +5,8 @@ import javax.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.config.ConfigProvider; + import io.quarkus.oidc.OidcRequestContext; import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.OidcTenantConfig.ApplicationType; @@ -34,9 +36,9 @@ public OidcTenantConfig get() { String tenantId = path.split("/")[2]; if ("tenant-d".equals(tenantId)) { OidcTenantConfig config = new OidcTenantConfig(); - config.setTenantId("tenant-d"); + config.setTenantId("tenant-c"); config.setAuthServerUrl(getIssuerUrl() + "/realms/quarkus-d"); - config.setClientId("quarkus-d"); + config.setClientId("quarkus-app-d"); config.getCredentials().setSecret("secret"); config.getToken().setIssuer(getIssuerUrl() + "/realms/quarkus-d"); config.getAuthentication().setUserInfoRequired(true); @@ -158,6 +160,6 @@ public OidcTenantConfig get() { } private String getIssuerUrl() { - return System.getProperty("keycloak.url", "http://localhost:8180/auth"); + return ConfigProvider.getConfig().getValue("keycloak.url", String.class); } } diff --git a/integration-tests/oidc-tenancy/src/main/resources/application.properties b/integration-tests/oidc-tenancy/src/main/resources/application.properties index 20a4673716dab..0074691749591 100644 --- a/integration-tests/oidc-tenancy/src/main/resources/application.properties +++ b/integration-tests/oidc-tenancy/src/main/resources/application.properties @@ -1,9 +1,12 @@ +quarkus.keycloak.devservices.create-realm=false +quarkus.keycloak.devservices.realm-name=quarkus-a + quarkus.http.cors=true +quarkus.http.cors.origins=* quarkus.oidc.token-cache.max-size=3 # Default Tenant -quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus-a quarkus.oidc.client-id=quarkus-app-a quarkus.oidc.credentials.secret=secret quarkus.oidc.application-type=service @@ -116,4 +119,4 @@ quarkus.native.additional-build-args=-H:IncludeResources=.*\\.pem quarkus.log.category."io.quarkus.oidc.runtime.CodeAuthenticationMechanism".min-level=TRACE -quarkus.log.category."io.quarkus.oidc.runtime.CodeAuthenticationMechanism".level=TRACE \ No newline at end of file +quarkus.log.category."io.quarkus.oidc.runtime.CodeAuthenticationMechanism".level=TRACE diff --git a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java index 019cdc86f0362..1608749e307e6 100644 --- a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java +++ b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java @@ -10,11 +10,11 @@ import java.io.IOException; import java.net.URI; import java.time.Duration; +import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; -import org.keycloak.representations.AccessTokenResponse; import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; import com.gargoylesoftware.htmlunit.WebClient; @@ -26,6 +26,7 @@ import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.keycloak.client.KeycloakTestClient; import io.restassured.RestAssured; import io.vertx.core.json.JsonObject; @@ -36,8 +37,7 @@ @QuarkusTestResource(KeycloakRealmResourceManager.class) public class BearerTokenAuthorizationTest { - private static final String KEYCLOAK_SERVER_URL = System.getProperty("keycloak.url", "http://localhost:8180/auth"); - private static final String KEYCLOAK_REALM = "quarkus-"; + private KeycloakTestClient client = new KeycloakTestClient(); @Test public void testResolveTenantIdentifierWebApp() throws IOException { @@ -334,14 +334,14 @@ public void testCustomHeaderCustomScheme() { @Test public void testResolveTenantConfig() { - RestAssured.given().auth().oauth2(getAccessToken("alice", "d")) + RestAssured.given().auth().oauth2(getAccessToken("alice", "d", "d", List.of("openid"))) .when().get("/tenant/tenant-d/api/user") .then() .statusCode(200) .body(equalTo("tenant-d:alice.alice")); // should give a 401 given that access token from issuer b can not access tenant c - RestAssured.given().auth().oauth2(getAccessToken("alice", "b")) + RestAssured.given().auth().oauth2(getAccessToken("alice", "b", "b", List.of("openid"))) .when().get("/tenant/tenant-d/api/user") .then() .statusCode(401); @@ -349,7 +349,7 @@ public void testResolveTenantConfig() { @Test public void testResolveTenantConfigNoDiscovery() { - RestAssured.given().auth().oauth2(getAccessToken("alice", "b")) + RestAssured.given().auth().oauth2(getAccessToken("alice", "b", "b", List.of("openid"))) .when().get("/tenant/tenant-b-no-discovery/api/user/no-discovery") .then() .statusCode(200) @@ -596,16 +596,12 @@ private String getAccessToken(String userName, String clientId) { } private String getAccessToken(String userName, String realmId, String clientId) { - return RestAssured - .given() - .param("grant_type", "password") - .param("username", userName) - .param("password", userName) - .param("client_id", "quarkus-app-" + clientId) - .param("client_secret", "secret") - .when() - .post(KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM + realmId + "/protocol/openid-connect/token") - .as(AccessTokenResponse.class).getToken(); + return getAccessToken(userName, realmId, clientId, null); + } + + private String getAccessToken(String userName, String realmId, String clientId, List scopes) { + return client.getRealmAccessToken("quarkus-" + realmId, userName, userName, "quarkus-app-" + clientId, "secret", + scopes); } private String getAccessTokenFromSimpleOidc(String kid) { diff --git a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java index 5725ebc832e30..a4a1b47f4068a 100644 --- a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java +++ b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java @@ -1,28 +1,26 @@ package io.quarkus.it.keycloak; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; -import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RolesRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.util.JsonSerialization; +import io.quarkus.test.common.DevServicesContext; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; -import io.restassured.RestAssured; +import io.quarkus.test.keycloak.client.KeycloakTestClient; -public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycleManager { +public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware { - private static final String KEYCLOAK_SERVER_URL = System.getProperty("keycloak.url", "http://localhost:8180/auth"); private static final String KEYCLOAK_REALM = "quarkus-"; + final KeycloakTestClient client = new KeycloakTestClient(); @Override public Map start() { @@ -37,34 +35,11 @@ public Map start() { realm.getUsers().add(createUser("admin", "user", "admin")); realm.getUsers().add(createUser("jdoe", "user", "confidential")); - try { - RestAssured - .given() - .auth().oauth2(getAdminAccessToken()) - .contentType("application/json") - .body(JsonSerialization.writeValueAsBytes(realm)) - .when() - .post(KEYCLOAK_SERVER_URL + "/admin/realms").then() - .statusCode(201); - } catch (IOException e) { - throw new RuntimeException(e); - } + client.createRealm(realm); } return Collections.emptyMap(); } - private static String getAdminAccessToken() { - return RestAssured - .given() - .param("grant_type", "password") - .param("username", "admin") - .param("password", "admin") - .param("client_id", "admin-cli") - .when() - .post(KEYCLOAK_SERVER_URL + "/realms/master/protocol/openid-connect/token") - .as(AccessTokenResponse.class).getToken(); - } - private static RealmRepresentation createRealm(String name) { RealmRepresentation realm = new RealmRepresentation(); @@ -127,12 +102,16 @@ private static UserRepresentation createUser(String username, String... realmRol @Override public void stop() { for (String realmId : Arrays.asList("a", "b", "c", "d", "webapp", "webapp2", "hybrid")) { - RestAssured - .given() - .auth().oauth2(getAdminAccessToken()) - .when() - .delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM + realmId).then().statusCode(204); + try { + client.deleteRealm(realmId); + } catch (Throwable t) { + + } } + } + @Override + public void setIntegrationTestContext(DevServicesContext context) { + client.setIntegrationTestContext(context); } } diff --git a/integration-tests/oidc-token-propagation-reactive/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java b/integration-tests/oidc-token-propagation-reactive/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java index 2f4ea6ffb2440..f9b09dfbb50dd 100644 --- a/integration-tests/oidc-token-propagation-reactive/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java +++ b/integration-tests/oidc-token-propagation-reactive/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java @@ -2,6 +2,11 @@ import static org.hamcrest.Matchers.is; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + import org.junit.jupiter.api.Test; import io.quarkus.test.common.http.TestHTTPEndpoint; @@ -17,40 +22,61 @@ public class TestSecurityLazyAuthTest { @Test - @TestSecurity(user = "user1", roles = "viewer") + @TestAsUser1Viewer public void testWithDummyUser() { RestAssured.when().get("test-security").then() .body(is("user1")); } @Test - @TestSecurity(user = "user1", roles = "tester") + @TestAsUser1Tester public void testWithDummyUserForbidden() { RestAssured.when().get("test-security").then().statusCode(403); } @Test - @TestSecurity(user = "user1", roles = "viewer") + @TestAsUser1Viewer public void testPostWithDummyUser() { RestAssured.given().contentType(ContentType.JSON).when().body("{\"name\":\"user1\"}").post("test-security").then() .body(is("user1:user1")); } @Test - @TestSecurity(user = "user1", roles = "tester") + @TestAsUser1Tester public void testPostWithDummyUserForbidden() { RestAssured.given().contentType(ContentType.JSON).when().body("{\"name\":\"user1\"}").post("test-security").then() .statusCode(403); } @Test + @TestAsUserJwtViewer + public void testJwtGetWithDummyUser() { + RestAssured.when().get("test-security-jwt").then() + .body(is("userJwt:viewer:user@gmail.com")); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD }) + @TestSecurity(user = "user1", roles = "viewer") + public @interface TestAsUser1Viewer { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD }) + @TestSecurity(user = "user1", roles = "tester") + public @interface TestAsUser1Tester { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD }) @TestSecurity(user = "userJwt", roles = "viewer") @OidcSecurity(claims = { @Claim(key = "email", value = "user@gmail.com") }) - public void testJwtGetWithDummyUser() { - RestAssured.when().get("test-security-jwt").then() - .body(is("userJwt:viewer:user@gmail.com")); + public @interface TestAsUserJwtViewer { + } } diff --git a/integration-tests/oidc-token-propagation/pom.xml b/integration-tests/oidc-token-propagation/pom.xml index 7e73b6dd3fba9..ff6de6eb41699 100644 --- a/integration-tests/oidc-token-propagation/pom.xml +++ b/integration-tests/oidc-token-propagation/pom.xml @@ -19,6 +19,10 @@ org.keycloak keycloak-adapter-core + + org.jboss.logging + commons-logging-jboss-logging + org.keycloak keycloak-core diff --git a/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/AccessTokenPropagationService.java b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/AccessTokenPropagationService.java index a85798bfccdbd..006fb46b04673 100644 --- a/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/AccessTokenPropagationService.java +++ b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/AccessTokenPropagationService.java @@ -7,7 +7,7 @@ import io.quarkus.oidc.token.propagation.AccessToken; -@RegisterRestClient +@RegisterRestClient(configKey = "access-token-propagation") @AccessToken @Path("/") public interface AccessTokenPropagationService { diff --git a/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/FrontendResource.java b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/FrontendResource.java index 61fb1f11fddb2..9ca4be2daaeac 100644 --- a/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/FrontendResource.java +++ b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/FrontendResource.java @@ -8,6 +8,8 @@ import org.eclipse.microprofile.rest.client.inject.RestClient; +import io.quarkus.security.Authenticated; + @Path("/frontend") public class FrontendResource { @Inject @@ -29,6 +31,13 @@ public String userNameJwtTokenPropagation() { return jwtTokenPropagationService.getUserName(); } + @GET + @Path("client-jwt-token-propagation") + @Authenticated + public String clientUserNameJwtTokenPropagation() { + return jwtTokenPropagationService.getClientName(); + } + @GET @Path("access-token-propagation") @RolesAllowed("user") diff --git a/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtTokenPropagationService.java b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtTokenPropagationService.java index c5c8b87eb9545..c08cca9159f64 100644 --- a/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtTokenPropagationService.java +++ b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/JwtTokenPropagationService.java @@ -7,11 +7,15 @@ import io.quarkus.oidc.token.propagation.JsonWebToken; -@RegisterRestClient +@RegisterRestClient(configKey = "jwt-token-propagation") @JsonWebToken @Path("/") public interface JwtTokenPropagationService { @GET String getUserName(); + + @GET + @Path("client") + String getClientName(); } diff --git a/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java index b99056445fdee..c8fcc1309a3a5 100644 --- a/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java +++ b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java @@ -21,4 +21,10 @@ public class ProtectedResource { public String principalName() { return principal.getName(); } + + @GET + @Path("client") + public String clientName() { + return principal.getName(); + } } diff --git a/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/ServiceAccountService.java b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/ServiceAccountService.java index 014fcdee418d6..4f1b6e176534e 100644 --- a/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/ServiceAccountService.java +++ b/integration-tests/oidc-token-propagation/src/main/java/io/quarkus/it/keycloak/ServiceAccountService.java @@ -7,7 +7,7 @@ import io.quarkus.oidc.client.filter.OidcClientFilter; -@RegisterRestClient +@RegisterRestClient(configKey = "service-account-service") @OidcClientFilter @Path("/") public interface ServiceAccountService { diff --git a/integration-tests/oidc-token-propagation/src/main/resources/application.properties b/integration-tests/oidc-token-propagation/src/main/resources/application.properties index 5fe8f28962a13..01fa61296556b 100644 --- a/integration-tests/oidc-token-propagation/src/main/resources/application.properties +++ b/integration-tests/oidc-token-propagation/src/main/resources/application.properties @@ -21,6 +21,9 @@ quarkus.oidc-client.exchange-token.grant-options.exchange.audience=quarkus-app-e quarkus.oidc-token-propagation.exchange-token=true quarkus.oidc-token-propagation.client-name=exchange-token -io.quarkus.it.keycloak.JwtTokenPropagationService/mp-rest/uri=http://localhost:8081/protected -io.quarkus.it.keycloak.AccessTokenPropagationService/mp-rest/uri=http://localhost:8081/protected -io.quarkus.it.keycloak.ServiceAccountService/mp-rest/uri=http://localhost:8081/protected +quarkus.rest-client.jwt-token-propagation.uri=http://localhost:8081/protected +quarkus.rest-client.jwt-token-propagation.verify-host=false +quarkus.rest-client.access-token-propagation.uri=http://localhost:8081/protected +quarkus.rest-client.access-token-propagation.verify-host=false +quarkus.rest-client.service-account-service.uri=http://localhost:8081/protected +quarkus.rest-client.service-account-service.verify-host=false diff --git a/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java b/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java index fca153d3d1750..bcd717025d989 100644 --- a/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java +++ b/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java @@ -25,6 +25,15 @@ public void testGetUserNameWithJwtTokenPropagation() { .body(equalTo("alice")); } + @Test + public void testGetClientNameWithJwtTokenPropagation() { + RestAssured.given().auth().oauth2(getClientAccessToken()) + .when().get("/frontend/client-jwt-token-propagation") + .then() + .statusCode(200) + .body(equalTo("service-account-quarkus-app")); + } + @Test public void testGetUserNameWithAccessTokenPropagation() { // At the moment it is not possible to configure Keycloak Token Exchange permissions @@ -58,4 +67,8 @@ public void testGetUserNameFromServiceAccount() { public String getAccessToken(String userName) { return client.getAccessToken(userName, userName, "quarkus-app", "secret"); } + + public String getClientAccessToken() { + return client.getClientAccessToken(); + } } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java index 1353ab349ff36..ecd6d4478671d 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java @@ -1,5 +1,6 @@ package io.quarkus.it.keycloak; +import javax.annotation.security.PermitAll; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -48,4 +49,11 @@ public String accessGitHubCachedInIdToken() { public String accessDynamicGitHub() { return access(); } + + @GET + @PermitAll + @Path("/clear-token-cache") + public void clearTokenCache() { + tokenCache.clearCache(); + } } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java index 5271cc2bdccae..84eae694557c0 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java @@ -21,6 +21,7 @@ public Uni augment(SecurityIdentity identity, AuthenticationRe if (routingContext != null && (routingContext.normalizedPath().endsWith("code-flow-user-info-only") || routingContext.normalizedPath().endsWith("code-flow-user-info-github") + || routingContext.normalizedPath().endsWith("bearer-user-info-github-service") || routingContext.normalizedPath().endsWith("code-flow-user-info-dynamic-github") || routingContext.normalizedPath().endsWith("code-flow-user-info-github-cached-in-idtoken"))) { QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java index 1473b7fb802a9..61ee4ff44d326 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java @@ -32,6 +32,9 @@ public String resolve(RoutingContext context) { if (path.endsWith("code-flow-user-info-github")) { return "code-flow-user-info-github"; } + if (path.endsWith("bearer-user-info-github-service")) { + return "bearer-user-info-github-service"; + } if (path.endsWith("code-flow-user-info-github-cached-in-idtoken")) { return "code-flow-user-info-github-cached-in-idtoken"; } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OpaqueGithubResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OpaqueGithubResource.java new file mode 100644 index 0000000000000..db55846de3e89 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OpaqueGithubResource.java @@ -0,0 +1,31 @@ +package io.quarkus.it.keycloak; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import io.quarkus.oidc.AccessTokenCredential; +import io.quarkus.oidc.UserInfo; +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; + +@Authenticated +@Path("/bearer-user-info-github-service") +public class OpaqueGithubResource { + + @Inject + UserInfo userInfo; + + @Inject + SecurityIdentity identity; + + @Inject + AccessTokenCredential accessTokenCredential; + + @GET + public String access() { + return String.format("%s:%s:%s", identity.getPrincipal().getName(), userInfo.getString("preferred_username"), + accessTokenCredential.getToken()); + } + +} diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index ee4cc8c82d988..55c6643e7e2f7 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -72,6 +72,15 @@ quarkus.oidc.code-flow-user-info-github.code-grant.headers.X-Custom=XCustomHeade quarkus.oidc.code-flow-user-info-github.client-id=quarkus-web-app quarkus.oidc.code-flow-user-info-github.credentials.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow +quarkus.oidc.bearer-user-info-github-service.provider=github +quarkus.oidc.bearer-user-info-github-service.token.verify-access-token-with-user-info=true +quarkus.oidc.bearer-user-info-github-service.token.allow-jwt-introspection=false +quarkus.oidc.bearer-user-info-github-service.application-type=service +quarkus.oidc.bearer-user-info-github-service.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.bearer-user-info-github-service.user-info-path=github/userinfo +quarkus.oidc.bearer-user-info-github-service.client-id=quarkus-web-app +quarkus.oidc.bearer-user-info-github-service.credentials.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow + quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.provider=github quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.authorization-path=/ diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerOpaqueTokenAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerOpaqueTokenAuthorizationTest.java index 88fdac3650729..2c70886ec6fa4 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerOpaqueTokenAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerOpaqueTokenAuthorizationTest.java @@ -1,5 +1,9 @@ package io.quarkus.it.keycloak; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static org.hamcrest.Matchers.equalTo; import java.util.Arrays; @@ -7,8 +11,12 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; +import com.github.tomakehurst.wiremock.WireMockServer; + +import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.oidc.server.OidcWireMock; import io.quarkus.test.oidc.server.OidcWiremockTestResource; import io.restassured.RestAssured; @@ -16,6 +24,9 @@ @QuarkusTestResource(OidcWiremockTestResource.class) public class BearerOpaqueTokenAuthorizationTest { + @OidcWireMock + WireMockServer wireMockServer; + @Test public void testSecureAccessSuccessPreferredUsername() { for (String username : Arrays.asList("alice", "admin")) { @@ -64,4 +75,54 @@ public void testExpiredBearerToken() { .statusCode(401); } + @Test + public void testGitHubBearerTokenSuccess() { + final String validToken = OidcConstants.BEARER_SCHEME + " ghu_XirRniLaPuW53pDylNnAPOPBm14taM0C9HP4"; + wireMockServer.stubFor( + get(urlEqualTo("/auth/realms/quarkus/github/userinfo")) + .withHeader("Authorization", matching(validToken)) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"preferred_username\": \"alice\"" + + "}"))); + + RestAssured.given() + .header("Authorization", validToken) + .get("/bearer-user-info-github-service") + .then() + .statusCode(200) + .body(Matchers.equalTo("alice:alice:ghu_XirRniLaPuW53pDylNnAPOPBm14taM0C9HP4")); + } + + @Test + public void testGitHubBearerTokenUnauthorized() { + final String invalidToken = OidcConstants.BEARER_SCHEME + " Invalid"; + wireMockServer.stubFor( + get(urlEqualTo("/auth/realms/quarkus/github/userinfo")) + .withHeader("Authorization", matching(invalidToken)) + .willReturn(aResponse().withStatus(401))); + + RestAssured.given() + .header("Authorization", invalidToken) + .get("/bearer-user-info-github-service") + .then() + .statusCode(401); + } + + @Test + public void testGitHubBearerTokenNullUserInfo() { + final String validToken = OidcConstants.BEARER_SCHEME + " ghu_AAAAniLaPuW53pDylNnAPOPBm14ta7777777"; + wireMockServer.stubFor( + get(urlEqualTo("/auth/realms/quarkus/github/userinfo")) + .withHeader("Authorization", matching(validToken)) + .willReturn(aResponse().withStatus(200).withBody((String) null))); + + RestAssured.given() + .header("Authorization", validToken) + .get("/bearer-user-info-github-service") + .then() + .statusCode(401); + } + } diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java index 9b574aa049a34..50eb9458a38f0 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -15,6 +15,7 @@ import java.net.URL; import java.util.Set; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; @@ -44,6 +45,17 @@ public class CodeFlowAuthorizationTest { @OidcWireMock WireMockServer wireMockServer; + @BeforeAll + public static void clearCache() { + // clear token cache to make tests idempotent as we experienced failures + // on Windows when BearerTokenAuthorizationTest run before CodeFlowAuthorizationTest + RestAssured + .given() + .get("http://localhost:8081/clear-token-cache") + .then() + .statusCode(204); + } + @Test public void testCodeFlow() throws IOException { defineCodeFlowLogoutStub(); @@ -99,6 +111,7 @@ private void doTestCodeFlowEncryptedIdToken(String tenant) throws IOException { } } + @Test public void testCodeFlowFormPostAndBackChannelLogout() throws IOException { defineCodeFlowLogoutStub(); try (final WebClient webClient = createWebClient()) { diff --git a/integration-tests/oidc/src/main/resources/application.properties b/integration-tests/oidc/src/main/resources/application.properties index a82730a4bccf7..c9e1a6d9f3511 100644 --- a/integration-tests/oidc/src/main/resources/application.properties +++ b/integration-tests/oidc/src/main/resources/application.properties @@ -12,6 +12,7 @@ quarkus.oidc.tls.key-store-password=password quarkus.native.additional-build-args=-H:IncludeResources=.*\\.jks quarkus.http.cors=true +quarkus.http.cors.origins=* quarkus.http.auth.basic=true quarkus.security.users.embedded.enabled=true diff --git a/integration-tests/quartz/src/main/java/io/quarkus/it/quartz/FixedInstanceIdGenerator.java b/integration-tests/quartz/src/main/java/io/quarkus/it/quartz/FixedInstanceIdGenerator.java new file mode 100644 index 0000000000000..e809bf44ad0d0 --- /dev/null +++ b/integration-tests/quartz/src/main/java/io/quarkus/it/quartz/FixedInstanceIdGenerator.java @@ -0,0 +1,23 @@ +package io.quarkus.it.quartz; + +import org.quartz.SchedulerException; +import org.quartz.spi.InstanceIdGenerator; + +public class FixedInstanceIdGenerator implements InstanceIdGenerator { + + private String instanceId; + + @Override + public String generateInstanceId() throws SchedulerException { + return instanceId; + } + + public String getInstanceId() { + return this.instanceId; + } + + public void setInstanceId(String instanceId) { + this.instanceId = instanceId; + } + +} diff --git a/integration-tests/quartz/src/main/java/io/quarkus/it/quartz/FixedInstanceIdResource.java b/integration-tests/quartz/src/main/java/io/quarkus/it/quartz/FixedInstanceIdResource.java new file mode 100644 index 0000000000000..bc88a39142e37 --- /dev/null +++ b/integration-tests/quartz/src/main/java/io/quarkus/it/quartz/FixedInstanceIdResource.java @@ -0,0 +1,24 @@ +package io.quarkus.it.quartz; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.quartz.Scheduler; +import org.quartz.SchedulerException; + +@Path("/scheduler/instance-id") +public class FixedInstanceIdResource { + + @Inject + Scheduler quartzScheduler; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getInstanceId() throws SchedulerException { + return quartzScheduler.getSchedulerInstanceId(); + } + +} diff --git a/integration-tests/quartz/src/main/resources/application.properties b/integration-tests/quartz/src/main/resources/application.properties index 84cc04b240877..5a7202bdf9440 100644 --- a/integration-tests/quartz/src/main/resources/application.properties +++ b/integration-tests/quartz/src/main/resources/application.properties @@ -15,6 +15,11 @@ quarkus.flyway.baseline-on-migrate=true quarkus.flyway.baseline-version=1.0 quarkus.flyway.baseline-description=Quartz +# Use a instance ID generator +quarkus.quartz.instance-id=fixed +quarkus.quartz.instance-id-generators.fixed.class=io.quarkus.it.quartz.FixedInstanceIdGenerator +quarkus.quartz.instance-id-generators.fixed.properties.instanceId=myInstanceId + # Register de LoggingJobHistoryPlugin for testing quarkus.quartz.plugins.jobHistory.class=org.quartz.plugins.history.LoggingJobHistoryPlugin quarkus.quartz.plugins.jobHistory.properties.jobSuccessMessage=Job [{1}.{0}] execution complete and reports: {8} diff --git a/integration-tests/quartz/src/test/java/io/quarkus/it/quartz/QuartzTestCase.java b/integration-tests/quartz/src/test/java/io/quarkus/it/quartz/QuartzTestCase.java index e5fd18363e7cf..b00a9e47faae2 100644 --- a/integration-tests/quartz/src/test/java/io/quarkus/it/quartz/QuartzTestCase.java +++ b/integration-tests/quartz/src/test/java/io/quarkus/it/quartz/QuartzTestCase.java @@ -28,6 +28,11 @@ public void testDisabledMethodsShouldNeverBeExecuted() throws InterruptedExcepti assertEmptyValueForDisabledMethod("/scheduler/disabled/every"); } + @Test + public void testFixedInstanceIdGenerator() { + assertExpectedBodyString("/scheduler/instance-id", "myInstanceId"); + } + private void assertCounter(String counterPath) { Response response = given().when().get(counterPath); String body = response.asString(); @@ -39,9 +44,13 @@ private void assertCounter(String counterPath) { } private void assertEmptyValueForDisabledMethod(String path) { + assertExpectedBodyString(path, ""); + } + + private void assertExpectedBodyString(String path, String expectedBody) { Response response = given().when().get(path); String body = response.asString(); - assertEquals("", body); + assertEquals(expectedBody, body); response .then() .statusCode(200); diff --git a/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/RedisResource.java b/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/RedisResource.java index e088c44fce7f6..a84ba98f88555 100644 --- a/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/RedisResource.java +++ b/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/RedisResource.java @@ -1,5 +1,7 @@ package io.quarkus.redis.it; +import java.util.List; + import javax.enterprise.context.ApplicationScoped; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -8,6 +10,7 @@ import io.quarkus.redis.datasource.ReactiveRedisDataSource; import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.redis.datasource.keys.ReactiveKeyCommands; import io.quarkus.redis.datasource.value.ReactiveValueCommands; import io.quarkus.redis.datasource.value.ValueCommands; import io.smallrye.mutiny.Uni; @@ -18,11 +21,13 @@ public class RedisResource { private final ValueCommands blocking; private final ReactiveValueCommands reactive; + private final ReactiveKeyCommands keys; public RedisResource(RedisDataSource ds, ReactiveRedisDataSource reactiveDs) { blocking = ds.value(String.class); reactive = reactiveDs.value(String.class); + keys = reactiveDs.key(); } // synchronous @@ -51,4 +56,10 @@ public Uni setReactive(@PathParam("key") String key, String value) { return this.reactive.set(key, value); } + @GET + @Path("/import") + public Uni startWarsKey() { + return keys.keys("people:*").map(List::size); + } + } diff --git a/integration-tests/redis-client/src/main/resources/application.properties b/integration-tests/redis-client/src/main/resources/application.properties index 01f0e74a02688..6bb245e908d5e 100644 --- a/integration-tests/redis-client/src/main/resources/application.properties +++ b/integration-tests/redis-client/src/main/resources/application.properties @@ -7,3 +7,5 @@ quarkus.redis.parameter-injection.hosts=redis://localhost:6379/2 quarkus.redis.instance-client.hosts=redis://localhost:6379/5 # use DB 3 quarkus.redis.provided-hosts.hosts-provider-name=test-hosts-provider + +quarkus.redis.load-script=starwars.redis \ No newline at end of file diff --git a/integration-tests/redis-client/src/main/resources/starwars.redis b/integration-tests/redis-client/src/main/resources/starwars.redis new file mode 100644 index 0000000000000..2d165d3515a91 --- /dev/null +++ b/integration-tests/redis-client/src/main/resources/starwars.redis @@ -0,0 +1,6 @@ +HSET people:1 firstName "luke" lastName "skywalker" +HSET people:2 firstName "leia" lastName "organa" +HSET people:3 firstName "anakin" lastName "skywalker" +HSET people:4 firstName "c-3po" lastName "" +HSET people:5 firstName "r2-d2" lastName "" +HSET people:6 firstName "owen" lastName "lars" \ No newline at end of file diff --git a/integration-tests/redis-client/src/test/java/io/quarkus/redis/it/QuarkusRedisTest.java b/integration-tests/redis-client/src/test/java/io/quarkus/redis/it/QuarkusRedisTest.java index 238584da636e7..a8d2f27659de3 100644 --- a/integration-tests/redis-client/src/test/java/io/quarkus/redis/it/QuarkusRedisTest.java +++ b/integration-tests/redis-client/src/test/java/io/quarkus/redis/it/QuarkusRedisTest.java @@ -1,6 +1,7 @@ package io.quarkus.redis.it; import org.hamcrest.CoreMatchers; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; @@ -78,4 +79,11 @@ public void reactive() { .body(CoreMatchers.is(REACTIVE_VALUE)); } } + + @Test + public void testPreloading() { + RestAssured.get("/quarkus-redis/import").then() + .statusCode(200) + .body(Matchers.equalTo("6")); + } } diff --git a/integration-tests/rest-client-reactive/pom.xml b/integration-tests/rest-client-reactive/pom.xml index 030ba003a3c9a..f7fd7db83b29a 100644 --- a/integration-tests/rest-client-reactive/pom.xml +++ b/integration-tests/rest-client-reactive/pom.xml @@ -14,6 +14,8 @@ ${project.build.directory}/self-signed.p12 changeit + ${project.build.directory}/wrong-host.p12 + changeit @@ -175,6 +177,23 @@ LEAF + + wrong-host-truststore + generate-test-resources + + generate-truststore + + + PKCS12 + ${wrong-host.trust-store} + ${wrong-host.trust-store-password} + + wrong.host.badssl.com:443 + + true + LEAF + + diff --git a/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java b/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java index 1e311fef17815..9e88e5bd86c63 100644 --- a/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java +++ b/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java @@ -22,6 +22,7 @@ import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; import io.quarkus.it.rest.client.main.MyResponseExceptionMapper.MyException; import io.quarkus.it.rest.client.main.selfsigned.ExternalSelfSignedClient; +import io.quarkus.it.rest.client.main.wronghost.WrongHostClient; import io.smallrye.mutiny.Uni; import io.vertx.core.Future; import io.vertx.core.json.Json; @@ -48,6 +49,9 @@ public class ClientCallingResource { @RestClient ExternalSelfSignedClient externalSelfSignedClient; + @RestClient + WrongHostClient wrongHostClient; + @Inject InMemorySpanExporter inMemorySpanExporter; @@ -172,6 +176,9 @@ void init(@Observes Router router) { router.get("/self-signed").blockingHandler( rc -> rc.response().setStatusCode(200).end(String.valueOf(externalSelfSignedClient.invoke().getStatus()))); + + router.get("/wrong-host").blockingHandler( + rc -> rc.response().setStatusCode(200).end(String.valueOf(wrongHostClient.invoke().getStatus()))); } private Future success(RoutingContext rc, String body) { diff --git a/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/wronghost/WrongHostClient.java b/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/wronghost/WrongHostClient.java new file mode 100644 index 0000000000000..5ad6b037d7b18 --- /dev/null +++ b/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/wronghost/WrongHostClient.java @@ -0,0 +1,18 @@ +package io.quarkus.it.rest.client.main.wronghost; + +import javax.ws.rs.GET; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.eclipse.microprofile.faulttolerance.Retry; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient(baseUri = "https://wrong.host.badssl.com/", configKey = "wrong-host") +public interface WrongHostClient { + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Retry(delay = 1000) + Response invoke(); +} diff --git a/integration-tests/rest-client-reactive/src/main/resources/application.properties b/integration-tests/rest-client-reactive/src/main/resources/application.properties index 03c013f85ce30..4bf8d7c8f6404 100644 --- a/integration-tests/rest-client-reactive/src/main/resources/application.properties +++ b/integration-tests/rest-client-reactive/src/main/resources/application.properties @@ -2,6 +2,10 @@ w-exception-mapper/mp-rest/url=${test.url} w-fault-tolerance/mp-rest/url=${test.url} io.quarkus.it.rest.client.main.ParamClient/mp-rest/url=${test.url} io.quarkus.it.rest.client.multipart.MultipartClient/mp-rest/url=${test.url} -# HTTPS +# Self-Signed client quarkus.rest-client.self-signed.trust-store=${self-signed.trust-store} quarkus.rest-client.self-signed.trust-store-password=${self-signed.trust-store-password} +# Wrong Host client +quarkus.rest-client.wrong-host.trust-store=${wrong-host.trust-store} +quarkus.rest-client.wrong-host.trust-store-password=${wrong-host.trust-store-password} +quarkus.rest-client.wrong-host.verify-host=false diff --git a/integration-tests/rest-client-reactive/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostTestCase.java b/integration-tests/rest-client-reactive/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostTestCase.java new file mode 100644 index 0000000000000..d963c850c0dce --- /dev/null +++ b/integration-tests/rest-client-reactive/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostTestCase.java @@ -0,0 +1,20 @@ +package io.quarkus.it.rest.client.wronghost; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class ExternalWrongHostTestCase { + @Test + public void restClient() { + when() + .get("/wrong-host") + .then() + .statusCode(200) + .body(is("200")); + } +} diff --git a/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/trustall/ExternalTlsTrustAllTestCase.java b/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/trustall/ExternalTlsTrustAllTestCase.java index ba89527182f5d..99e93df420eaf 100644 --- a/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/trustall/ExternalTlsTrustAllTestCase.java +++ b/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/trustall/ExternalTlsTrustAllTestCase.java @@ -5,11 +5,13 @@ import org.junit.jupiter.api.Test; +import io.quarkus.it.rest.client.wronghost.ExternalWrongHostTestResourceUsingHostnameVerifier; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; @QuarkusTest @QuarkusTestResource(ExternalTlsTrustAllTestResource.class) +@QuarkusTestResource(value = ExternalWrongHostTestResourceUsingHostnameVerifier.class, restrictToAnnotatedClass = true) public class ExternalTlsTrustAllTestCase { @Test diff --git a/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostTestCase.java b/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/BaseExternalWrongHostTestCase.java similarity index 79% rename from integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostTestCase.java rename to integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/BaseExternalWrongHostTestCase.java index 5cfc0380d7c2e..3e3a5a85599d6 100644 --- a/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostTestCase.java +++ b/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/BaseExternalWrongHostTestCase.java @@ -7,12 +7,7 @@ import org.junit.jupiter.api.Test; -import io.quarkus.test.common.QuarkusTestResource; -import io.quarkus.test.junit.QuarkusTest; - -@QuarkusTest -@QuarkusTestResource(ExternalWrongHostTestResource.class) -public class ExternalWrongHostTestCase { +public abstract class BaseExternalWrongHostTestCase { @Test public void restClient() { diff --git a/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostTestResource.java b/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostTestResourceUsingHostnameVerifier.java similarity index 87% rename from integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostTestResource.java rename to integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostTestResourceUsingHostnameVerifier.java index 723ea722b6638..e9f7cfe023c9e 100644 --- a/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostTestResource.java +++ b/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostTestResourceUsingHostnameVerifier.java @@ -9,7 +9,7 @@ /** * The only point of this class is to propagate the properties when running the native tests */ -public class ExternalWrongHostTestResource implements QuarkusTestResourceLifecycleManager { +public class ExternalWrongHostTestResourceUsingHostnameVerifier implements QuarkusTestResourceLifecycleManager { @Override public Map start() { diff --git a/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostTestResourceUsingVerifyHost.java b/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostTestResourceUsingVerifyHost.java new file mode 100644 index 0000000000000..8ee70c21f8db0 --- /dev/null +++ b/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostTestResourceUsingVerifyHost.java @@ -0,0 +1,26 @@ +package io.quarkus.it.rest.client.wronghost; + +import java.util.HashMap; +import java.util.Map; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +/** + * The only point of this class is to propagate the properties when running the native tests + */ +public class ExternalWrongHostTestResourceUsingVerifyHost implements QuarkusTestResourceLifecycleManager { + + @Override + public Map start() { + Map result = new HashMap<>(); + result.put("wrong-host/mp-rest/trustStore", System.getProperty("rest-client.trustStore")); + result.put("wrong-host/mp-rest/trustStorePassword", System.getProperty("rest-client.trustStorePassword")); + result.put("wrong-host/mp-rest/verifyHost", Boolean.FALSE.toString()); + return result; + } + + @Override + public void stop() { + + } +} diff --git a/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostIT.java b/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostUsingHostnameVerifierIT.java similarity index 53% rename from integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostIT.java rename to integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostUsingHostnameVerifierIT.java index ab7625fee03e1..4b1cad5c37f40 100644 --- a/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostIT.java +++ b/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostUsingHostnameVerifierIT.java @@ -3,5 +3,5 @@ import io.quarkus.test.junit.QuarkusIntegrationTest; @QuarkusIntegrationTest -public class ExternalWrongHostIT extends ExternalWrongHostTestCase { +public class ExternalWrongHostUsingHostnameVerifierIT extends ExternalWrongHostUsingHostnameVerifierTestCase { } diff --git a/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostUsingHostnameVerifierTestCase.java b/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostUsingHostnameVerifierTestCase.java new file mode 100644 index 0000000000000..382b68e36ceff --- /dev/null +++ b/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostUsingHostnameVerifierTestCase.java @@ -0,0 +1,9 @@ +package io.quarkus.it.rest.client.wronghost; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@QuarkusTestResource(value = ExternalWrongHostTestResourceUsingHostnameVerifier.class, restrictToAnnotatedClass = true) +public class ExternalWrongHostUsingHostnameVerifierTestCase extends BaseExternalWrongHostTestCase { +} diff --git a/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostUsingVerifyHostTestCase.java b/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostUsingVerifyHostTestCase.java new file mode 100644 index 0000000000000..9ad9ce8d3930f --- /dev/null +++ b/integration-tests/rest-client/src/test/java/io/quarkus/it/rest/client/wronghost/ExternalWrongHostUsingVerifyHostTestCase.java @@ -0,0 +1,9 @@ +package io.quarkus.it.rest.client.wronghost; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@QuarkusTestResource(value = ExternalWrongHostTestResourceUsingVerifyHost.class, restrictToAnnotatedClass = true) +public class ExternalWrongHostUsingVerifyHostTestCase extends BaseExternalWrongHostTestCase { +} diff --git a/integration-tests/resteasy-mutiny/pom.xml b/integration-tests/resteasy-mutiny/pom.xml index 4332b5dfec8ab..73ee7fc0bdeeb 100644 --- a/integration-tests/resteasy-mutiny/pom.xml +++ b/integration-tests/resteasy-mutiny/pom.xml @@ -21,7 +21,7 @@ io.quarkus - quarkus-rest-client-reactive + quarkus-rest-client-reactive-jsonb io.quarkus @@ -46,7 +46,7 @@ io.quarkus - quarkus-rest-client-reactive-deployment + quarkus-rest-client-reactive-jsonb-deployment ${project.version} pom test diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/GreetingApplication.kt b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/GreetingApplication.kt new file mode 100644 index 0000000000000..1b18c62d2a3d4 --- /dev/null +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/GreetingApplication.kt @@ -0,0 +1,15 @@ +package io.quarkus.it.resteasy.reactive.kotlin + +import io.quarkus.runtime.Quarkus.run +import io.quarkus.runtime.annotations.QuarkusMain + +@QuarkusMain +class GreetingApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + run(*args) + } + } +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties b/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties index e4ff9949fbbaf..6658f1736fb16 100644 --- a/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties @@ -1,7 +1,6 @@ mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus smallrye.jwt.path.groups=realm_access/roles - io.quarkus.it.keycloak.JwtTokenPropagationService/mp-rest/uri=http://localhost:8081/jwt-resigned-protected io.quarkus.it.keycloak.AccessTokenPropagationService/mp-rest/uri=http://localhost:8081/protected @@ -13,4 +12,4 @@ smallrye.jwt.new-token.override-matching-claims=true quarkus.http.auth.proactive=false -quarkus.native.additional-build-args=-H:IncludeResources=.*\\.pem \ No newline at end of file +quarkus.native.additional-build-args=-H:IncludeResources=.*\\.pem diff --git a/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java index 13139c870675e..45812da96c15a 100644 --- a/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java +++ b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java @@ -2,6 +2,11 @@ import static org.hamcrest.Matchers.is; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + import org.junit.jupiter.api.Test; import io.quarkus.test.common.http.TestHTTPEndpoint; @@ -17,40 +22,61 @@ public class TestSecurityLazyAuthTest { @Test - @TestSecurity(user = "user1", roles = "viewer") + @TestAsUser1Viewer public void testWithDummyUser() { RestAssured.when().get("test-security").then() .body(is("user1:user1:user1")); } @Test - @TestSecurity(user = "user1", roles = "tester") + @TestAsUser1Tester public void testWithDummyUserForbidden() { RestAssured.when().get("test-security").then().statusCode(403); } @Test - @TestSecurity(user = "user1", roles = "viewer") + @TestAsUser1Viewer public void testPostWithDummyUser() { RestAssured.given().contentType(ContentType.JSON).when().body("{\"name\":\"user1\"}").post("test-security").then() .body(is("user1:user1")); } @Test - @TestSecurity(user = "user1", roles = "tester") + @TestAsUser1Tester public void testPostWithDummyUserForbidden() { RestAssured.given().contentType(ContentType.JSON).when().body("{\"name\":\"user1\"}").post("test-security").then() .statusCode(403); } @Test + @TestAsUserJwtViewer + public void testJwtGetWithDummyUser() { + RestAssured.when().get("test-security-jwt").then() + .body(is("userJwt:userJwt:userJwt:viewer:user@gmail.com")); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD }) + @TestSecurity(user = "user1", roles = "viewer") + public @interface TestAsUser1Viewer { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD }) + @TestSecurity(user = "user1", roles = "tester") + public @interface TestAsUser1Tester { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD }) @TestSecurity(user = "userJwt", roles = "viewer") @JwtSecurity(claims = { @Claim(key = "email", value = "user@gmail.com") }) - public void testJwtGetWithDummyUser() { - RestAssured.when().get("test-security-jwt").then() - .body(is("userJwt:userJwt:userJwt:viewer:user@gmail.com")); + public @interface TestAsUserJwtViewer { + } } diff --git a/integration-tests/test-extension/extension/deployment/src/main/java/io/quarkus/extest/deployment/TestProcessor.java b/integration-tests/test-extension/extension/deployment/src/main/java/io/quarkus/extest/deployment/TestProcessor.java index f1f7bf97d718f..01b27cd7d4002 100644 --- a/integration-tests/test-extension/extension/deployment/src/main/java/io/quarkus/extest/deployment/TestProcessor.java +++ b/integration-tests/test-extension/extension/deployment/src/main/java/io/quarkus/extest/deployment/TestProcessor.java @@ -32,6 +32,7 @@ import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.arc.deployment.ConfigPropertyBuildItem; +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; @@ -62,6 +63,7 @@ import io.quarkus.extest.runtime.config.TestMappingBuildTime; import io.quarkus.extest.runtime.config.TestMappingBuildTimeRunTime; import io.quarkus.extest.runtime.config.TestMappingRunTime; +import io.quarkus.extest.runtime.config.UnremovableMappingFromBuildItem; import io.quarkus.extest.runtime.config.XmlConfig; import io.quarkus.extest.runtime.logging.AdditionalLogHandlerValueFactory; import io.quarkus.extest.runtime.runtimeinitializedpackage.RuntimeInitializedClass; @@ -475,6 +477,11 @@ void runTimeConfigBuilder(BuildProducer configBui configBuilders.produce(new RunTimeConfigBuilderBuildItem(RunTimeConfigBuilder.class.getName())); } + @BuildStep + void unremoveableBeans(BuildProducer unremovableBeans) { + unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(UnremovableMappingFromBuildItem.class)); + } + @BuildStep(onlyIf = Never.class) void neverRunThisOne() { throw new IllegalStateException("Not supposed to run!"); diff --git a/integration-tests/test-extension/extension/deployment/src/main/resources/application.properties b/integration-tests/test-extension/extension/deployment/src/main/resources/application.properties index 1170bbeb28cf3..82ee3392f780a 100644 --- a/integration-tests/test-extension/extension/deployment/src/main/resources/application.properties +++ b/integration-tests/test-extension/extension/deployment/src/main/resources/application.properties @@ -184,3 +184,5 @@ my.prefix.bt.nested.oov=nested-1234+nested-5678 another.another-prefix.prop=5678 another.another-prefix.map.prop=5678 + +unremoveable.value=1234 diff --git a/integration-tests/test-extension/extension/deployment/src/test/java/io/quarkus/config/UnremoveableConfigMappingTest.java b/integration-tests/test-extension/extension/deployment/src/test/java/io/quarkus/config/UnremoveableConfigMappingTest.java new file mode 100644 index 0000000000000..600a1aa45f98c --- /dev/null +++ b/integration-tests/test-extension/extension/deployment/src/test/java/io/quarkus/config/UnremoveableConfigMappingTest.java @@ -0,0 +1,28 @@ +package io.quarkus.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.extest.runtime.config.UnremovableMappingFromBuildItem; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.config.SmallRyeConfig; + +public class UnremoveableConfigMappingTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(UnremovableMappingFromBuildItem.class)); + + @Inject + SmallRyeConfig config; + + @Test + void unremoveableMapping() { + UnremovableMappingFromBuildItem mapping = config.getConfigMapping(UnremovableMappingFromBuildItem.class); + assertEquals("1234", mapping.value()); + } +} diff --git a/integration-tests/test-extension/extension/runtime/src/main/java/io/quarkus/extest/runtime/config/UnremovableMappingFromBuildItem.java b/integration-tests/test-extension/extension/runtime/src/main/java/io/quarkus/extest/runtime/config/UnremovableMappingFromBuildItem.java new file mode 100644 index 0000000000000..b844fdc10cd8e --- /dev/null +++ b/integration-tests/test-extension/extension/runtime/src/main/java/io/quarkus/extest/runtime/config/UnremovableMappingFromBuildItem.java @@ -0,0 +1,8 @@ +package io.quarkus.extest.runtime.config; + +import io.smallrye.config.ConfigMapping; + +@ConfigMapping(prefix = "unremoveable") +public interface UnremovableMappingFromBuildItem { + String value(); +} diff --git a/jakarta/overrides/rest-client/QuarkusRestClientBuilder.java b/jakarta/overrides/rest-client/QuarkusRestClientBuilder.java index 501e18f059b09..07dd31a1dcaea 100644 --- a/jakarta/overrides/rest-client/QuarkusRestClientBuilder.java +++ b/jakarta/overrides/rest-client/QuarkusRestClientBuilder.java @@ -50,6 +50,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.ext.ParamConverterProvider; +import io.quarkus.runtime.ImageMode; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.rest.client.RestClientBuilder; @@ -59,7 +60,6 @@ import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; -import org.graalvm.nativeimage.ImageInfo; import org.jboss.logging.Logger; import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; import org.jboss.resteasy.client.jaxrs.ResteasyClient; @@ -338,7 +338,7 @@ public T build(Class aClass, ClientHttpEngine httpEngine) configureTrustAll(resteasyClientBuilder); // we need to push a disabled SSL context when SSL has been disabled // because otherwise Apache HTTP Client will try to initialize one and will fail - if (ImageInfo.inImageRuntimeCode() && !SslContextConfiguration.isSslNativeEnabled()) { + if (ImageMode.current() == ImageMode.NATIVE_RUN && !SslContextConfiguration.isSslNativeEnabled()) { resteasyClientBuilder.sslContext(new DisabledSSLContext()); } diff --git a/jakarta/quarkus3.yml b/jakarta/quarkus3.yml index a000584aeb224..20ed40c60c855 100644 --- a/jakarta/quarkus3.yml +++ b/jakarta/quarkus3.yml @@ -24,10 +24,10 @@ tags: recipeList: - org.openrewrite.maven.ChangePropertyValue: key: quarkus.platform.version - newValue: 3.0.0.Alpha1 + newValue: 3.0.0.Alpha2 - org.openrewrite.maven.ChangePropertyValue: key: quarkus.version - newValue: 3.0.0.Alpha1 + newValue: 3.0.0.Alpha2 - org.openrewrite.java.migrate.JavaxMigrationToJakarta --- type: specs.openrewrite.org/v1beta/recipe diff --git a/jakarta/rewrite.yml b/jakarta/rewrite.yml index 7b4f564497be7..6c7d255d15bc6 100644 --- a/jakarta/rewrite.yml +++ b/jakarta/rewrite.yml @@ -567,7 +567,7 @@ recipeList: newValue: '3.0' - org.openrewrite.maven.ChangePropertyValue: key: smallrye-config.version - newValue: '3.1.0' + newValue: '3.1.1' - org.openrewrite.maven.ChangePropertyValue: key: smallrye-fault-tolerance.version newValue: '6.1.0' @@ -613,8 +613,8 @@ recipeList: key: smallrye-metrics.version newValue: '4.0.0' - org.openrewrite.maven.ChangePropertyValue: - key: smallrye-open-api.version - newValue: '3.0.1' + key: microprofile-openapi.version + newValue: '3.0' - org.openrewrite.maven.ChangePropertyValue: key: microprofile-opentracing-api.version newValue: '3.0' @@ -623,7 +623,7 @@ recipeList: newValue: '3.0.0' - org.openrewrite.maven.ChangePropertyValue: key: smallrye-reactive-messaging.version - newValue: '4.1.0' + newValue: '4.1.1' - org.openrewrite.maven.ChangePropertyValue: key: microprofile-rest-client.version newValue: '3.0' diff --git a/jakarta/transform.sh b/jakarta/transform.sh index a381dc8986160..0fb11684df14b 100755 --- a/jakarta/transform.sh +++ b/jakarta/transform.sh @@ -34,7 +34,7 @@ if [ "${REWRITE_OFFLINE-false}" != "true" ]; then # Build Kotlin Maven Plugin to allow skipping main compilation # (skipping test compilation is supported but not main) rm -rf target/kotlin - git clone -b v1.7.21-jakarta --depth 1 https://github.com/gsmet/kotlin.git target/kotlin + git clone -b v1.7.22-jakarta --depth 1 https://github.com/gsmet/kotlin.git target/kotlin pushd target/kotlin/libraries/tools/kotlin-maven-plugin mvn -B clean install -DskipTests -DskipITs popd @@ -74,9 +74,10 @@ transform_module () { transform_kotlin_module () { # this is very ad hoc but hopefully it will be good enough - for package in javax.inject. javax.enterprise. javax.ws.rs. javax.annotation. javax.persistence. javax.json. javax.websocket. javax.xml.bind. javax.transaction.Transactional; do + for package in javax.inject. javax.enterprise. javax.ws.rs. javax.annotation. javax.persistence. javax.json. javax.websocket. javax.xml.bind. javax.transaction.Transactional javax.validation.; do local newPackage=${package/javax/jakarta} find $1 -name '*.kt' | xargs --no-run-if-empty sed -i "s@import ${package}@import ${newPackage}@g" + find $1 -name '*.kts' | xargs --no-run-if-empty sed -i "s@annotation(\"${package}@annotation(\"${newPackage}@g" done } @@ -301,6 +302,9 @@ sed -i 's@org.jboss.narayana.rts:narayana-lra@org.jboss.narayana.rts:narayana-lr sed -i 's@org.jboss.narayana.rts:lra-client@org.jboss.narayana.rts:lra-client-jakarta@g' extensions/narayana-lra/runtime/pom.xml sed -i 's@META-INF/services/javax.ws.rs.client.ClientBuilder@META-INF/services/jakarta.ws.rs.client.ClientBuilder@g' extensions/narayana-lra/runtime/pom.xml +# Disable REST Client TCK timeout tests that are not working +sed -i 's@ @ \n org.eclipse.microprofile.rest.client.tck.timeout.TimeoutBuilderIndependentOfMPConfigTest\n org.eclipse.microprofile.rest.client.tck.timeout.TimeoutTest\n org.eclipse.microprofile.rest.client.tck.timeout.TimeoutViaMPConfigTest\n org.eclipse.microprofile.rest.client.tck.timeout.TimeoutViaMPConfigWithConfigKeyTest\n @' tcks/microprofile-rest-client/pom.xml + find integration-tests/gradle -name build.gradle | xargs sed -i 's/javax.enterprise.context.ApplicationScoped/jakarta.enterprise.context.ApplicationScoped/g' find integration-tests/gradle -name build.gradle | xargs sed -i 's/javax.ws.rs.Path/jakarta.ws.rs.Path/g' diff --git a/pom.xml b/pom.xml index d6261c4c69327..afde16bccf1a8 100644 --- a/pom.xml +++ b/pom.xml @@ -69,11 +69,11 @@ - 1.51.0 + 1.51.1 1.2.1 3.21.9 ${protoc.version} - 2.10.0 + 2.11.0 diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java b/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java index f3c2d2c7e1412..e42ac10e8ee86 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java @@ -5,19 +5,27 @@ import static io.quarkus.test.common.LauncherUtil.waitForCapturedListeningData; import static io.quarkus.test.common.LauncherUtil.waitForStartedFunction; import static java.lang.ProcessBuilder.Redirect.DISCARD; +import static java.lang.ProcessBuilder.Redirect.PIPE; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.net.ServerSocket; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.Function; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.TeeInputStream; import org.apache.commons.lang3.RandomStringUtils; import io.quarkus.runtime.util.ContainerRuntimeUtil; @@ -43,6 +51,8 @@ public class DefaultDockerContainerLauncher implements DockerContainerArtifactLa private String containerRuntimeBinaryName; + private ExecutorService executorService = Executors.newSingleThreadExecutor(); + @Override public void init(DockerContainerArtifactLauncher.DockerInitContext initContext) { this.httpPort = initContext.httpPort(); @@ -131,6 +141,10 @@ public void start() throws IOException { Files.deleteIfExists(logFile); Files.createDirectories(logFile.getParent()); + Path containerLogFile = Paths.get("target", "container.log"); + Files.createDirectories(containerLogFile.getParent()); + FileOutputStream containerLogOutputStream = new FileOutputStream(containerLogFile.toFile(), true); + System.out.println("Executing \"" + String.join(" ", args) + "\""); Function startedFunction = createStartedFunction(); @@ -138,8 +152,9 @@ public void start() throws IOException { // the idea here is to obtain the logs of the application simply by redirecting all its output the a file // this is done in contrast with the JarLauncher and NativeImageLauncher because in the case of the container // the log itself is written inside the container - Process quarkusProcess = new ProcessBuilder(args).redirectError(logFile.toFile()).redirectOutput(logFile.toFile()) - .start(); + Process quarkusProcess = new ProcessBuilder(args).redirectError(PIPE).redirectOutput(PIPE).start(); + InputStream tee = new TeeInputStream(quarkusProcess.getInputStream(), new FileOutputStream(logFile.toFile())); + executorService.submit(() -> IOUtils.copy(tee, containerLogOutputStream)); if (startedFunction != null) { IntegrationTestStartedNotifier.Result result = waitForStartedFunction(startedFunction, quarkusProcess, @@ -194,6 +209,7 @@ public void close() { } catch (IOException | InterruptedException e) { System.out.println("Unable to stop container '" + containerName + "'"); } + executorService.shutdown(); } } diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/TestClassIndexer.java b/test-framework/common/src/main/java/io/quarkus/test/common/TestClassIndexer.java index ec86da4fad87c..59eaac4c89322 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/TestClassIndexer.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/TestClassIndexer.java @@ -29,8 +29,11 @@ private TestClassIndexer() { } public static Index indexTestClasses(Class testClass) { + return indexTestClasses(getTestClassesLocation(testClass)); + } + + public static Index indexTestClasses(final Path testClassesLocation) { final Indexer indexer = new Indexer(); - final Path testClassesLocation = getTestClassesLocation(testClass); try { if (Files.isDirectory(testClassesLocation)) { indexTestClassesDir(indexer, testClassesLocation); @@ -48,7 +51,11 @@ public static Index indexTestClasses(Class testClass) { } public static void writeIndex(Index index, Class testClass) { - try (FileOutputStream fos = new FileOutputStream(indexPath(testClass).toFile(), false)) { + writeIndex(index, getTestClassesLocation(testClass), testClass); + } + + public static void writeIndex(Index index, Path testClassLocation, Class testClass) { + try (FileOutputStream fos = new FileOutputStream(indexPath(testClassLocation, testClass).toFile(), false)) { IndexWriter indexWriter = new IndexWriter(fos); indexWriter.write(index); } catch (IOException ignored) { @@ -58,7 +65,11 @@ public static void writeIndex(Index index, Class testClass) { } public static Index readIndex(Class testClass) { - Path path = indexPath(testClass); + return readIndex(getTestClassesLocation(testClass), testClass); + } + + public static Index readIndex(Path testClassLocation, Class testClass) { + Path path = indexPath(testClassLocation, testClass); if (path.toFile().exists()) { try (FileInputStream fis = new FileInputStream(path.toFile())) { return new IndexReader(fis).read(); @@ -75,7 +86,11 @@ public static Index readIndex(Class testClass) { } private static Path indexPath(Class testClass) { - return PathTestHelper.getTestClassesLocation(testClass).resolve(testClass.getSimpleName() + ".idx"); + return indexPath(PathTestHelper.getTestClassesLocation(testClass), testClass); + } + + private static Path indexPath(Path testClassLocation, Class testClass) { + return testClassLocation.resolve(testClass.getSimpleName() + ".idx"); } private static void indexTestClassesDir(Indexer indexer, final Path testClassesLocation) throws IOException { diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java index b75f18a68a79c..05fcffc736e64 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java @@ -4,6 +4,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -56,6 +57,13 @@ public TestResourceManager(Class testClass, Class profileClass, List testClass, Class profileClass, List additionalTestResources, boolean disableGlobalTestResources, Map devServicesProperties, Optional containerNetworkId) { + this(testClass, profileClass, additionalTestResources, disableGlobalTestResources, devServicesProperties, + containerNetworkId, PathTestHelper.getTestClassesLocation(testClass)); + } + + public TestResourceManager(Class testClass, Class profileClass, List additionalTestResources, + boolean disableGlobalTestResources, Map devServicesProperties, + Optional containerNetworkId, Path testClassLocation) { this.parallelTestResourceEntries = new ArrayList<>(); this.sequentialTestResourceEntries = new ArrayList<>(); @@ -65,7 +73,8 @@ public TestResourceManager(Class testClass, Class profileClass, List(additionalTestResources); } else { - uniqueEntries = getUniqueTestResourceClassEntries(testClass, profileClass, additionalTestResources); + uniqueEntries = getUniqueTestResourceClassEntries(testClassLocation, testClass, profileClass, + additionalTestResources); } Set remainingUniqueEntries = initParallelTestResources(uniqueEntries); initSequentialTestResources(remainingUniqueEntries); @@ -247,9 +256,10 @@ private TestResourceManager.TestResourceEntry buildTestResourceEntry(TestResourc } } - private Set getUniqueTestResourceClassEntries(Class testClass, Class profileClass, + private Set getUniqueTestResourceClassEntries(Path testClassLocation, Class testClass, + Class profileClass, List additionalTestResources) { - IndexView index = TestClassIndexer.readIndex(testClass); + IndexView index = TestClassIndexer.readIndex(testClassLocation, testClass); Set uniqueEntries = new LinkedHashSet<>(); // reload the test and profile classes in the right CL Class testClassFromTCCL; diff --git a/test-framework/common/src/main/java/io/quarkus/test/util/annotations/AnnotationContainer.java b/test-framework/common/src/main/java/io/quarkus/test/util/annotations/AnnotationContainer.java new file mode 100644 index 0000000000000..a67b60f5ef05b --- /dev/null +++ b/test-framework/common/src/main/java/io/quarkus/test/util/annotations/AnnotationContainer.java @@ -0,0 +1,23 @@ +package io.quarkus.test.util.annotations; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; + +public final class AnnotationContainer { + + private final AnnotatedElement element; + private final A annotation; + + public AnnotationContainer(AnnotatedElement element, A annotation) { + this.element = element; + this.annotation = annotation; + } + + public AnnotatedElement getElement() { + return element; + } + + public A getAnnotation() { + return annotation; + } +} diff --git a/test-framework/common/src/main/java/io/quarkus/test/util/annotations/AnnotationUtils.java b/test-framework/common/src/main/java/io/quarkus/test/util/annotations/AnnotationUtils.java new file mode 100644 index 0000000000000..49f210ab0c930 --- /dev/null +++ b/test-framework/common/src/main/java/io/quarkus/test/util/annotations/AnnotationUtils.java @@ -0,0 +1,126 @@ +package io.quarkus.test.util.annotations; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.lang.reflect.AnnotatedElement; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import org.junit.platform.commons.util.Preconditions; + +/** + * Provides utility methods for obtaining annotations on test classes. + * + * This class is basically an adaptation of {@link org.junit.platform.commons.support.AnnotationSupport} + * altered to include the element which was annotated in the result and filtered out to only contain methods we use. + */ +public final class AnnotationUtils { + + private AnnotationUtils() { + } + + /** + * Find the first annotation of {@code annotationType} that is either + * directly present, meta-present, or indirectly + * present on the supplied {@code element}. + * + *

    + * If the element is a class and the annotation is neither directly + * present nor meta-present on the class, this method will + * additionally search on interfaces implemented by the class before + * finding an annotation that is indirectly present on the class + * (meaning that the same process will be repeated for superclasses if {@link Inherited} is present on + * {@code annotationType}). + * + * @param the annotation type + * @param element the element on which to search for the annotation; may be + * {@code null} + * @param annotationType the annotation type to search for; never {@code null} + * @return an {@code Optional} containing the annotation and the element on which it was present; never {@code null} but + * potentially empty + */ + public static Optional> findAnnotation(AnnotatedElement element, + Class annotationType) { + Preconditions.notNull(annotationType, "annotationType must not be null"); + boolean inherited = annotationType.isAnnotationPresent(Inherited.class); + return findAnnotation(element, annotationType, inherited, new HashSet<>()); + } + + private static Optional> findAnnotation(AnnotatedElement element, + Class annotationType, + boolean inherited, Set visited) { + + Preconditions.notNull(annotationType, "annotationType must not be null"); + + if (element == null) { + return Optional.empty(); + } + + // Directly present? + A annotation = element.getDeclaredAnnotation(annotationType); + if (annotation != null) { + return Optional.of(new AnnotationContainer<>(element, annotation)); + } + + // Meta-present on directly present annotations? + Optional> directMetaAnnotation = findMetaAnnotation(annotationType, + element.getDeclaredAnnotations(), + inherited, visited); + if (directMetaAnnotation.isPresent()) { + return directMetaAnnotation; + } + + if (element instanceof Class) { + Class clazz = (Class) element; + + // Search on interfaces + for (Class ifc : clazz.getInterfaces()) { + if (ifc != Annotation.class) { + Optional> annotationOnInterface = findAnnotation(ifc, annotationType, inherited, + visited); + if (annotationOnInterface.isPresent()) { + return annotationOnInterface; + } + } + } + + // Indirectly present? + // Search in class hierarchy + if (inherited) { + Class superclass = clazz.getSuperclass(); + if (superclass != null && superclass != Object.class) { + Optional> annotationOnSuperclass = findAnnotation(superclass, annotationType, + inherited, visited); + if (annotationOnSuperclass.isPresent()) { + return annotationOnSuperclass; + } + } + } + } + + // Meta-present on indirectly present annotations? + return findMetaAnnotation(annotationType, element.getAnnotations(), inherited, visited); + } + + private static Optional> findMetaAnnotation(Class annotationType, + Annotation[] candidates, boolean inherited, Set visited) { + + for (Annotation candidateAnnotation : candidates) { + Class candidateAnnotationType = candidateAnnotation.annotationType(); + if (!isInJavaLangAnnotationPackage(candidateAnnotationType) && visited.add(candidateAnnotation)) { + Optional> metaAnnotation = findAnnotation(candidateAnnotationType, annotationType, + inherited, + visited); + if (metaAnnotation.isPresent()) { + return metaAnnotation; + } + } + } + return Optional.empty(); + } + + private static boolean isInJavaLangAnnotationPackage(Class annotationType) { + return (annotationType != null && annotationType.getName().startsWith("java.lang.annotation")); + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java index 629dd7c3df92f..5c73ff7f00568 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java @@ -3,6 +3,7 @@ import static io.quarkus.test.common.PathTestHelper.getAppClassLocationForTestLocation; import static io.quarkus.test.common.PathTestHelper.getTestClassesLocation; +import java.io.IOException; import java.lang.annotation.Annotation; import java.nio.file.Files; import java.nio.file.Path; @@ -25,10 +26,12 @@ import io.quarkus.bootstrap.app.CuratedApplication; import io.quarkus.bootstrap.app.QuarkusBootstrap; import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.resolver.AppModelResolverException; import io.quarkus.bootstrap.runner.Timing; import io.quarkus.bootstrap.utils.BuildToolHelper; import io.quarkus.bootstrap.workspace.ArtifactSources; import io.quarkus.bootstrap.workspace.SourceDir; +import io.quarkus.bootstrap.workspace.WorkspaceModule; import io.quarkus.deployment.dev.testing.CurrentTestApplication; import io.quarkus.paths.PathList; import io.quarkus.runtime.configuration.ProfileManager; @@ -53,25 +56,86 @@ public class AbstractJvmQuarkusTestExtension extends AbstractQuarkusTestWithCont protected PrepareResult createAugmentor(ExtensionContext context, Class profile, Collection shutdownTasks) throws Exception { - Class requiredTestClass = context.getRequiredTestClass(); - currentJUnitTestClass = requiredTestClass; - Path testClassLocation = getTestClassesLocation(requiredTestClass); - final Path appClassLocation = getAppClassLocationForTestLocation(testClassLocation.toString()); - final PathList.Builder rootBuilder = PathList.builder(); - Consumer addToBuilderIfConditionMet = path -> { if (path != null && Files.exists(path) && !rootBuilder.contains(path)) { rootBuilder.add(path); } }; - if (!appClassLocation.equals(testClassLocation)) { - addToBuilderIfConditionMet.accept(testClassLocation); - // if test classes is a dir, we should also check whether test resources dir exists as a separate dir (gradle) - // TODO: this whole app/test path resolution logic is pretty dumb, it needs be re-worked using proper workspace discovery - final Path testResourcesLocation = PathTestHelper.getResourcesForClassesDirOrNull(testClassLocation, "test"); - addToBuilderIfConditionMet.accept(testResourcesLocation); + final Class requiredTestClass = context.getRequiredTestClass(); + currentJUnitTestClass = requiredTestClass; + + final Path testClassLocation; + final Path appClassLocation; + final Path projectRoot = Paths.get("").normalize().toAbsolutePath(); + + final ApplicationModel gradleAppModel = getGradleAppModelForIDE(projectRoot); + // If gradle project running directly with IDE + if (gradleAppModel != null && gradleAppModel.getApplicationModule() != null) { + final WorkspaceModule module = gradleAppModel.getApplicationModule(); + final String testClassFileName = requiredTestClass.getName().replace('.', '/') + ".class"; + Path testClassesDir = null; + for (String classifier : module.getSourceClassifiers()) { + final ArtifactSources sources = module.getSources(classifier); + if (sources.isOutputAvailable() && sources.getOutputTree().contains(testClassFileName)) { + for (SourceDir src : sources.getSourceDirs()) { + addToBuilderIfConditionMet.accept(src.getOutputDir()); + if (Files.exists(src.getOutputDir().resolve(testClassFileName))) { + testClassesDir = src.getOutputDir(); + } + } + for (SourceDir src : sources.getResourceDirs()) { + addToBuilderIfConditionMet.accept(src.getOutputDir()); + } + for (SourceDir src : module.getMainSources().getSourceDirs()) { + addToBuilderIfConditionMet.accept(src.getOutputDir()); + } + for (SourceDir src : module.getMainSources().getResourceDirs()) { + addToBuilderIfConditionMet.accept(src.getOutputDir()); + } + break; + } + } + if (testClassesDir == null) { + final StringBuilder sb = new StringBuilder(); + sb.append("Failed to locate ").append(requiredTestClass.getName()).append(" in "); + for (String classifier : module.getSourceClassifiers()) { + final ArtifactSources sources = module.getSources(classifier); + if (sources.isOutputAvailable()) { + for (SourceDir d : sources.getSourceDirs()) { + if (Files.exists(d.getOutputDir())) { + sb.append(System.lineSeparator()).append(d.getOutputDir()); + } + } + } + } + throw new RuntimeException(sb.toString()); + } + testClassLocation = testClassesDir; + + } else { + if (System.getProperty(BootstrapConstants.OUTPUT_SOURCES_DIR) != null) { + final String[] sourceDirectories = System.getProperty(BootstrapConstants.OUTPUT_SOURCES_DIR).split(","); + for (String sourceDirectory : sourceDirectories) { + final Path directory = Paths.get(sourceDirectory); + addToBuilderIfConditionMet.accept(directory); + } + } + + testClassLocation = getTestClassesLocation(requiredTestClass); + appClassLocation = getAppClassLocationForTestLocation(testClassLocation.toString()); + if (!appClassLocation.equals(testClassLocation)) { + addToBuilderIfConditionMet.accept(testClassLocation); + // if test classes is a dir, we should also check whether test resources dir exists as a separate dir (gradle) + // TODO: this whole app/test path resolution logic is pretty dumb, it needs be re-worked using proper workspace discovery + final Path testResourcesLocation = PathTestHelper.getResourcesForClassesDirOrNull(testClassLocation, "test"); + addToBuilderIfConditionMet.accept(testResourcesLocation); + } + + addToBuilderIfConditionMet.accept(appClassLocation); + final Path appResourcesLocation = PathTestHelper.getResourcesForClassesDirOrNull(appClassLocation, "main"); + addToBuilderIfConditionMet.accept(appResourcesLocation); } originalCl = Thread.currentThread().getContextClassLoader(); @@ -105,52 +169,18 @@ protected PrepareResult createAugmentor(ExtensionContext context, Class getQuarkusTestProfile(ExtensionContext extensionContext) { @@ -222,12 +258,14 @@ protected static class PrepareResult { protected final AugmentAction augmentAction; protected final QuarkusTestProfile profileInstance; protected final CuratedApplication curatedApplication; + protected final Path testClassLocation; public PrepareResult(AugmentAction augmentAction, QuarkusTestProfile profileInstance, - CuratedApplication curatedApplication) { + CuratedApplication curatedApplication, Path testClassLocation) { this.augmentAction = augmentAction; this.profileInstance = profileInstance; this.curatedApplication = curatedApplication; + this.testClassLocation = testClassLocation; } } } diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTest.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTest.java index c63013d4a910f..bb0cf06ecffcd 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTest.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTest.java @@ -30,7 +30,7 @@ * {@link @QuarkusTest} so the test class structure must take this into account. */ @Target(ElementType.TYPE) -@ExtendWith({ DisabledOnIntegrationTestCondition.class, QuarkusTestExtension.class, QuarkusIntegrationTestExtension.class }) +@ExtendWith({ DisabledOnIntegrationTestCondition.class, QuarkusIntegrationTestExtension.class }) @Retention(RetentionPolicy.RUNTIME) public @interface QuarkusIntegrationTest { diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index 4c49143c21fcf..fd9153d2010fc 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -225,12 +225,12 @@ public Thread newThread(Runnable r) { //must be done after the TCCL has been set testResourceManager = (Closeable) startupAction.getClassLoader().loadClass(TestResourceManager.class.getName()) - .getConstructor(Class.class, Class.class, List.class, boolean.class, Map.class, Optional.class) + .getConstructor(Class.class, Class.class, List.class, boolean.class, Map.class, Optional.class, Path.class) .newInstance(requiredTestClass, profile != null ? profile : null, getAdditionalTestResources(profileInstance, startupAction.getClassLoader()), profileInstance != null && profileInstance.disableGlobalTestResources(), - startupAction.getDevServicesProperties(), Optional.empty()); + startupAction.getDevServicesProperties(), Optional.empty(), result.testClassLocation); testResourceManager.getClass().getMethod("init", String.class).invoke(testResourceManager, profile != null ? profile.getName() : null); Map properties = (Map) testResourceManager.getClass().getMethod("start") @@ -1251,7 +1251,7 @@ public static class TestBuildChainFunction implements Function> apply(Map stringObjectMap) { Path testLocation = (Path) stringObjectMap.get(TEST_LOCATION); // the index was written by the extension - Index testClassesIndex = TestClassIndexer.readIndex((Class) stringObjectMap.get(TEST_CLASS)); + Index testClassesIndex = TestClassIndexer.readIndex(testLocation, (Class) stringObjectMap.get(TEST_CLASS)); List> allCustomizers = new ArrayList<>(1); Consumer defaultCustomizer = new Consumer() { diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/ConfigUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/ConfigUtil.java index 248ff31f1bb31..2e77ecb7c9ea4 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/ConfigUtil.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/ConfigUtil.java @@ -38,4 +38,10 @@ public static Duration waitTimeValue(Config config) { .orElseGet(() -> config.getOptionalValue("quarkus.test.jar-wait-time", Duration.class) // legacy value .orElseGet(() -> Duration.ofSeconds(DEFAULT_WAIT_TIME_SECONDS))); } + + public static String integrationTestProfile(Config config) { + return config.getOptionalValue("quarkus.test.integration-test-profile", String.class) + .orElseGet(() -> config.getOptionalValue("quarkus.test.native-image-profile", String.class) + .orElse(null)); + } } diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/DockerContainerLauncherProvider.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/DockerContainerLauncherProvider.java index 039cf407c76d4..cf898e3fa53a8 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/DockerContainerLauncherProvider.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/DockerContainerLauncherProvider.java @@ -45,7 +45,7 @@ public DockerContainerArtifactLauncher create(CreateContext context) { config.getValue("quarkus.http.test-port", OptionalInt.class).orElse(DEFAULT_PORT), config.getValue("quarkus.http.test-ssl-port", OptionalInt.class).orElse(DEFAULT_HTTPS_PORT), ConfigUtil.waitTimeValue(config), - config.getOptionalValue("quarkus.test.native-image-profile", String.class).orElse(null), + ConfigUtil.integrationTestProfile(config), ConfigUtil.argLineValue(config), context.devServicesLaunchResult(), containerImage, diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/JarLauncherProvider.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/JarLauncherProvider.java index 2b90dc993e063..ef8cde8ffcd69 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/JarLauncherProvider.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/JarLauncherProvider.java @@ -43,7 +43,7 @@ public JarArtifactLauncher create(CreateContext context) { config.getValue("quarkus.http.test-port", OptionalInt.class).orElse(DEFAULT_PORT), config.getValue("quarkus.http.test-ssl-port", OptionalInt.class).orElse(DEFAULT_HTTPS_PORT), ConfigUtil.waitTimeValue(config), - config.getOptionalValue("quarkus.test.native-image-profile", String.class).orElse(null), + ConfigUtil.integrationTestProfile(config), ConfigUtil.argLineValue(config), context.devServicesLaunchResult(), context.buildOutputDirectory().resolve(pathStr))); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/NativeImageLauncherProvider.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/NativeImageLauncherProvider.java index ec9d9f56039e8..7b994941c9a56 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/NativeImageLauncherProvider.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/NativeImageLauncherProvider.java @@ -41,7 +41,7 @@ public NativeImageLauncher create(CreateContext context) { config.getValue("quarkus.http.test-port", OptionalInt.class).orElse(DEFAULT_PORT), config.getValue("quarkus.http.test-ssl-port", OptionalInt.class).orElse(DEFAULT_HTTPS_PORT), ConfigUtil.waitTimeValue(config), - config.getOptionalValue("quarkus.test.native-image-profile", String.class).orElse(null), + ConfigUtil.integrationTestProfile(config), ConfigUtil.argLineValue(config), context.devServicesLaunchResult(), System.getProperty("native.image.path"), diff --git a/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/client/KeycloakTestClient.java b/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/client/KeycloakTestClient.java index b052998ecb909..83ec3a7109367 100644 --- a/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/client/KeycloakTestClient.java +++ b/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/client/KeycloakTestClient.java @@ -3,6 +3,9 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; import org.eclipse.microprofile.config.ConfigProvider; import org.keycloak.representations.AccessTokenResponse; @@ -31,9 +34,84 @@ public KeycloakTestClient() { } + /** + * Get an access token from the default tenant realm using a client_credentials grant. + * Realm name is set to `quarkus` unless it has been configured with the `quarkus.keycloak.devservices.realm-name` property. + * Client id is set to `quarkus-app` unless it has been configured with the `quarkus.oidc.client-id` property. + * Client secret is set to `secret` unless it has been configured with the `quarkus.oidc.credentials.secret` property. + */ + public String getClientAccessToken() { + return getClientAccessToken(getClientId()); + } + + /** + * Get an access token from the default tenant realm using a client_credentials grant with a + * the provided client id. + * Realm name is set to `quarkus` unless it has been configured with the `quarkus.keycloak.devservices.realm-name` property. + * Client secret will be to `secret` unless it has been configured with the `quarkus.oidc.credentials.secret` property. + */ + public String getClientAccessToken(String clientId) { + return getClientAccessToken(clientId, getClientSecret()); + } + + /** + * Get an access token from the default tenant realm using a client_credentials grant with a + * the provided client id and secret. + * Realm name is set to `quarkus` unless it has been configured with the `quarkus.keycloak.devservices.realm-name` property. + */ + public String getClientAccessToken(String clientId, String clientSecret) { + return getClientAccessToken(clientId, clientSecret, null); + } + + /** + * Get an access token from the default tenant realm using a client_credentials grant with a + * the provided client id and secret, and scopes. + * Realm name is set to `quarkus` unless it has been configured with the `quarkus.keycloak.devservices.realm-name` property. + */ + public String getClientAccessToken(String clientId, String clientSecret, List scopes) { + return getClientAccessTokenInternal(clientId, clientSecret, scopes, getAuthServerUrl()); + } + + /** + * Get an access token from the provided realm using a client_credentials grant. + * Client id is set to `quarkus-app` unless it has been configured with the `quarkus.oidc.client-id` property. + * Client secret is set to `secret` unless it has been configured with the `quarkus.oidc.credentials.secret` property. + */ + public String getRealmClientAccessToken(String realm) { + return getRealmClientAccessToken(realm, getClientId()); + } + + /** + * Get an access token from the provided realm using a client_credentials grant with a + * the provided client id. + * Client secret will be to `secret` unless it has been configured with the `quarkus.oidc.credentials.secret` property. + */ + public String getRealmClientAccessToken(String realm, String clientId) { + return getRealmClientAccessToken(realm, clientId, getClientSecret()); + } + + /** + * Get an access token from the provided realm using a client_credentials grant with a + * the provided client id and secret. + */ + public String getRealmClientAccessToken(String realm, String clientId, String clientSecret) { + return getRealmClientAccessToken(realm, clientId, clientSecret, null); + } + + /** + * Get an access token from the provided realm using a client_credentials grant with a + * the provided client id and secret, and scopes. + */ + public String getRealmClientAccessToken(String realm, String clientId, String clientSecret, List scopes) { + return getClientAccessTokenInternal(clientId, clientSecret, scopes, getAuthServerBaseUrl() + "/realms/" + realm); + } + /** * Get an access token from the default tenant realm using a password grant with a provided user name. - * User secret will be the same as the user name, client id will be set to 'quarkus-app' and client secret to 'secret'. + * Realm name is set to `quarkus` unless it has been configured with the `quarkus.keycloak.devservices.realm-name` property. + * User secret will be the same as the user name. + * Client id will be set to `quarkus-app` unless it has been configured with the `quarkus.oidc.client-id` property. + * Client secret will be to `secret` unless it has been configured with the `quarkus.oidc.credentials.secret` property. */ public String getAccessToken(String userName) { return getAccessToken(userName, getClientId()); @@ -41,7 +119,9 @@ public String getAccessToken(String userName) { /** * Get an access token from the default tenant realm using a password grant with the provided user name and client id. - * User secret will be the same as the user name, client secret will be set to 'secret'. + * Realm name is set to `quarkus` unless it has been configured with the `quarkus.keycloak.devservices.realm-name` property. + * User secret will be the same as the user name. + * Client secret will be to `secret` unless it has been configured with the `quarkus.oidc.credentials.secret` property. */ public String getAccessToken(String userName, String clientId) { return getAccessToken(userName, userName, clientId); @@ -50,7 +130,8 @@ public String getAccessToken(String userName, String clientId) { /** * Get an access token from the default tenant realm using a password grant with the provided user name, user secret and * client id. - * Client secret will be set to 'secret'. + * Realm name is set to `quarkus` unless it has been configured with the `quarkus.keycloak.devservices.realm-name` property. + * Client secret will be set to `secret` unless it has been configured with the `quarkus.oidc.credentials.secret` propertys. */ public String getAccessToken(String userName, String userSecret, String clientId) { return getAccessToken(userName, userSecret, clientId, getClientSecret()); @@ -59,15 +140,26 @@ public String getAccessToken(String userName, String userSecret, String clientId /** * Get an access token from the default tenant realm using a password grant with the provided user name, user secret, client * id and secret. - * Set the client secret to an empty string or null if it is not required. + * Realm name is set to `quarkus` unless it has been configured with the `quarkus.keycloak.devservices.realm-name` property. */ public String getAccessToken(String userName, String userSecret, String clientId, String clientSecret) { - return getAccessTokenInternal(userName, userSecret, clientId, clientSecret, getAuthServerUrl()); + return getAccessToken(userName, userSecret, clientId, clientSecret, null); + } + + /** + * Get an access token from the default tenant realm using a password grant with the provided user name, user secret, client + * id and secret, and scopes. + */ + public String getAccessToken(String userName, String userSecret, String clientId, String clientSecret, + List scopes) { + return getAccessTokenInternal(userName, userSecret, clientId, clientSecret, scopes, getAuthServerUrl()); } /** * Get a realm access token using a password grant with a provided user name. - * User secret will be the same as the user name, client id will be set to 'quarkus-app' and client secret to 'secret'. + * User secret will be the same as the user name. + * Client id will be set to `quarkus-app` unless it has been configured with the `quarkus.oidc.client-id` property. + * Client secret will be to `secret` unless it has been configured with the `quarkus.oidc.credentials.secret` property. */ public String getRealmAccessToken(String realm, String userName) { return getRealmAccessToken(realm, userName, getClientId()); @@ -75,7 +167,8 @@ public String getRealmAccessToken(String realm, String userName) { /** * Get a realm access token using a password grant with the provided user name and client id. - * User secret will be the same as the user name, client secret will be set to 'secret'. + * User secret will be the same as the user name. + * Client secret will be to `secret` unless it has been configured with the `quarkus.oidc.credentials.secret` property. */ public String getRealmAccessToken(String realm, String userName, String clientId) { return getRealmAccessToken(realm, userName, userName, clientId); @@ -83,7 +176,7 @@ public String getRealmAccessToken(String realm, String userName, String clientId /** * Get a realm access token using a password grant with the provided user name, user secret and client id. - * Client secret will be set to 'secret'. + * Client secret will be to `secret` unless it has been configured with the `quarkus.oidc.credentials.secret` property. */ public String getRealmAccessToken(String realm, String userName, String userSecret, String clientId) { return getRealmAccessToken(realm, userName, userSecret, clientId, getClientSecret()); @@ -94,12 +187,22 @@ public String getRealmAccessToken(String realm, String userName, String userSecr * Set the client secret to an empty string or null if it is not required. */ public String getRealmAccessToken(String realm, String userName, String userSecret, String clientId, String clientSecret) { - return getAccessTokenInternal(userName, userSecret, clientId, clientSecret, + return getRealmAccessToken(realm, userName, userSecret, clientId, clientSecret, null); + } + + /** + * Get a realm access token using a password grant with the provided user name, user secret, client id and secret, and + * scopes. + * Set the client secret to an empty string or null if it is not required. + */ + public String getRealmAccessToken(String realm, String userName, String userSecret, String clientId, String clientSecret, + List scopes) { + return getAccessTokenInternal(userName, userSecret, clientId, clientSecret, scopes, getAuthServerBaseUrl() + "/realms/" + realm); } private String getAccessTokenInternal(String userName, String userSecret, String clientId, String clientSecret, - String authServerUrl) { + List scopes, String authServerUrl) { RequestSpecification requestSpec = RestAssured.given().param("grant_type", "password") .param("username", userName) .param("password", userSecret) @@ -107,6 +210,23 @@ private String getAccessTokenInternal(String userName, String userSecret, String if (clientSecret != null && !clientSecret.isBlank()) { requestSpec = requestSpec.param("client_secret", clientSecret); } + if (scopes != null && !scopes.isEmpty()) { + requestSpec = requestSpec.param("scope", urlEncode(String.join(" ", scopes))); + } + return requestSpec.when().post(authServerUrl + "/protocol/openid-connect/token") + .as(AccessTokenResponse.class).getToken(); + } + + private String getClientAccessTokenInternal(String clientId, String clientSecret, + List scopes, String authServerUrl) { + RequestSpecification requestSpec = RestAssured.given().param("grant_type", "client_credentials") + .param("client_id", clientId); + if (clientSecret != null && !clientSecret.isBlank()) { + requestSpec = requestSpec.param("client_secret", clientSecret); + } + if (scopes != null && !scopes.isEmpty()) { + requestSpec = requestSpec.param("scope", urlEncode(String.join(" ", scopes))); + } return requestSpec.when().post(authServerUrl + "/protocol/openid-connect/token") .as(AccessTokenResponse.class).getToken(); } @@ -123,7 +243,7 @@ private String getClientSecret() { * Get an admin access token which can be used to create Keycloak realms and perform other Keycloak administration tasks. */ public String getAdminAccessToken() { - return getAccessTokenInternal("admin", "admin", "admin-cli", null, getAuthServerBaseUrl() + "/realms/master"); + return getAccessTokenInternal("admin", "admin", "admin-cli", null, null, getAuthServerBaseUrl() + "/realms/master"); } /** @@ -189,6 +309,13 @@ public void deleteRealm(String realm) { .delete(getAuthServerBaseUrl() + "/admin/realms/" + realm).then().statusCode(204); } + /** + * Delete a realm + */ + public void deleteRealm(RealmRepresentation realm) { + deleteRealm(realm.getRealm()); + } + private String getPropertyValue(String prop, String defaultValue) { return ConfigProvider.getConfig().getOptionalValue(prop, String.class) .orElseGet(() -> getDevProperty(prop, defaultValue)); @@ -203,4 +330,12 @@ private String getDevProperty(String prop, String defaultValue) { public void setIntegrationTestContext(DevServicesContext context) { this.testContext = context; } + + private static String urlEncode(String value) { + try { + return URLEncoder.encode(value, StandardCharsets.UTF_8.name()); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } } diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java b/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java index 676fde82020f5..a2f773fb6de88 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java @@ -4,6 +4,7 @@ import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashSet; +import java.util.Optional; import java.util.stream.Collectors; import javax.enterprise.inject.Instance; @@ -15,6 +16,8 @@ import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback; import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback; import io.quarkus.test.junit.callback.QuarkusTestMethodContext; +import io.quarkus.test.util.annotations.AnnotationContainer; +import io.quarkus.test.util.annotations.AnnotationUtils; public class QuarkusSecurityTestExtension implements QuarkusTestBeforeEachCallback, QuarkusTestAfterEachCallback { @@ -41,26 +44,18 @@ public void beforeEach(QuarkusTestMethodContext context) { throw new RuntimeException(e); } }).toArray(Class[]::new)); - Annotation[] allAnnotations = new Annotation[] {}; - TestSecurity testSecurity = method.getAnnotation(TestSecurity.class); - if (testSecurity == null) { - testSecurity = original.getAnnotation(TestSecurity.class); - if (testSecurity != null) { - allAnnotations = original.getAnnotations(); - } - while (testSecurity == null && original != Object.class) { - original = original.getSuperclass(); - testSecurity = original.getAnnotation(TestSecurity.class); - if (testSecurity != null) { - allAnnotations = original.getAnnotations(); - } - } - } else { - allAnnotations = method.getAnnotations(); + Annotation[] allAnnotations; + Optional> annotationContainerOptional = AnnotationUtils.findAnnotation(method, + TestSecurity.class); + if (annotationContainerOptional.isEmpty()) { + annotationContainerOptional = AnnotationUtils.findAnnotation(original, TestSecurity.class); } - if (testSecurity == null) { + if (annotationContainerOptional.isEmpty()) { return; } + var annotationContainer = annotationContainerOptional.get(); + allAnnotations = annotationContainer.getElement().getAnnotations(); + TestSecurity testSecurity = annotationContainer.getAnnotation(); CDI.current().select(TestAuthController.class).get().setEnabled(testSecurity.authorizationEnabled()); if (testSecurity.user().isEmpty()) { if (testSecurity.roles().length != 0) { diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java b/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java index 22a009171315a..338fdc0cde875 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java @@ -56,10 +56,11 @@ public void setIdentity(Uni identity) { @Override public Uni getDeferredIdentity() { - if (testIdentity != null) { - return Uni.createFrom().item(testIdentity); + if (testIdentity == null) { + return delegate.getDeferredIdentity(); } - return delegate.getDeferredIdentity(); + return delegate.getDeferredIdentity().onItem() + .transform(underlying -> underlying.isAnonymous() ? testIdentity : underlying); } @Override diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java b/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java index 10364a4bfab61..102b51cf4fdbf 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java @@ -1,12 +1,14 @@ package io.quarkus.test.security; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) +@Inherited public @interface TestSecurity { /** diff --git a/test-framework/security/src/test/java/io/quarkus/test/security/TestIdentityAssociationTest.java b/test-framework/security/src/test/java/io/quarkus/test/security/TestIdentityAssociationTest.java new file mode 100644 index 0000000000000..b1eeb6eab711d --- /dev/null +++ b/test-framework/security/src/test/java/io/quarkus/test/security/TestIdentityAssociationTest.java @@ -0,0 +1,90 @@ +package io.quarkus.test.security; + +import static io.quarkus.security.runtime.QuarkusSecurityIdentity.builder; +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.runtime.BlockingOperationControl; +import io.quarkus.runtime.IOThreadDetector; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.QuarkusPrincipal; +import io.smallrye.mutiny.Uni; + +public class TestIdentityAssociationTest { + + TestIdentityAssociation sut; + + @BeforeEach + void init() { + sut = new TestIdentityAssociation(); + sut.delegate = new DelegateSecurityIdentityAssociation(); + + BlockingOperationControl.setIoThreadDetector(new IOThreadDetector[0]); + } + + @Test + void useDelegateIfTestIdentityIsNull() { + // create anonymous identity + SecurityIdentity mockedIdentity = builder().setAnonymous(true).build(); + Uni mockedIdentityUni = Uni.createFrom().item(mockedIdentity); + sut.setIdentity(mockedIdentity); + sut.setIdentity(mockedIdentityUni); + + // reset testIdentity + sut.setTestIdentity(null); + + // get identity direct + deferred + SecurityIdentity deferred = sut.getDeferredIdentity().await().indefinitely(); + SecurityIdentity identity = sut.getIdentity(); + + // must be the same instance + assertSame(identity, deferred, "Must be same instance directly and deferred"); + assertSame(mockedIdentity, identity, "Expected delegate. (TestIdentity is null)"); + } + + @Test + void useTestIdentityIfDelegateIsAnonymous() { + // create anonymous identity + SecurityIdentity mockedIdentity = builder().setAnonymous(true).build(); + Uni mockedIdentityUni = Uni.createFrom().item(mockedIdentity); + // create test identity + SecurityIdentity mockedTestIdentity = builder().setPrincipal(new QuarkusPrincipal("test-identity")).build(); + sut.setIdentity(mockedIdentity); + sut.setIdentity(mockedIdentityUni); + + // reset testIdentity + sut.setTestIdentity(mockedTestIdentity); + + // get identity direct + deferred + SecurityIdentity deferred = sut.getDeferredIdentity().await().indefinitely(); + SecurityIdentity identity = sut.getIdentity(); + + // must be the same instance + assertSame(identity, deferred, "Must be same instance directly and deferred"); + assertSame(mockedTestIdentity, identity, "Expected testIdentity. (Delegate is anonymous)"); + } + + @Test + void useDelegateIfNotAnonymous() { + // create identity with principal + SecurityIdentity mockedIdentity = builder().setPrincipal(new QuarkusPrincipal("delegate")).build(); + Uni mockedIdentityUni = Uni.createFrom().item(mockedIdentity); + // create test identity + SecurityIdentity mockedTestIdentity = builder().setPrincipal(new QuarkusPrincipal("test-identity")).build(); + sut.setIdentity(mockedIdentity); + sut.setIdentity(mockedIdentityUni); + + // reset testIdentity + sut.setTestIdentity(mockedTestIdentity); + + // get identity direct + deferred + SecurityIdentity deferred = sut.getDeferredIdentity().await().indefinitely(); + SecurityIdentity identity = sut.getIdentity(); + + // must be the same instance + assertSame(identity, deferred, "Must be same instance directly and deferred"); + assertSame(mockedIdentity, identity, "Expected delegate. (Delegate is not anonymous)"); + } +}