diff --git a/.github/native-tests.json b/.github/native-tests.json index e1272265317d6..39a1833d0aef4 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -81,7 +81,7 @@ { "category": "Security3", "timeout": 50, - "test-modules": "keycloak-authorization, smallrye-jwt-token-propagation", + "test-modules": "keycloak-authorization, smallrye-jwt-token-propagation, security-webauthn", "os-name": "ubuntu-latest" }, { diff --git a/.github/quarkus-github-bot.yml b/.github/quarkus-github-bot.yml index 50bf57f2a5bb5..510115ef78a6e 100644 --- a/.github/quarkus-github-bot.yml +++ b/.github/quarkus-github-bot.yml @@ -225,6 +225,9 @@ triage: - integration-tests/spring- - labels: [env/windows] titleBody: "windows" + - labels: [env/m1] + titleBody: "\\bm1\\b" + notify: [gastaldi] - labels: [area/kubernetes] titleBody: "kubernetes" notify: [geoand, iocanel] diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index 2f8a88f3d2350..86c8f34512178 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -4,7 +4,6 @@ on: push: branches-ignore: - 'dependabot/**' - - 'jakarta-rewrite' # paths-ignore in ci-fork-mvn-cache.yml should match paths-ignore: - '.gitignore' @@ -70,6 +69,21 @@ jobs: name: pull-request-number-${{ github.event.number }} path: pull-request-number retention-days: 1 + attach-report-issue-number: + runs-on: ubuntu-latest + name: Attach report issue number + if: github.ref_name == 'jakarta-rewrite' + steps: + - name: Create file + shell: bash + run: | + echo -n 25363 > report-issue-number + - name: Upload report issue number + uses: actions/upload-artifact@v2 + with: + name: report-issue-number-25363 + path: report-issue-number + retention-days: 1 ci-sanity-check: name: "CI Sanity Check" runs-on: ubuntu-latest @@ -130,6 +144,10 @@ jobs: path: ~/.m2/repository # refresh cache every month to avoid unlimited growth key: q2maven-${{ steps.get-date.outputs.date }} + - name: Prepare Jakarta artifacts + if: github.ref_name == 'jakarta-rewrite' + shell: bash + run: ./jakarta/prepare.sh - name: Build run: | ./mvnw -T1C $COMMON_MAVEN_ARGS -DskipTests -DskipITs -Dinvoker.skip -Dno-format -Dtcks clean install @@ -164,7 +182,19 @@ jobs: # mvnw just for creating gib-impacted.log ("validate" should not waste much time if not incremental at all, e.g. on main) run: | ./mvnw -q -T1C $COMMON_MAVEN_ARGS -Dtcks -Dquickly-ci ${{ steps.get-gib-args.outputs.gib_args }} -Dgib.logImpactedTo=gib-impacted.log validate - [ -f gib-impacted.log ] && GIB_IMPACTED=$(cat gib-impacted.log) || GIB_IMPACTED='' + if [ -f gib-impacted.log ] + then + # TODO: for now, we don't run TCKs and for the jakarta-rewrite branch + # we filter them here so that it cascades to all the builds + if [ "${GITHUB_REF_NAME}" == "jakarta-rewrite" ] + then + GIB_IMPACTED=$(cat gib-impacted.log | grep -Pv '^(tcks/|integration-tests/grpc-|integration-tests/infinispan-client|integration-tests/kafka-avro|integration-tests/opentelemetry-grpc)') + else + GIB_IMPACTED=$(cat gib-impacted.log) + fi + else + GIB_IMPACTED='' + fi echo "GIB_IMPACTED: ${GIB_IMPACTED}" echo "::set-output name=impacted_modules::${GIB_IMPACTED//$'\n'/'%0A'}" - name: Tar Maven Repo diff --git a/.github/workflows/jakarta-rewrite.yml b/.github/workflows/jakarta-rewrite.yml index dd00a0d9c01df..1ed7e6bd1e668 100644 --- a/.github/workflows/jakarta-rewrite.yml +++ b/.github/workflows/jakarta-rewrite.yml @@ -30,7 +30,7 @@ jobs: run: | export PATH="$HOME/.jbang/bin:$PATH" git checkout -b temp-jakarta-rewrite - ./jakarta/transform.sh + REWRITE_TESTS_CONTAINERS=true ./jakarta/transform.sh git add . git commit -m 'Transform sources to Jakarta' shell: bash diff --git a/.gitignore b/.gitignore index 2ab436ff74c7f..bcf6f3fd6fe36 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ ObjectStore docker/distroless/bazel-* /.apt_generated_tests/ quarkus.log +quarkus.log* replay_*.logß nbactions.xml nb-configuration.xml diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000000000..3d06e70f531f3 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,17 @@ +# This configuration file was automatically generated by Gitpod. +# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file) +# and commit this file to your remote git repository to share the goodness with others. +image: + file: .gitpod/Dockerfile + +tasks: + - init: ./mvnw -Dquickly + - name: Quarkus command + openMode: split-right + +vscode: + extensions: + - "redhat.java" + - "vscjava.vscode-java-dependency" + - "vscjava.vscode-java-debug" + - "vscjava.vscode-java-pack" diff --git a/.gitpod/Dockerfile b/.gitpod/Dockerfile new file mode 100644 index 0000000000000..f15c4361b51e9 --- /dev/null +++ b/.gitpod/Dockerfile @@ -0,0 +1,8 @@ +FROM gitpod/workspace-java-11 + +RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh && \ + sdk install java 11.0.9.j9-adpt && \ + sdk use java 11.0.9.j9-adpt && \ + yes | sdk install quarkus && \ + rm -rf $HOME/.sdkman/archives/* && \ + rm -rf $HOME/.sdkman/tmp/* " diff --git a/.idea/icon.png b/.idea/icon.png new file mode 100644 index 0000000000000..8160d24433ba4 Binary files /dev/null and b/.idea/icon.png differ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbd4de060f869..cd99e254b7c13 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,7 @@ But first, read this page (including the small print at the end). * [`OutOfMemoryError` while importing](#-outofmemoryerror--while-importing) * [`package sun.misc does not exist` while building](#-package-sunmisc-does-not-exist--while-building) * [Formatting](#formatting) + + [Gitpod](#gitpod) * [Build](#build) + [Workflow tips](#workflow-tips) - [Building all modules of an extension](#building-all-modules-of-an-extension) @@ -288,6 +289,11 @@ navigate to _Editor_ -> _Code Style_ -> _Java_ -> _Imports_ and set _Class count to use import with '\*'_ to `999`. Do the same with _Names count to use static import with '\*'_. +### Gitpod + +You can also use [Gitpod](https://gitpod.io) to contribute without installing anything on your computer. Click [here](https://gitpod.io/#https://github.com/quarkusio/quarkus/-/tree/main/) to start a workspace. + + ## Build * Clone the repository: `git clone https://github.com/quarkusio/quarkus.git` @@ -307,10 +313,29 @@ export MAVEN_OPTS="-Xmx4g" This build skipped all the tests, native-image builds, documentation generation etc. and used the Maven goals `clean install` by default. For more details about `-Dquickly` have a look at the `quick-build` profile in `quarkus-parent` (root `pom.xml`). -Adding `-DskipTests=false -DskipITs=false` enables the tests. -It will take much longer to build but will give you more guarantees on your code. +When contributing to Quarkus, it is recommended to respect the following rules. + +**Contributing to an extension** + +When you contribute to an extension, after having applied your changes, run: + +* `./mvnw -Dquickly` from the root directory to make sure you haven't broken anything obvious +* `./mvnw -f extensions/ clean install` to run a full build of your extension including the tests +* `./mvnw -f integration-tests/ clean install` to make sure ITs are still passing +* `./mvnw -f integration-tests/ clean install -Dnative` to test the native build (for small changes, it might not be useful, use your own judgement) + +**Contributing to a core artifact** + +Obviously, when you contribute to a core artifact of Quarkus, a change may impact any part of Quarkus. +So the rule of thumb would be to run the full test suite locally but this is clearly impractical as it takes a lot of time/resources. + +Thus it is recommended to use the following approach: -You can build and test native images in the integration tests supporting it by using `./mvnw install -Dnative`. +* run `./mvnw -Dquickly` from the root directory to make sure you haven't broken anything obvious +* run any build that might be useful to test the behavior you changed actually fixes the issue you had (might be an extension build, an integration test build...) +* push your work to your own fork of Quarkus to trigger CI there +* you can create a draft pull request to keep track of your work +* wait until the build is green in your fork (use your own judgement if it's not fully green) before marking your pull request as ready for review (which will trigger Quarkus CI) ### Workflow tips @@ -433,10 +458,10 @@ The Asciidoc files can be found in the [`src/main/asciidoc` directory](https://g When contributing a significant documentation change, it is highly recommended to run the build and check the output. -First build the whole Quarkus repository with the documentation build enabled (`-Dquickly` skips the documentation build): +First build the whole Quarkus repository with the documentation build enabled: ``` -./mvnw -Dquickly -DskipDocs=false clean install +./mvnw -DquicklyDocs ``` This will generate the configuration properties documentation includes in the root `target/asciidoc/generated/config/` directory and will avoid a lot of warnings when building the documentation module. diff --git a/MAINTAINERS.adoc b/MAINTAINERS.adoc index a2af6b362cf52..0182cb1750b8e 100644 --- a/MAINTAINERS.adoc +++ b/MAINTAINERS.adoc @@ -257,7 +257,7 @@ If you think some information is outdated, either provide a pull request or send |https://github.com/FroMage[Stéphane Épardaud], https://github.com/manovotn[Matěj Novotný] |SmallRye Fault Tolerance -|https://github.com/Ladicek[Ladislav Thon], https://github.com/jmartisk[Jan Martiska], https://github.com/michalszynkiewicz[Michal Szynkiewicz] +|https://github.com/Ladicek[Ladislav Thon] |SmallRye Health |https://github.com/antoinesd[Antoine Sabot-Durand], https://github.com/jmartisk[Jan Martiska] @@ -278,7 +278,7 @@ If you think some information is outdated, either provide a pull request or send |https://github.com/jmartisk[Jan Martiska], https://github.com/phillip-kruger[Phillip Kruger] |SmallRye OpenTracing -|https://github.com/pavolloffay[Pavol Loffay], https://github.com/Ladicek[Ladislav Thon] +|https://github.com/radcortez[Roberto Cortez] |SmallRye Reactive Messaging |https://github.com/cescoffier[Clément Escoffier] @@ -287,10 +287,10 @@ If you think some information is outdated, either provide a pull request or send |https://github.com/cescoffier[Clément Escoffier] |SmallRye Reactive Messaging - Kafka -|https://github.com/cescoffier[Clément Escoffier] +|https://github.com/cescoffier[Clément Escoffier], https://github.com/ozangunalp[Ozan Gunalp] |SmallRye Reactive Messaging - MQTT -|https://github.com/cescoffier[Clément Escoffier], https://github.com/michalszynkiewicz[Michał Szynkiewicz] +|https://github.com/cescoffier[Clément Escoffier] |Spring Boot Properties |https://github.com/gytis[Gytis Trikleris] diff --git a/bom/application/pom.xml b/bom/application/pom.xml index d36177b5156dc..a13176ed1f827 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -24,11 +24,11 @@ 0.2.4 0.1.15 0.1.5 - 1.12.0 - 1.12.0-alpha + 1.13.0 + 1.13.0-alpha 1.8.0 - 4.1.8 - 1.8.4 + 4.1.9 + 1.9.0 0.22.0 2.0.1 3.0.1 @@ -43,15 +43,15 @@ 3.2.1 3.0.4 2.1.22 - 1.4.4 + 1.5.0 2.1.0 5.4.0 - 3.3.3 + 3.4.0 1.2.2 1.0.13 2.7.0 - 2.19.0 - 3.15.0 + 2.21.0 + 3.16.0 1.1.0 1.2.1 1.3.5 @@ -74,28 +74,29 @@ 2.0.0.Final 9.3 2.11.0 - 14.0.0.Final + 15.0.0.Final 3.0-alpha-2 3.3.0 2.1.0 - - 22.0.0.2 - 1.0.10.Final - 2.13.2.20220328 + 22.1.0 + + 22.0.0.2 + 1.0.11.Final + 2.13.3 1.0.0.Final 3.12.0 1.15 1.5.1 - 5.6.8.Final + 5.6.9.Final 1.12.9 - 1.1.4.Final + 1.1.5.Final 6.2.3.Final - 6.1.3.Final + 6.1.5.Final 5.12.6.Final 1.1.1.Final 1.16 7.6.0.Final - 8.1.2 + 8.2.0 7.10.2 2.2.21 @@ -107,7 +108,7 @@ 1.0.1.Final 1.19.0.Final 3.4.2.Final - 4.2.6 + 4.2.7 4.5.13 4.4.15 4.1.5 @@ -116,7 +117,7 @@ 2.1.210 42.3.3 3.0.4 - 8.0.28 + 8.0.29 7.2.2.jre8 1.6.7 21.5.0.0 @@ -127,56 +128,56 @@ 5.8.2 1.5.0 6.14.2 - 13.0.8.Final - 4.4.1.Final + 13.0.10.Final + 4.4.3.Final 2.9.3 4.1.74.Final 1.0.3 - 3.4.3.Final + 3.5.0.Final 1.4.0 3.1.0 1.8.0 1.1.8.4 0.100.0 - 2.12.13 + 2.13.8 1.2.1 3.11.0 2.11.1 1.4.2 - 1.6.20 + 1.6.21 1.6.1 - 1.3.2 - 2.9.1 + 1.3.3 + 5.12.2 + 2.9.2 3.1.0 4.2.0 - 1.0.9 - 8.5.7 + 1.0.10 + 8.5.11 1.0.11 - 4.9.1 + 4.10.0 1.30 6.0.0 4.3.4 - 1.2.1 + 1.4.0 0.33.10 3.14.9 - 0.1.0 - 5.12.2 + 0.1.1 3.3.0 5.2.SP6 2.1.SP2 5.3.Final 2.1.SP1 - 4.4.0 + 4.5.1 5.8.0 4.9.2 1.1.4.Final - 17.0.1 + 18.0.0 1.15.0 - 3.21.4 - 2.12.1 + 3.22.0 + 2.13.1 0.21.0 - 1.41.6 + 1.41.8 2.1 4.6.3 1.0.4 @@ -186,8 +187,8 @@ 2.17.2 1.3.0.Final 1.11.0 - 2.2.1.Final - 0.1.7.Final + 2.2.3.Final + 0.1.9.Final 0.8.8 1.17.1 3.2.13 @@ -195,8 +196,9 @@ 2.6 0.10.0 - 9.21 - 0.0.4 + 9.22 + 0.0.6 + 0.1.1 @@ -317,6 +319,17 @@ pom import + + org.testcontainers + testcontainers + ${testcontainers.version} + + + org.hamcrest + hamcrest-core + + + @@ -525,6 +538,16 @@ quarkus-jsonp-deployment ${project.version} + + io.quarkus + quarkus-hal + ${project.version} + + + io.quarkus + quarkus-hal-deployment + ${project.version} + io.quarkus quarkus-resteasy-reactive-kotlin-serialization @@ -1208,6 +1231,16 @@ quarkus-avro-deployment ${project.version} + + io.quarkus + quarkus-apicurio-registry-common + ${project.version} + + + io.quarkus + quarkus-apicurio-registry-common-deployment + ${project.version} + io.quarkus quarkus-apicurio-registry-avro @@ -1218,6 +1251,36 @@ quarkus-apicurio-registry-avro-deployment ${project.version} + + io.quarkus + quarkus-confluent-registry-common + ${project.version} + + + io.quarkus + quarkus-confluent-registry-common-deployment + ${project.version} + + + io.quarkus + quarkus-confluent-registry-avro + ${project.version} + + + io.quarkus + quarkus-confluent-registry-avro-deployment + ${project.version} + + + io.quarkus + quarkus-schema-registry-devservice + ${project.version} + + + io.quarkus + quarkus-schema-registry-devservice-deployment + ${project.version} + io.quarkus quarkus-smallrye-health @@ -3091,6 +3154,11 @@ agroal-pool ${agroal.version} + + io.apicurio + apicurio-registry-client + ${apicurio-registry.version} + io.apicurio apicurio-registry-serdes-avro-serde @@ -3532,7 +3600,7 @@ io.smallrye - smallrye-graphql-client-implementation + smallrye-graphql-client ${smallrye-graphql.version} @@ -3776,7 +3844,7 @@ org.apache.kafka - kafka_2.12 + kafka_2.13 ${kafka3.version} @@ -3996,7 +4064,7 @@ org.graalvm.nativeimage svm - ${graal-sdk.version} + ${graal-svm.version} provided @@ -5209,6 +5277,11 @@ keycloak-core ${keycloak.version} + + org.keycloak + keycloak-common + ${keycloak.version} + org.keycloak keycloak-admin-client @@ -5482,6 +5555,11 @@ quarkus-awt-deployment ${project.version} + + io.github.crac + org-crac + ${org-crac.version} + @@ -5779,6 +5857,9 @@ io.quarkus.jakarta-security io.quarkus.smallrye io.quarkus.bom.resteasy-microprofile + io.quarkus.bom.resteasy-spring-web + io.quarkus.jakarta-json + io.quarkus.keycloak-admin-client @@ -5806,6 +5887,7 @@ io.quarkus.jakarta-jaxrs-jaxb-cleanup io.quarkus.jakarta-security-cleanup + io.quarkus.jakarta-json-cleanup diff --git a/build-parent/pom.xml b/build-parent/pom.xml index b11af9d381566..5cfcb162945a8 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -20,9 +20,9 @@ 3.8.1 - 1.6.20 - 1.6.10 - 2.12.13 + 1.6.21 + 1.6.21 + 2.13.8 4.6.1 ${scala-maven-plugin.version} @@ -39,8 +39,8 @@ - 22.0.0.2 - 22.0 + 22.1.0 + 22.1 2.5.3 2.40.0 @@ -80,7 +80,7 @@ docker.elastic.co/elasticsearch/elasticsearch-oss:${elasticsearch-server.version} http 1.2.3 - opensearchproject/opensearch:${opensearch-server.version} + docker.io/opensearchproject/opensearch:${opensearch-server.version} http @@ -97,19 +97,19 @@ - 17.0.1 + 18.0.0 quay.io/keycloak/keycloak:${keycloak.version} quay.io/keycloak/keycloak:${keycloak.version}-legacy - 6.0.4 + 6.0.5 3.22.0 - 2.33.1 + 2.33.2 7.1.0 - 2.21.0 + 2.22.0 diff --git a/core/builder/src/main/java/io/quarkus/builder/BuildStepBuilder.java b/core/builder/src/main/java/io/quarkus/builder/BuildStepBuilder.java index 5ff8c1378757a..85190c18661b1 100644 --- a/core/builder/src/main/java/io/quarkus/builder/BuildStepBuilder.java +++ b/core/builder/src/main/java/io/quarkus/builder/BuildStepBuilder.java @@ -90,7 +90,7 @@ public BuildStepBuilder afterProduce(Class type) { public BuildStepBuilder produces(Class type) { Assert.checkNotNullParam("type", type); if (EmptyBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot produce an empty build item"); + throw new IllegalArgumentException("Cannot produce an empty build item, use @Produce(class) instead"); } addProduces(new ItemId(type), Constraint.REAL, ProduceFlags.NONE); return this; @@ -109,7 +109,7 @@ public BuildStepBuilder produces(Class type, ProduceFlag fl Assert.checkNotNullParam("type", type); Assert.checkNotNullParam("flag", flag); if (EmptyBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot produce an empty build item"); + throw new IllegalArgumentException("Cannot produce an empty build item, use @Produce(class) instead"); } addProduces(new ItemId(type), Constraint.REAL, ProduceFlags.of(flag)); return this; @@ -129,7 +129,7 @@ public BuildStepBuilder produces(Class type, ProduceFlag fl Assert.checkNotNullParam("type", type); Assert.checkNotNullParam("flag", flag1); if (EmptyBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot produce an empty build item"); + throw new IllegalArgumentException("Cannot produce an empty build item, use @Produce(class) instead"); } addProduces(new ItemId(type), Constraint.REAL, ProduceFlags.of(flag1).with(flag2)); return this; @@ -148,7 +148,7 @@ public BuildStepBuilder produces(Class type, ProduceFlags f Assert.checkNotNullParam("type", type); Assert.checkNotNullParam("flag", flags); if (EmptyBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot produce an empty build item"); + throw new IllegalArgumentException("Cannot produce an empty build item, use @Produce(class) instead"); } addProduces(new ItemId(type), Constraint.REAL, flags); return this; @@ -164,7 +164,7 @@ public BuildStepBuilder produces(Class type, ProduceFlags f public BuildStepBuilder consumes(Class type) { Assert.checkNotNullParam("type", type); if (EmptyBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot consume an empty build item"); + throw new IllegalArgumentException("Cannot consume an empty build item, use @Consume(class) instead"); } addConsumes(new ItemId(type), Constraint.REAL, ConsumeFlags.NONE); return this; @@ -181,7 +181,7 @@ public BuildStepBuilder consumes(Class type) { public BuildStepBuilder consumes(Class type, ConsumeFlags flags) { Assert.checkNotNullParam("type", type); if (EmptyBuildItem.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Cannot consume an empty build item"); + throw new IllegalArgumentException("Cannot consume an empty build item, use @Consume(class) instead"); } addConsumes(new ItemId(type), Constraint.REAL, flags); return this; diff --git a/core/builder/src/main/java/io/quarkus/builder/item/EmptyBuildItem.java b/core/builder/src/main/java/io/quarkus/builder/item/EmptyBuildItem.java index f33aa2c94d289..a2760581707e1 100644 --- a/core/builder/src/main/java/io/quarkus/builder/item/EmptyBuildItem.java +++ b/core/builder/src/main/java/io/quarkus/builder/item/EmptyBuildItem.java @@ -3,6 +3,9 @@ /** * An empty build item. Empty build items carry no data and may be used, for example, for ordering and for * running steps which don't otherwise produce anything. + * + * Empty build items cannot be instantiated, you must use @Produce(MyEmptyBuildItem.class) or + * @Consume(MyEmptyBuildItem.class) instead of the standard ways to consume or produce build items. */ public abstract class EmptyBuildItem extends BuildItem { protected EmptyBuildItem() { diff --git a/core/deployment/pom.xml b/core/deployment/pom.xml index 7588aa22db05a..fce7859f7272e 100644 --- a/core/deployment/pom.xml +++ b/core/deployment/pom.xml @@ -133,6 +133,15 @@ + + maven-surefire-plugin + + + + true + + + maven-enforcer-plugin @@ -179,4 +188,28 @@ + + + + test-native-container-build + + + start-containers + + + + + + maven-surefire-plugin + + + + false + + + + + + + diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Capability.java b/core/deployment/src/main/java/io/quarkus/deployment/Capability.java index 90602ffba6ad1..5af46ef4d0b2f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Capability.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Capability.java @@ -35,6 +35,8 @@ public interface Capability { String JSONB = QUARKUS_PREFIX + "jsonb"; + String HAL = QUARKUS_PREFIX + "hal"; + String REST = QUARKUS_PREFIX + "rest"; String REST_CLIENT = REST + ".client"; String REST_JACKSON = REST + ".jackson"; @@ -80,6 +82,7 @@ public interface Capability { String QUARTZ = QUARKUS_PREFIX + "quartz"; String KUBERNETES_SERVICE_BINDING = QUARKUS_PREFIX + "kubernetes.service.binding"; + String KUBERNETES_CLIENT = QUARKUS_PREFIX + "kubernetes.client"; /** * @deprecated @@ -121,4 +124,7 @@ public interface Capability { String APICURIO_REGISTRY = QUARKUS_PREFIX + "apicurio.registry"; String APICURIO_REGISTRY_AVRO = APICURIO_REGISTRY + ".avro"; + + String CONFLUENT_REGISTRY = QUARKUS_PREFIX + "confluent.registry"; + String CONFLUENT_REGISTRY_AVRO = CONFLUENT_REGISTRY + ".avro"; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/DockerStatusProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/DockerStatusProcessor.java new file mode 100644 index 0000000000000..03b5d8b30e213 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/DockerStatusProcessor.java @@ -0,0 +1,13 @@ +package io.quarkus.deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; + +public class DockerStatusProcessor { + + @BuildStep + DockerStatusBuildItem IsDockerWorking(LaunchModeBuildItem launchMode) { + return new DockerStatusBuildItem(new IsDockerWorking(launchMode.getLaunchMode().isDevOrTest())); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java b/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java index fb33b8f5689f2..a48946735e4ea 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java @@ -57,6 +57,7 @@ import io.quarkus.builder.ProduceFlag; import io.quarkus.builder.ProduceFlags; import io.quarkus.builder.item.BuildItem; +import io.quarkus.builder.item.EmptyBuildItem; import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.builder.item.SimpleBuildItem; import io.quarkus.deployment.annotations.BuildProducer; @@ -126,7 +127,6 @@ private static boolean isRecorder(AnnotatedElement element) { * @param classLoader the class loader * @param buildSystemProps the build system properties to use * @param launchMode launch mode - * @param configCustomizer configuration customizer * @return a consumer which adds the steps to the given chain builder * @throws IOException if the class loader could not load a resource * @throws ClassNotFoundException if a build step class is not found @@ -313,6 +313,12 @@ private static Consumer loadStepsFromClass(Class clazz, .asSubclass(SimpleBuildItem.class); stepConfig = stepConfig.andThen(bsb -> bsb.consumes(buildItemClass)); ctorParamFns.add(bc -> bc.consume(buildItemClass)); + } else if (isAnEmptyBuildItemConsumer(parameterType)) { + throw reportError(parameter, + "Cannot consume an empty build item, use @Consume(class) on the constructor instead"); + } else if (isAnEmptyBuildItemProducer(parameterType)) { + throw reportError(parameter, + "Cannot produce an empty build item, use @Produce(class) on the constructor instead"); } else if (isListOf(parameterType, MultiBuildItem.class)) { final Class buildItemClass = rawTypeOfParameter(parameterType, 0) .asSubclass(MultiBuildItem.class); @@ -423,6 +429,10 @@ private static Consumer loadStepsFromClass(Class clazz, stepConfig = stepConfig.andThen(bsb -> bsb.consumes(buildItemClass)); stepInstanceSetup = stepInstanceSetup .andThen((bc, o) -> ReflectUtil.setFieldVal(field, o, bc.consume(buildItemClass))); + } else if (isAnEmptyBuildItemConsumer(fieldType)) { + throw reportError(field, "Cannot consume an empty build item, use @Consume(class) on the field instead"); + } else if (isAnEmptyBuildItemProducer(fieldType)) { + throw reportError(field, "Cannot produce an empty build item, use @Produce(class) on the field instead"); } else if (isListOf(fieldType, MultiBuildItem.class)) { final Class buildItemClass = rawTypeOfParameter(fieldType, 0) .asSubclass(MultiBuildItem.class); @@ -588,6 +598,12 @@ private static Consumer loadStepsFromClass(Class clazz, .asSubclass(SimpleBuildItem.class); methodStepConfig = methodStepConfig.andThen(bsb -> bsb.consumes(buildItemClass)); methodParamFns.add((bc, bri) -> bc.consume(buildItemClass)); + } else if (isAnEmptyBuildItemConsumer(parameterType)) { + throw reportError(parameter, + "Cannot consume an empty build item, use @Produce(class) on the build step method instead"); + } else if (isAnEmptyBuildItemProducer(parameterType)) { + throw reportError(parameter, + "Cannot produce an empty build item, use @Produce(class) on the build step method instead"); } else if (isListOf(parameterType, MultiBuildItem.class)) { final Class buildItemClass = rawTypeOfParameter(parameterType, 0) .asSubclass(MultiBuildItem.class); @@ -745,6 +761,9 @@ private static Consumer loadStepsFromClass(Class clazz, final boolean overridable = method.isAnnotationPresent(Overridable.class); if (rawTypeIs(returnType, void.class)) { resultConsumer = Functions.discardingBiConsumer(); + } else if (rawTypeExtends(returnType, EmptyBuildItem.class) || isOptionalOf(returnType, EmptyBuildItem.class)) { + throw reportError(method, + "Cannot produce an empty build item, use @Produce(class) on the build step method instead"); } else if (rawTypeExtends(returnType, BuildItem.class)) { final Class type = method.getReturnType().asSubclass(BuildItem.class); if (overridable) { @@ -956,6 +975,18 @@ public String toString() { return chainConfig; } + private static boolean isAnEmptyBuildItemProducer(Type parameterType) { + return isBuildProducerOf(parameterType, EmptyBuildItem.class) + || isSupplierOf(parameterType, EmptyBuildItem.class) + || isSupplierOfOptionalOf(parameterType, EmptyBuildItem.class); + } + + private static boolean isAnEmptyBuildItemConsumer(Type parameterType) { + return rawTypeExtends(parameterType, EmptyBuildItem.class) + || isOptionalOf(parameterType, EmptyBuildItem.class) + || isConsumerOf(parameterType, EmptyBuildItem.class); + } + private static void deprecatedProducer(final Object element) { loadLog.warnf( "Producing values from constructors and fields is no longer supported and will be removed in a future release: %s", diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java index 7010488ea423c..01f7016e8832b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java @@ -16,6 +16,7 @@ public enum Feature { CACHE, CDI, CONFIG_YAML, + CONFLUENT_REGISTRY_AVRO, ELASTICSEARCH_REST_CLIENT_COMMON, ELASTICSEARCH_REST_CLIENT, ELASTICSEARCH_REST_HIGH_LEVEL_CLIENT, 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 7c10c563dfa3b..861e77cb98b0f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java @@ -7,9 +7,11 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.InvocationTargetException; +import java.net.InetSocketAddress; import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; +import java.time.Duration; import java.util.List; import java.util.Optional; import java.util.function.BooleanSupplier; @@ -25,6 +27,8 @@ public class IsDockerWorking implements BooleanSupplier { private static final Logger LOGGER = Logger.getLogger(IsDockerWorking.class.getName()); + public static final int DOCKER_HOST_CHECK_TIMEOUT = 1000; + public static final int DOCKER_CMD_CHECK_TIMEOUT = 3000; private final List strategies; @@ -40,6 +44,7 @@ public IsDockerWorking(boolean silent) { @Override public boolean getAsBoolean() { for (Strategy strategy : strategies) { + LOGGER.debugf("Checking Docker Environment using strategy %s", strategy.getClass().getName()); Result result = strategy.get(); if (result == Result.AVAILABLE) { return true; @@ -81,6 +86,9 @@ public Result get() { Object dockerClientFactoryInstance = dockerClientFactoryClass.getMethod("instance").invoke(null); boolean isAvailable = (boolean) dockerClientFactoryClass.getMethod("isDockerAvailable") .invoke(dockerClientFactoryInstance); + if (!isAvailable) { + compressor.closeAndDumpCaptured(); + } return isAvailable ? Result.AVAILABLE : Result.UNAVAILABLE; } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { if (!silent) { @@ -110,14 +118,15 @@ public Result get() { if (dockerHost != null && !dockerHost.startsWith("unix:")) { try { URI url = new URI(dockerHost); - try (Socket s = new Socket(url.getHost(), url.getPort())) { + try (Socket s = new Socket()) { + s.connect(new InetSocketAddress(url.getHost(), url.getPort()), DOCKER_HOST_CHECK_TIMEOUT); return Result.AVAILABLE; } catch (IOException e) { LOGGER.warnf( "Unable to connect to DOCKER_HOST URI %s, make sure docker is running on the specified host", dockerHost); } - } catch (URISyntaxException e) { + } catch (URISyntaxException | IllegalArgumentException e) { LOGGER.warnf("Unable to parse DOCKER_HOST URI %s, it will be ignored for working docker detection", dockerHost); } @@ -140,7 +149,7 @@ private DockerBinaryStrategy(boolean silent) { @Override public Result get() { try { - if (!ExecUtil.execSilent(binary, "-v")) { + if (!ExecUtil.execSilentWithTimeout(Duration.ofMillis(DOCKER_CMD_CHECK_TIMEOUT), binary, "-v")) { LOGGER.warnf("'%s -v' returned an error code. Make sure your Docker binary is correct", binary); return Result.UNKNOWN; } @@ -151,7 +160,8 @@ public Result get() { try { OutputFilter filter = new OutputFilter(); - if (ExecUtil.exec(new File("."), filter, "docker", "version", "--format", "'{{.Server.Version}}'")) { + if (ExecUtil.execWithTimeout(new File("."), filter, Duration.ofMillis(DOCKER_CMD_CHECK_TIMEOUT), + "docker", "version", "--format", "'{{.Server.Version}}'")) { LOGGER.debugf("Docker daemon found. Version: %s", filter.getOutput()); return Result.AVAILABLE; } else { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java index 80a9a3214984c..ca5eba396188b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java @@ -42,6 +42,8 @@ public final class BytecodeTransformerBuildItem extends MultiBuildItem { final int classReaderOptions; + final boolean continueOnFailure; + public BytecodeTransformerBuildItem(String classToTransform, BiFunction visitorFunction) { this(classToTransform, visitorFunction, null); @@ -78,6 +80,7 @@ public BytecodeTransformerBuildItem(boolean eager, String classToTransform, this.cacheable = cacheable; this.inputTransformer = null; this.classReaderOptions = 0; + this.continueOnFailure = false; } public BytecodeTransformerBuildItem(Builder builder) { @@ -88,6 +91,7 @@ public BytecodeTransformerBuildItem(Builder builder) { this.cacheable = builder.cacheable; this.inputTransformer = builder.inputTransformer; this.classReaderOptions = builder.classReaderOptions; + this.continueOnFailure = builder.continueOnFailure; if (visitorFunction == null && inputTransformer == null) { throw new IllegalArgumentException("One of either visitorFunction or inputTransformer must be set"); } @@ -121,8 +125,13 @@ public BiFunction getInputTransformer() { return inputTransformer; } + public boolean isContinueOnFailure() { + return continueOnFailure; + } + public static class Builder { public BiFunction inputTransformer; + public boolean continueOnFailure; private String classToTransform; private BiFunction visitorFunction; private Set requireConstPoolEntry = null; @@ -130,6 +139,11 @@ public static class Builder { private boolean cacheable = false; private int classReaderOptions = 0; + public Builder setContinueOnFailure(boolean continueOnFailure) { + this.continueOnFailure = continueOnFailure; + return this; + } + public Builder setInputTransformer(BiFunction inputTransformer) { this.inputTransformer = inputTransformer; return this; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/DockerStatusBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/DockerStatusBuildItem.java new file mode 100644 index 0000000000000..9309683477926 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/DockerStatusBuildItem.java @@ -0,0 +1,21 @@ +package io.quarkus.deployment.builditem; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.deployment.IsDockerWorking; + +public final class DockerStatusBuildItem extends SimpleBuildItem { + + private final IsDockerWorking isDockerWorking; + private Boolean cachedStatus; + + public DockerStatusBuildItem(IsDockerWorking isDockerWorking) { + this.isDockerWorking = isDockerWorking; + } + + public synchronized boolean isDockerAvailable() { + if (cachedStatus == null) { + cachedStatus = isDockerWorking.getAsBoolean(); + } + return cachedStatus; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/LogFileFormatBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/LogFileFormatBuildItem.java new file mode 100644 index 0000000000000..2547b409e8984 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/LogFileFormatBuildItem.java @@ -0,0 +1,36 @@ +package io.quarkus.deployment.builditem; + +import java.util.Optional; +import java.util.logging.Formatter; + +import org.wildfly.common.Assert; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.runtime.RuntimeValue; + +/** + * The log file format build item. Producing this item will cause the logging subsystem to disregard its + * file logging formatting configuration and use the formatter provided instead. If multiple formatters + * are enabled at runtime, a warning message is printed and only one is used. + */ +public final class LogFileFormatBuildItem extends MultiBuildItem { + private final RuntimeValue> formatterValue; + + /** + * Construct a new instance. + * + * @param formatterValue the optional formatter runtime value to use (must not be {@code null}) + */ + public LogFileFormatBuildItem(final RuntimeValue> formatterValue) { + this.formatterValue = Assert.checkNotNullParam("formatterValue", formatterValue); + } + + /** + * Get the formatter value. + * + * @return the formatter value + */ + public RuntimeValue> getFormatterValue() { + return formatterValue; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java index 4d997b6ac3700..9427194fd7f49 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java @@ -633,6 +633,39 @@ public void run() { // bootstrap config default values readBootstrapConfig.writeArrayValue(bootstrapConfigSourcesArray, 3, readBootstrapConfig.readStaticField(C_BOOTSTRAP_DEFAULTS_CONFIG_SOURCE)); + + // add bootstrap safe static sources + for (String bootstrapConfigSource : staticConfigSources) { + readBootstrapConfig.invokeStaticMethod(CU_ADD_SOURCE_PROVIDER, bootstrapBuilder, + readBootstrapConfig.newInstance(RCS_NEW, readBootstrapConfig.load(bootstrapConfigSource))); + } + // add bootstrap safe static source providers + for (String bootstrapConfigSourceProvider : staticConfigSourceProviders) { + readBootstrapConfig.invokeStaticMethod(CU_ADD_SOURCE_PROVIDER, bootstrapBuilder, + readBootstrapConfig.newInstance(RCSP_NEW, readBootstrapConfig.load(bootstrapConfigSourceProvider))); + } + // add bootstrap safe static source factories + for (String discoveredConfigSourceFactory : staticConfigSourceFactories) { + readBootstrapConfig.invokeStaticMethod(CU_ADD_SOURCE_FACTORY_PROVIDER, bootstrapBuilder, + readBootstrapConfig.newInstance(RCSF_NEW, readBootstrapConfig.load(discoveredConfigSourceFactory))); + } + // add bootstrap mappings + for (ConfigClassWithPrefix configMapping : staticConfigMappings) { + readBootstrapConfig.invokeStaticMethod(CU_WITH_MAPPING, bootstrapBuilder, + readBootstrapConfig.load(configMapping.getKlass().getName()), + readBootstrapConfig.load(configMapping.getPrefix())); + } + + // add bootstrap config builders + ResultHandle bootstrapConfigBuilderInstances = readBootstrapConfig.newInstance(AL_NEW); + for (String configBuilder : staticConfigBuilders) { + ResultHandle staticConfigBuilderInstance = readBootstrapConfig + .newInstance(MethodDescriptor.ofConstructor(configBuilder)); + readBootstrapConfig.invokeVirtualMethod(AL_ADD, bootstrapConfigBuilderInstances, + staticConfigBuilderInstance); + } + readBootstrapConfig.invokeStaticMethod(CU_CONFIG_BUILDER_LIST, bootstrapBuilder, + bootstrapConfigBuilderInstances); } // add in our custom sources diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/CompilationProvider.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/CompilationProvider.java index 334936d3b04b5..91cd9106f28a6 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/CompilationProvider.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/CompilationProvider.java @@ -6,9 +6,10 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import io.quarkus.paths.PathCollection; @@ -17,6 +18,10 @@ public interface CompilationProvider extends Closeable { Set handledExtensions(); + default String getProviderKey() { + return getClass().getName(); + } + default Set handledSourcePaths() { return Collections.emptySet(); } @@ -38,7 +43,7 @@ class Context { private final File sourceDirectory; private final File outputDirectory; private final Charset sourceEncoding; - private final List compilerOptions; + private final Map> compilerOptions; private final String releaseJavaVersion; private final String sourceJavaVersion; private final String targetJvmVersion; @@ -52,7 +57,7 @@ public Context( File sourceDirectory, File outputDirectory, String sourceEncoding, - List compilerOptions, + Map> compilerOptions, String releaseJavaVersion, String sourceJavaVersion, String targetJvmVersion, @@ -64,7 +69,7 @@ public Context( this.sourceDirectory = sourceDirectory; this.outputDirectory = outputDirectory; this.sourceEncoding = sourceEncoding == null ? StandardCharsets.UTF_8 : Charset.forName(sourceEncoding); - this.compilerOptions = compilerOptions == null ? new ArrayList() : compilerOptions; + this.compilerOptions = compilerOptions == null ? new HashMap<>() : compilerOptions; this.releaseJavaVersion = releaseJavaVersion; this.sourceJavaVersion = sourceJavaVersion; this.targetJvmVersion = targetJvmVersion; @@ -96,8 +101,8 @@ public Charset getSourceEncoding() { return sourceEncoding; } - public List getCompilerOptions() { - return compilerOptions; + public Set getCompilerOptions(String key) { + return compilerOptions.getOrDefault(key, Collections.emptySet()); } public String getReleaseJavaVersion() { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java index 2416acf96aa91..114468381a11f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java @@ -7,6 +7,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -50,7 +51,7 @@ public class DevModeContext implements Serializable { // args of the main-method private String[] args; - private List compilerOptions; + private Map> compilerOptions; private String releaseJavaVersion; private String sourceJavaVersion; private String targetJvmVersion; @@ -138,11 +139,11 @@ public void setAbortOnFailedStart(boolean abortOnFailedStart) { this.abortOnFailedStart = abortOnFailedStart; } - public List getCompilerOptions() { + public Map> getCompilerOptions() { return compilerOptions; } - public void setCompilerOptions(List compilerOptions) { + public void setCompilerOptions(Map> compilerOptions) { this.compilerOptions = compilerOptions; } @@ -451,6 +452,6 @@ public boolean isEnablePreview() { if (compilerOptions == null) { return false; } - return compilerOptions.contains(ENABLE_PREVIEW_FLAG); + return compilerOptions.getOrDefault("java", Collections.emptySet()).contains(ENABLE_PREVIEW_FLAG); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java index 1052ec948e798..04ad2dd633b2f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java @@ -86,22 +86,23 @@ public void start() throws Exception { } } } - final PathList.Builder appRoots = PathList.builder(); - Path p = Path.of(context.getApplicationRoot().getMain().getClassesPath()); - if (Files.exists(p)) { - appRoots.add(p); + final PathList.Builder appRootsBuilder = PathList.builder(); + final Path classesPath = Path.of(context.getApplicationRoot().getMain().getClassesPath()); + if (Files.exists(classesPath)) { + appRootsBuilder.add(classesPath); } if (context.getApplicationRoot().getMain().getResourcesOutputPath() != null && !context.getApplicationRoot().getMain().getResourcesOutputPath() .equals(context.getApplicationRoot().getMain().getClassesPath())) { - p = Paths.get(context.getApplicationRoot().getMain().getResourcesOutputPath()); - if (Files.exists(p)) { - appRoots.add(p); + final Path resourcesOutputPath = Paths.get(context.getApplicationRoot().getMain().getResourcesOutputPath()); + if (Files.exists(resourcesOutputPath)) { + appRootsBuilder.add(resourcesOutputPath); } } + final PathList appRoots = appRootsBuilder.build(); QuarkusBootstrap.Builder bootstrapBuilder = QuarkusBootstrap.builder() - .setApplicationRoot(appRoots.build()) + .setApplicationRoot(appRoots) .setIsolateDeployment(true) .setLocalProjectDiscovery(context.isLocalProjectDiscovery()) .addAdditionalDeploymentArchive(path) @@ -109,12 +110,10 @@ public void start() throws Exception { .setMode(context.getMode()); if (context.getDevModeRunnerJarFile() != null) { bootstrapBuilder.setTargetDirectory(context.getDevModeRunnerJarFile().getParentFile().toPath()); + } else if (context.getApplicationRoot().getTargetDir() != null) { + bootstrapBuilder.setTargetDirectory(Path.of(context.getApplicationRoot().getTargetDir())); } - if (context.getProjectDir() != null) { - bootstrapBuilder.setProjectRoot(context.getProjectDir().toPath()); - } else { - bootstrapBuilder.setProjectRoot(new File(".").toPath()); - } + bootstrapBuilder.setProjectRoot(resolveProjectRoot()); for (ArtifactKey i : context.getLocalArtifacts()) { bootstrapBuilder.addLocalArtifact(i); } @@ -140,6 +139,18 @@ public void start() throws Exception { } } + private Path resolveProjectRoot() { + final Path projectRoot; + if (context.getProjectDir() != null) { + projectRoot = context.getProjectDir().toPath(); + } else if (context.getApplicationRoot().getProjectDirectory() != null) { + projectRoot = Path.of(context.getApplicationRoot().getProjectDirectory()); + } else { + projectRoot = new File(".").toPath(); + } + return projectRoot; + } + // links the .env file to the directory where the process is running // this is done because for the .env file to take effect, it needs to be // in the same directory as the running process diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/HotDeploymentConfigFileBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/HotDeploymentWatchedFileBuildStep.java similarity index 89% rename from core/deployment/src/main/java/io/quarkus/deployment/dev/HotDeploymentConfigFileBuildStep.java rename to core/deployment/src/main/java/io/quarkus/deployment/dev/HotDeploymentWatchedFileBuildStep.java index 9e5fa01ad8238..d4b832420f7cb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/HotDeploymentConfigFileBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/HotDeploymentWatchedFileBuildStep.java @@ -10,10 +10,10 @@ import io.quarkus.deployment.builditem.ServiceStartBuildItem; import io.quarkus.dev.testing.TestWatchedFiles; -public class HotDeploymentConfigFileBuildStep { +public class HotDeploymentWatchedFileBuildStep { @BuildStep - ServiceStartBuildItem setupConfigFileHotDeployment(List files, + ServiceStartBuildItem setupWatchedFileHotDeployment(List files, LaunchModeBuildItem launchModeBuildItem) { // TODO: this should really be an output of the RuntimeRunner RuntimeUpdatesProcessor processor = RuntimeUpdatesProcessor.INSTANCE; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java index 6c5394dab1e3e..0dcc28aaf501c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java @@ -54,6 +54,7 @@ import io.quarkus.deployment.dev.testing.TestSupport; import io.quarkus.deployment.steps.ClassTransformingBuildStep; import io.quarkus.deployment.util.FSWatchUtil; +import io.quarkus.dev.appstate.ApplicationStartException; import io.quarkus.dev.console.DevConsoleManager; import io.quarkus.dev.spi.DeploymentFailedStartHandler; import io.quarkus.dev.spi.DevModeType; @@ -183,7 +184,11 @@ public void accept(String args) { } RuntimeUpdatesProcessor.INSTANCE.startupFailed(); //try and wait till logging is setup - log.error("Failed to start quarkus", t); + + //this exception has already been logged, so don't log it again + if (!(t instanceof ApplicationStartException)) { + log.error("Failed to start quarkus", t); + } } catch (Exception e) { close(); log.error("Failed to start quarkus", t); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/JavaCompilationProvider.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/JavaCompilationProvider.java index 6957c6df0fbc0..477638127eb1f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/JavaCompilationProvider.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/JavaCompilationProvider.java @@ -40,6 +40,11 @@ public class JavaCompilationProvider implements CompilationProvider { StandardJavaFileManager fileManager; DiagnosticCollector fileManagerDiagnostics; + @Override + public String getProviderKey() { + return "java"; + } + @Override public Set handledExtensions() { return Collections.singleton(".java"); @@ -64,7 +69,7 @@ public void compile(Set filesToCompile, Context context) { fileManager.setLocation(StandardLocation.CLASS_PATH, context.getClasspath()); fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Collections.singleton(context.getOutputDirectory())); - CompilerFlags compilerFlags = new CompilerFlags(COMPILER_OPTIONS, context.getCompilerOptions(), + CompilerFlags compilerFlags = new CompilerFlags(COMPILER_OPTIONS, context.getCompilerOptions(getProviderKey()), context.getReleaseJavaVersion(), context.getSourceJavaVersion(), context.getTargetJvmVersion()); Iterable sources = fileManager.getJavaFileObjectsFromFiles(filesToCompile); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusCompiler.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusCompiler.java index 5f7e30d5ed4fd..47cbd930727a4 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusCompiler.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusCompiler.java @@ -38,6 +38,9 @@ public class QuarkusCompiler implements Closeable { private static final Logger log = Logger.getLogger(QuarkusCompiler.class); private static final Pattern WHITESPACE_PATTERN = Pattern.compile(" "); + private static final String JAVA_COMPILER_KEY = "java"; + private static final String KOTLIN_COMPILER_KEY = "kotlin"; + private final List compilationProviders; /** * map of compilation contexts to source directories @@ -157,6 +160,7 @@ public void setupSourceCompilationContext(DevModeContext context, Set clas + "'. It is advised that this module be compiled before launching dev mode"); return; } + for (Path sourcePath : compilationUnit.getSourcePaths()) { final String srcPathStr = sourcePath.toString(); if (this.compilationContexts.containsKey(srcPathStr)) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java index 23cd0fc0a1590..b78f854de8acc 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java @@ -147,15 +147,19 @@ public B sourceEncoding(String srcEncoding) { return (B) this; } - @SuppressWarnings("unchecked") - public B compilerOption(String option) { - compilerOptions.add(option); + public B compilerOptions(String name, List options) { + compilerOptions.compute(name, (key, value) -> { + if (value == null) { + return new HashSet<>(options); + } + value.addAll(options); + return value; + }); return (B) this; } - @SuppressWarnings("unchecked") - public B compilerOptions(List options) { - compilerOptions.addAll(options); + public B compilerOptions(Map> options) { + compilerOptions.putAll(options); return (B) this; } @@ -286,7 +290,7 @@ public R build() throws Exception { private String applicationName; private String applicationVersion; private String sourceEncoding; - private List compilerOptions = new ArrayList<>(0); + private Map> compilerOptions = new HashMap<>(1); private List compilerPluginArtifacts; private List compilerPluginOptions; private String releaseJavaVersion; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java index 7f0765dad1be8..e58f35fbca804 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java @@ -874,6 +874,7 @@ Set checkForFileChange(Function roots = rootPaths.stream() .filter(Files::exists) .filter(Files::isReadable) @@ -883,7 +884,6 @@ Set checkForFileChange(Function seen = new HashSet<>(moduleResources); try { for (Path root : roots) { - Path outputDir = Paths.get(outputPath); //since the stream is Closeable, use a try with resources so the underlying iterator is closed try (final Stream walk = Files.walk(root)) { walk.forEach(path -> { @@ -927,11 +927,13 @@ Set checkForFileChange(Function checkForFileChange(Function 0); + } + } + if (!pathCurrentlyExisting) { + if (pathPreviouslyExisting) { + ret.add(path); + } + + Path target = outputDir.resolve(path); + try { + FileUtil.deleteIfExists(target); + } catch (IOException e) { + throw new UncheckedIOException(e); } } } @@ -1011,7 +1020,12 @@ Set checkForFileChange(Function 0) { + ret.add(watchedFilePath); + } } } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java index e64c13d3d6742..0d72f7a3b04dc 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java @@ -122,13 +122,18 @@ public void instrumentTestClasses(CombinedIndexBuildItem combinedIndexBuildItem, for (ClassInfo clazz : combinedIndexBuildItem.getIndex().getKnownClasses()) { String theClassName = clazz.name().toString(); if (isAppClass(theClassName)) { - transformerProducer.produce(new BytecodeTransformerBuildItem(false, theClassName, - new BiFunction() { - @Override - public ClassVisitor apply(String s, ClassVisitor classVisitor) { - return new TracingClassVisitor(classVisitor, theClassName); - } - }, true)); + transformerProducer.produce(new BytecodeTransformerBuildItem.Builder().setEager(false) + .setClassToTransform(theClassName) + .setVisitorFunction( + new BiFunction() { + @Override + public ClassVisitor apply(String s, ClassVisitor classVisitor) { + return new TracingClassVisitor(classVisitor, theClassName); + } + }) + .setCacheable(true) + .setContinueOnFailure(true) + .build()); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java index ee7c0e1cddad7..4d432b40cf6ed 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java @@ -12,6 +12,7 @@ import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; @@ -54,6 +55,7 @@ import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.LogCategoryBuildItem; import io.quarkus.deployment.builditem.LogConsoleFormatBuildItem; +import io.quarkus.deployment.builditem.LogFileFormatBuildItem; import io.quarkus.deployment.builditem.LogHandlerBuildItem; import io.quarkus.deployment.builditem.NamedLogHandlersBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; @@ -168,7 +170,9 @@ void miscSetup( LoggingSetupBuildItem setupLoggingRuntimeInit(LoggingSetupRecorder recorder, LogConfig log, LogBuildTimeConfig buildLog, Optional logStreamHandlerBuildItem, List handlerBuildItems, - List namedHandlerBuildItems, List consoleFormatItems, + List namedHandlerBuildItems, + List consoleFormatItems, + List fileFormatItems, Optional possibleBannerBuildItem, List logStreamBuildItems, LaunchModeBuildItem launchModeBuildItem, @@ -199,9 +203,12 @@ LoggingSetupBuildItem setupLoggingRuntimeInit(LoggingSetupRecorder recorder, Log alwaysEnableLogStream = true; } + List>> possibleFileFormatters = fileFormatItems.stream() + .map(LogFileFormatBuildItem::getFormatterValue).collect(Collectors.toList()); recorder.initializeLogging(log, buildLog, alwaysEnableLogStream, devUiLogHandler, handlers, namedHandlers, consoleFormatItems.stream().map(LogConsoleFormatBuildItem::getFormatterValue).collect(Collectors.toList()), + possibleFileFormatters, possibleSupplier, launchModeBuildItem.getLaunchMode()); LogConfig logConfig = new LogConfig(); ConfigInstantiator.handleObject(logConfig); @@ -215,7 +222,8 @@ LoggingSetupBuildItem setupLoggingRuntimeInit(LoggingSetupRecorder recorder, Log } ConsoleRuntimeConfig crc = new ConsoleRuntimeConfig(); ConfigInstantiator.handleObject(crc); - LoggingSetupRecorder.initializeBuildTimeLogging(logConfig, buildLog, crc, launchModeBuildItem.getLaunchMode()); + LoggingSetupRecorder.initializeBuildTimeLogging(logConfig, buildLog, crc, possibleFileFormatters, + launchModeBuildItem.getLaunchMode()); ((QuarkusClassLoader) Thread.currentThread().getContextClassLoader()).addCloseTask(new Runnable() { @Override public void run() { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java index ba04dd8393c19..8b107fb8aa0cd 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java @@ -16,8 +16,10 @@ @ConfigRoot(phase = ConfigPhase.BUILD_TIME) public class NativeConfig { - public static final String DEFAULT_GRAALVM_BUILDER_IMAGE = "quay.io/quarkus/ubi-quarkus-native-image:22.0-java11"; - public static final String DEFAULT_MANDREL_BUILDER_IMAGE = "quay.io/quarkus/ubi-quarkus-mandrel:22.0-java11"; + public static final String GRAALVM_BUILDER_IMAGE_JAVA_11 = "quay.io/quarkus/ubi-quarkus-native-image:22.1-java11"; + public static final String GRAALVM_BUILDER_IMAGE_JAVA_17 = "quay.io/quarkus/ubi-quarkus-native-image:22.1-java17"; + public static final String MANDREL_BUILDER_IMAGE_JAVA_11 = "quay.io/quarkus/ubi-quarkus-mandrel:22.1-java11"; + public static final String MANDREL_BUILDER_IMAGE_JAVA_17 = "quay.io/quarkus/ubi-quarkus-mandrel:22.1-java17"; /** * Comma-separated, additional arguments to pass to the build process. @@ -213,12 +215,12 @@ public boolean isContainerBuild() { @ConfigItem(defaultValue = "${platform.quarkus.native.builder-image}") public String builderImage; - public String getEffectiveBuilderImage() { + public String getEffectiveBuilderImage(boolean useJava17Images) { final String builderImageName = this.builderImage.toUpperCase(); if (builderImageName.equals(BuilderImageProvider.GRAALVM.name())) { - return DEFAULT_GRAALVM_BUILDER_IMAGE; + return useJava17Images ? GRAALVM_BUILDER_IMAGE_JAVA_17 : GRAALVM_BUILDER_IMAGE_JAVA_11; } else if (builderImageName.equals(BuilderImageProvider.MANDREL.name())) { - return DEFAULT_MANDREL_BUILDER_IMAGE; + return useJava17Images ? MANDREL_BUILDER_IMAGE_JAVA_17 : MANDREL_BUILDER_IMAGE_JAVA_11; } else { return this.builderImage; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java index 01c8f851f692a..7ee2993cbd908 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java @@ -128,6 +128,27 @@ public class PackageConfig { @ConfigItem public Optional appcdsBuilderImage; + /** + * Whether creation of the AppCDS archive should run in a container if available. + * + *

+ * Normally, if either a suitable container image to create the AppCDS archive inside of + * can be determined automatically or if one is explicitly set using the + * {@code quarkus.package.appcds-builder-image} setting, the AppCDS archive is generated by + * running the JDK contained in the image as a container. + * + *

+ * If this option is set to {@code false}, a container will not be used to generate the + * AppCDS archive. Instead, the JDK used to build the application is also used to create the + * archive. Note that the exact same JDK version must be used to run the application in this + * case. + * + *

+ * Ignored if {@code quarkus.package.create-appcds} is set to {@code false}. + */ + @ConfigItem(defaultValue = "true") + public boolean appcdsUseContainer; + /** * This is an advanced option that only takes effect for the mutable-jar format. *

diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java index a999af8a0602a..c7d10dd152feb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/CompiledJavaVersionBuildItem.java @@ -24,6 +24,8 @@ public JavaVersion getJavaVersion() { public interface JavaVersion { + Status isExactlyJava11(); + Status isJava11OrHigher(); Status isJava17OrHigher(); @@ -36,9 +38,16 @@ enum Status { final class Unknown implements JavaVersion { + public static final Unknown INSTANCE = new Unknown(); + Unknown() { } + @Override + public Status isExactlyJava11() { + return Status.UNKNOWN; + } + @Override public Status isJava11OrHigher() { return Status.UNKNOWN; @@ -61,19 +70,28 @@ final class Known implements JavaVersion { this.determinedMajor = determinedMajor; } + @Override + public Status isExactlyJava11() { + return equalStatus(JAVA_11_MAJOR); + } + @Override public Status isJava11OrHigher() { - return getStatus(JAVA_11_MAJOR); + return higherOrEqualStatus(JAVA_11_MAJOR); } @Override public Status isJava17OrHigher() { - return getStatus(JAVA_17_MAJOR); + return higherOrEqualStatus(JAVA_17_MAJOR); } - private Status getStatus(int javaMajor) { + private Status higherOrEqualStatus(int javaMajor) { return determinedMajor >= javaMajor ? Status.TRUE : Status.FALSE; } + + private Status equalStatus(int javaMajor) { + return determinedMajor == javaMajor ? Status.TRUE : Status.FALSE; + } } } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java index 66da8f0954da1..06b53d8c64671 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java @@ -102,7 +102,9 @@ public void build(Optional appCDsRequested, private String determineContainerImage(PackageConfig packageConfig, Optional appCDSContainerImage) { - if (packageConfig.appcdsBuilderImage.isPresent()) { + if (!packageConfig.appcdsUseContainer) { + return null; + } else if (packageConfig.appcdsBuilderImage.isPresent()) { return packageConfig.appcdsBuilderImage.get(); } else if (appCDSContainerImage.isPresent()) { return appCDSContainerImage.get().getContainerImage(); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java index 99ebba5d78824..df26f48ebd845 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java @@ -27,8 +27,10 @@ static final class Version implements Comparable { static final Version VERSION_21_3_2 = new Version("GraalVM 21.3.2", "21.3.2", Distribution.ORACLE); static final Version VERSION_22_0_0_2 = new Version("GraalVM 22.0.0.2", "22.0.0.2", Distribution.ORACLE); + static final Version VERSION_22_1_0 = new Version("GraalVM 22.1.0", "22.1.0", Distribution.ORACLE); + static final Version MINIMUM = VERSION_21_2; - static final Version CURRENT = VERSION_22_0_0_2; + static final Version CURRENT = VERSION_22_1_0; public static final int UNDEFINED = -1; final String fullVersion; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java index a3e8b127f65e2..f61f1c6ffe4fb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java @@ -14,6 +14,7 @@ import org.jboss.logging.Logger; import io.quarkus.deployment.pkg.NativeConfig; +import io.quarkus.deployment.pkg.builditem.CompiledJavaVersionBuildItem; import io.quarkus.deployment.util.ProcessUtil; import io.quarkus.runtime.util.ContainerRuntimeUtil; @@ -26,8 +27,10 @@ public abstract class NativeImageBuildContainerRunner extends NativeImageBuildRu String[] baseContainerRuntimeArgs; protected final String outputPath; private final String containerName; + private final boolean needJava17Image; - public NativeImageBuildContainerRunner(NativeConfig nativeConfig, Path outputDir) { + public NativeImageBuildContainerRunner(NativeConfig nativeConfig, Path outputDir, + CompiledJavaVersionBuildItem.JavaVersion javaVersion) { this.nativeConfig = nativeConfig; containerRuntime = nativeConfig.containerRuntime.orElseGet(ContainerRuntimeUtil::detectContainerRuntime); log.infof("Using %s to run the native image builder", containerRuntime.getExecutableName()); @@ -36,6 +39,7 @@ public NativeImageBuildContainerRunner(NativeConfig nativeConfig, Path outputDir outputPath = outputDir == null ? null : outputDir.toAbsolutePath().toString(); containerName = "build-native-" + RandomStringUtils.random(5, true, false); + this.needJava17Image = javaVersion.isExactlyJava11() == CompiledJavaVersionBuildItem.JavaVersion.Status.FALSE; } @Override @@ -45,15 +49,16 @@ public void setup(boolean processInheritIODisabled) { // we pull the docker image in order to give users an indication of which step the process is at // it's not strictly necessary we do this, however if we don't the subsequent version command // will appear to block and no output will be shown - log.info("Checking image status " + nativeConfig.getEffectiveBuilderImage()); + String effectiveBuilderImage = nativeConfig.getEffectiveBuilderImage(needJava17Image); + log.info("Checking image status " + effectiveBuilderImage); Process pullProcess = null; try { final ProcessBuilder pb = new ProcessBuilder( - Arrays.asList(containerRuntime.getExecutableName(), "pull", nativeConfig.getEffectiveBuilderImage())); + Arrays.asList(containerRuntime.getExecutableName(), "pull", effectiveBuilderImage)); pullProcess = ProcessUtil.launchProcess(pb, processInheritIODisabled); pullProcess.waitFor(); } catch (IOException | InterruptedException e) { - throw new RuntimeException("Failed to pull builder image " + nativeConfig.getEffectiveBuilderImage(), e); + throw new RuntimeException("Failed to pull builder image " + effectiveBuilderImage, e); } finally { if (pullProcess != null) { pullProcess.destroy(); @@ -120,7 +125,8 @@ protected List getContainerRuntimeBuildArgs() { protected String[] buildCommand(String dockerCmd, List containerRuntimeArgs, List command) { return Stream .of(Stream.of(containerRuntime.getExecutableName()), Stream.of(dockerCmd), Stream.of(baseContainerRuntimeArgs), - containerRuntimeArgs.stream(), Stream.of(nativeConfig.getEffectiveBuilderImage()), command.stream()) + containerRuntimeArgs.stream(), Stream.of(nativeConfig.getEffectiveBuilderImage(needJava17Image)), + command.stream()) .flatMap(Function.identity()).toArray(String[]::new); } 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 b63f2cb92886f..da449581e169e 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 @@ -11,13 +11,15 @@ import org.apache.commons.lang3.SystemUtils; import io.quarkus.deployment.pkg.NativeConfig; +import io.quarkus.deployment.pkg.builditem.CompiledJavaVersionBuildItem; import io.quarkus.deployment.util.FileUtil; import io.quarkus.runtime.util.ContainerRuntimeUtil; public class NativeImageBuildLocalContainerRunner extends NativeImageBuildContainerRunner { - public NativeImageBuildLocalContainerRunner(NativeConfig nativeConfig, Path outputDir) { - super(nativeConfig, outputDir); + public NativeImageBuildLocalContainerRunner(NativeConfig nativeConfig, Path outputDir, + CompiledJavaVersionBuildItem.JavaVersion javaVersion) { + super(nativeConfig, outputDir, javaVersion); if (SystemUtils.IS_OS_LINUX) { ArrayList containerRuntimeArgs = new ArrayList<>(Arrays.asList(baseContainerRuntimeArgs)); String uid = getLinuxID("-ur"); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRemoteContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRemoteContainerRunner.java index f427ca4c5268b..48902b775a1f4 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRemoteContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildRemoteContainerRunner.java @@ -11,6 +11,7 @@ import org.jboss.logging.Logger; import io.quarkus.deployment.pkg.NativeConfig; +import io.quarkus.deployment.pkg.builditem.CompiledJavaVersionBuildItem; public class NativeImageBuildRemoteContainerRunner extends NativeImageBuildContainerRunner { @@ -25,8 +26,9 @@ public class NativeImageBuildRemoteContainerRunner extends NativeImageBuildConta private String containerId; public NativeImageBuildRemoteContainerRunner(NativeConfig nativeConfig, Path outputDir, + CompiledJavaVersionBuildItem.JavaVersion javaVersion, String nativeImageName, String resultingExecutableName) { - super(nativeConfig, outputDir); + super(nativeConfig, outputDir, javaVersion); this.nativeImageName = nativeImageName; this.resultingExecutableName = resultingExecutableName; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index 844727aad0f53..da4ece1c7703f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -35,6 +35,7 @@ import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem; +import io.quarkus.deployment.pkg.builditem.CompiledJavaVersionBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.deployment.pkg.builditem.NativeImageBuildItem; import io.quarkus.deployment.pkg.builditem.NativeImageSourceJarBuildItem; @@ -155,6 +156,7 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, LocalesBuildTimeCon List jpmsExportBuildItems, List nativeMinimalJavaVersions, List unsupportedOses, + CompiledJavaVersionBuildItem compiledJavaVersionBuildItem, Optional processInheritIODisabled, Optional processInheritIODisabledBuildItem) { if (nativeConfig.debug.enabled) { @@ -181,6 +183,7 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, LocalesBuildTimeCon Path finalExecutablePath = outputTargetBuildItem.getOutputDirectory().resolve(resultingExecutableName); NativeImageBuildRunner buildRunner = getNativeImageBuildRunner(nativeConfig, outputDir, + compiledJavaVersionBuildItem.getJavaVersion(), nativeImageName, resultingExecutableName); buildRunner.setup(processInheritIODisabled.isPresent() || processInheritIODisabledBuildItem.isPresent()); final GraalVM.Version graalVMVersion = buildRunner.getGraalVMVersion(); @@ -280,6 +283,7 @@ private String getResultingExecutableName(String nativeImageName, boolean isCont } private static NativeImageBuildRunner getNativeImageBuildRunner(NativeConfig nativeConfig, Path outputDir, + CompiledJavaVersionBuildItem.JavaVersion javaVersion, String nativeImageName, String resultingExecutableName) { if (!nativeConfig.isContainerBuild()) { NativeImageBuildLocalRunner localRunner = getNativeImageBuildLocalRunner(nativeConfig, outputDir.toFile()); @@ -295,10 +299,10 @@ private static NativeImageBuildRunner getNativeImageBuildRunner(NativeConfig nat log.warn(errorMessage + " Attempting to fall back to container build."); } if (nativeConfig.remoteContainerBuild) { - return new NativeImageBuildRemoteContainerRunner(nativeConfig, outputDir, + return new NativeImageBuildRemoteContainerRunner(nativeConfig, outputDir, javaVersion, nativeImageName, resultingExecutableName); } - return new NativeImageBuildLocalContainerRunner(nativeConfig, outputDir); + return new NativeImageBuildLocalContainerRunner(nativeConfig, outputDir, javaVersion); } private void copyJarSourcesToLib(OutputTargetBuildItem outputTargetBuildItem, diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java index 2f12f6edf7be0..043ff6fd1e38c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/UpxCompressionBuildStep.java @@ -18,6 +18,7 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.pkg.NativeConfig; import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; +import io.quarkus.deployment.pkg.builditem.CompiledJavaVersionBuildItem; import io.quarkus.deployment.pkg.builditem.NativeImageBuildItem; import io.quarkus.deployment.pkg.builditem.UpxCompressedBuildItem; import io.quarkus.deployment.util.FileUtil; @@ -35,6 +36,7 @@ public class UpxCompressionBuildStep { @BuildStep(onlyIf = NativeBuild.class) public void compress(NativeConfig nativeConfig, NativeImageBuildItem image, + CompiledJavaVersionBuildItem compiledJavaVersionBuildItem, BuildProducer upxCompressedProducer, BuildProducer artifactResultProducer) { if (nativeConfig.compression.level.isEmpty()) { @@ -42,6 +44,9 @@ public void compress(NativeConfig nativeConfig, NativeImageBuildItem image, return; } + String effectiveBuilderImage = nativeConfig.getEffectiveBuilderImage( + compiledJavaVersionBuildItem.getJavaVersion() + .isExactlyJava11() == CompiledJavaVersionBuildItem.JavaVersion.Status.FALSE); Optional upxPathFromSystem = getUpxFromSystem(); if (upxPathFromSystem.isPresent()) { log.debug("Running UPX from system path"); @@ -49,8 +54,8 @@ public void compress(NativeConfig nativeConfig, NativeImageBuildItem image, throw new IllegalStateException("Unable to compress the native executable"); } } else if (nativeConfig.isContainerBuild()) { - log.infof("Running UPX from a container using the builder image: " + nativeConfig.getEffectiveBuilderImage()); - if (!runUpxInContainer(image, nativeConfig)) { + log.infof("Running UPX from a container using the builder image: " + effectiveBuilderImage); + if (!runUpxInContainer(image, nativeConfig, effectiveBuilderImage)) { throw new IllegalStateException("Unable to compress the native executable"); } } else { @@ -95,7 +100,8 @@ private boolean runUpxFromHost(File upx, File executable, NativeConfig nativeCon } - private boolean runUpxInContainer(NativeImageBuildItem nativeImage, NativeConfig nativeConfig) { + private boolean runUpxInContainer(NativeImageBuildItem nativeImage, NativeConfig nativeConfig, + String effectiveBuilderImage) { String level = getCompressionLevel(nativeConfig.compression.level.getAsInt()); List extraArgs = nativeConfig.compression.additionalArgs.orElse(Collections.emptyList()); @@ -132,7 +138,7 @@ private boolean runUpxInContainer(NativeImageBuildItem nativeImage, NativeConfig Collections.addAll(commandLine, "-v", volumeOutputPath + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH + ":z"); - commandLine.add(nativeConfig.getEffectiveBuilderImage()); + commandLine.add(effectiveBuilderImage); commandLine.add(level); commandLine.addAll(extraArgs); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java index d9ab5a0d55fa6..95517d41d08a8 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java @@ -125,6 +125,9 @@ public byte[] apply(String className, byte[] originalBytes) { if (classTransformers == null) { return originalBytes; } + boolean continueOnFailure = classTransformers.stream() + .filter(a -> !a.isContinueOnFailure()) + .findAny().isEmpty(); List> visitors = classTransformers.stream() .map(BytecodeTransformerBuildItem::getVisitorFunction).filter(Objects::nonNull) .collect(Collectors.toList()); @@ -154,6 +157,17 @@ public byte[] apply(String className, byte[] originalBytes) { } else { return originalBytes; } + } catch (Throwable e) { + if (continueOnFailure) { + if (log.isDebugEnabled()) { + log.errorf(e, "Failed to transform %s", className); + } else { + log.errorf("Failed to transform %s", className); + } + return originalBytes; + } else { + throw e; + } } finally { Thread.currentThread().setContextClassLoader(old); } @@ -184,6 +198,10 @@ public byte[] apply(String className, byte[] originalBytes) { entry.getKey()); continue; } + + boolean continueOnFailure = entry.getValue().stream() + .filter(a -> !a.isContinueOnFailure()) + .findAny().isEmpty(); List> visitors = entry.getValue().stream() .map(BytecodeTransformerBuildItem::getVisitorFunction).filter(Objects::nonNull) .collect(Collectors.toList()); @@ -196,9 +214,9 @@ public byte[] apply(String className, byte[] originalBytes) { public TransformedClassesBuildItem.TransformedClass call() throws Exception { ClassLoader old = Thread.currentThread().getContextClassLoader(); try { + byte[] classData = classPathElement.getResource(classFileName).getData(); Thread.currentThread().setContextClassLoader(transformCl); Set constValues = constScanning.get(className); - byte[] classData = classPathElement.getResource(classFileName).getData(); if (constValues != null && !noConstScanning.contains(className)) { if (!ConstPoolScanner.constPoolEntryPresent(classData, constValues)) { return null; @@ -214,6 +232,17 @@ public TransformedClassesBuildItem.TransformedClass call() throws Exception { transformedClassesCache.put(className, transformedClass); } return transformedClass; + } catch (Throwable e) { + if (continueOnFailure) { + if (log.isDebugEnabled()) { + log.errorf(e, "Failed to transform %s", className); + } else { + log.errorf("Failed to transform %s", className); + } + return null; + } else { + throw e; + } } finally { Thread.currentThread().setContextClassLoader(old); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java index 167849ed1c349..be3f6406bcebb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java @@ -200,9 +200,7 @@ private static void reportUnknownBuildProperties(LaunchMode launchMode, Set(unknownBuildProperties)); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/ExecUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/ExecUtil.java index 88d7c9b52d8da..51bd4dd8cb9df 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/ExecUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/ExecUtil.java @@ -6,6 +6,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.time.Duration; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import org.jboss.logging.Logger; @@ -17,6 +19,8 @@ public class ExecUtil { private static final Function PRINT_OUTPUT = i -> new HandleOutput(i); private static final Function SILENT = i -> new HandleOutput(i, Logger.Level.DEBUG); + private static final int PROCESS_CHECK_INTERVAL = 500; + private static class HandleOutput implements Runnable { private final InputStream is; @@ -60,6 +64,18 @@ public static boolean exec(String command, String... args) { return exec(new File("."), command, args); } + /** + * Execute the specified command until the given timeout from within the current directory. + * + * @param timeout The timeout + * @param command The command + * @param args The command arguments + * @return true if commands where executed successfully + */ + public static boolean execWithTimeout(Duration timeout, String command, String... args) { + return execWithTimeout(new File("."), timeout, command, args); + } + /** * Execute the specified command from within the current directory and hide the output. * @@ -71,6 +87,18 @@ public static boolean execSilent(String command, String... args) { return execSilent(new File("."), command, args); } + /** + * Execute the specified command until the given timeout from within the current directory and hide the output. + * + * @param timeout The timeout + * @param command The command + * @param args The command arguments + * @return true if commands where executed successfully + */ + public static boolean execSilentWithTimeout(Duration timeout, String command, String... args) { + return execSilentWithTimeout(new File("."), timeout, command, args); + } + /** * Execute the specified command from within the specified directory. * @@ -83,6 +111,19 @@ public static boolean exec(File directory, String command, String... args) { return exec(directory, PRINT_OUTPUT, command, args); } + /** + * Execute the specified command until the given timeout from within the specified directory. + * + * @param directory The directory + * @param timeout The timeout + * @param command The command + * @param args The command arguments + * @return true if commands where executed successfully + */ + public static boolean execWithTimeout(File directory, Duration timeout, String command, String... args) { + return execWithTimeout(directory, PRINT_OUTPUT, timeout, command, args); + } + /** * Execute the specified command from within the specified directory and hide the output. * @@ -95,6 +136,19 @@ public static boolean execSilent(File directory, String command, String... args) return exec(directory, SILENT, command, args); } + /** + * Execute the specified command until the given timeout from within the specified directory and hide the output. + * + * @param directory The directory + * @param timeout The timeout + * @param command The command + * @param args The command arguments + * @return true if commands where executed successfully + */ + public static boolean execSilentWithTimeout(File directory, Duration timeout, String command, String... args) { + return execWithTimeout(directory, SILENT, timeout, command, args); + } + /** * Execute the specified command from within the specified directory. * The method allows specifying an output filter that processes the command output. @@ -107,27 +161,87 @@ public static boolean execSilent(File directory, String command, String... args) */ public static boolean exec(File directory, Function outputFilterFunction, String command, String... args) { - Process process = null; + try { + Process process = startProcess(directory, command, args); + outputFilterFunction.apply(process.getInputStream()); + process.waitFor(); + return process.exitValue() == 0; + } catch (InterruptedException e) { + return false; + } + } + + /** + * Execute the specified command until the given timeout from within the specified directory. + * The method allows specifying an output filter that processes the command output. + * + * @param directory The directory + * @param outputFilterFunction A {@link Function} that gets an {@link InputStream} and returns an outputFilter. + * @param timeout The timeout + * @param command The command + * @param args The command arguments + * @return true if commands where executed successfully + */ + public static boolean execWithTimeout(File directory, Function outputFilterFunction, + Duration timeout, String command, String... args) { + try { + Process process = startProcess(directory, command, args); + Thread t = new Thread(outputFilterFunction.apply(process.getInputStream())); + t.setName("Process stdout"); + t.setDaemon(true); + t.start(); + process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS); + destroyProcess(process); + return process.exitValue() == 0; + } catch (InterruptedException e) { + return false; + } + } + + /** + * Start a process executing given command with arguments within the specified directory. + * + * @param directory The directory + * @param command The command + * @param args The command arguments + * @return the process + */ + public static Process startProcess(File directory, String command, String... args) { try { String[] cmd = new String[args.length + 1]; cmd[0] = command; if (args.length > 0) { System.arraycopy(args, 0, cmd, 1, args.length); } - process = new ProcessBuilder() + return new ProcessBuilder() .directory(directory) .command(cmd) .redirectErrorStream(true) .start(); - - outputFilterFunction.apply(process.getInputStream()).run(); - process.waitFor(); } catch (IOException e) { throw new RuntimeException("Input/Output error while executing command.", e); - } catch (InterruptedException e) { - return false; } - return process != null && process.exitValue() == 0; + } + + /** + * Kill the process, if still alive, kill it forcibly + * + * @param process the process to kill + */ + public static void destroyProcess(Process process) { + process.destroy(); + int i = 0; + while (process.isAlive() && i++ < 10) { + try { + process.waitFor(PROCESS_CHECK_INTERVAL, TimeUnit.MILLISECONDS); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + + if (process.isAlive()) { + process.destroyForcibly(); + } } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/FileUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/FileUtil.java index 9916dd8092978..711072d50b166 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/FileUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/FileUtil.java @@ -13,10 +13,29 @@ public class FileUtil { + public static void deleteIfExists(final Path path) throws IOException { + BasicFileAttributes attributes; + try { + attributes = Files.readAttributes(path, BasicFileAttributes.class); + } catch (IOException ignored) { + // Files.isDirectory is also simply returning when any IOException occurs, same behaviour is fine + return; + } + if (attributes.isDirectory()) { + deleteDirectoryIfExists(path); + } else if (attributes.isRegularFile()) { + Files.deleteIfExists(path); + } + } + public static void deleteDirectory(final Path directory) throws IOException { if (!Files.isDirectory(directory)) { return; } + deleteDirectoryIfExists(directory); + } + + private static void deleteDirectoryIfExists(final Path directory) throws IOException { Files.walkFileTree(directory, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/NativeConfigTest.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/NativeConfigTest.java index 640667b990cf3..2ac183ef4b1e6 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/pkg/NativeConfigTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/NativeConfigTest.java @@ -8,22 +8,32 @@ class NativeConfigTest { @Test public void testBuilderImageProperlyDetected() { + assertThat(createConfig("graalvm").getEffectiveBuilderImage(false)).contains("ubi-quarkus-native-image") + .contains("java11"); + assertThat(createConfig("graalvm").getEffectiveBuilderImage(true)).contains("ubi-quarkus-native-image") + .contains("java17"); + assertThat(createConfig("GraalVM").getEffectiveBuilderImage(true)).contains("ubi-quarkus-native-image") + .contains("java17"); + assertThat(createConfig("GraalVM").getEffectiveBuilderImage(true)).contains("ubi-quarkus-native-image") + .contains("java17"); + assertThat(createConfig("GRAALVM").getEffectiveBuilderImage(false)).contains("ubi-quarkus-native-image") + .contains("java11"); + assertThat(createConfig("GRAALVM").getEffectiveBuilderImage(true)).contains("ubi-quarkus-native-image") + .contains("java17"); + + assertThat(createConfig("mandrel").getEffectiveBuilderImage(false)).contains("ubi-quarkus-mandrel").contains("java11"); + assertThat(createConfig("mandrel").getEffectiveBuilderImage(true)).contains("ubi-quarkus-mandrel").contains("java17"); + assertThat(createConfig("Mandrel").getEffectiveBuilderImage(false)).contains("ubi-quarkus-mandrel").contains("java11"); + assertThat(createConfig("Mandrel").getEffectiveBuilderImage(true)).contains("ubi-quarkus-mandrel").contains("java17"); + assertThat(createConfig("MANDREL").getEffectiveBuilderImage(false)).contains("ubi-quarkus-mandrel").contains("java11"); + assertThat(createConfig("MANDREL").getEffectiveBuilderImage(true)).contains("ubi-quarkus-mandrel").contains("java17"); + + assertThat(createConfig("aRandomString").getEffectiveBuilderImage(false)).isEqualTo("aRandomString"); + } + + private NativeConfig createConfig(String configValue) { NativeConfig nativeConfig = new NativeConfig(); - nativeConfig.builderImage = "graalvm"; - assertThat(nativeConfig.getEffectiveBuilderImage().contains("ubi-quarkus-native-image")).isTrue(); - nativeConfig.builderImage = "GraalVM"; - assertThat(nativeConfig.getEffectiveBuilderImage().contains("ubi-quarkus-native-image")).isTrue(); - nativeConfig.builderImage = "GRAALVM"; - assertThat(nativeConfig.getEffectiveBuilderImage().contains("ubi-quarkus-native-image")).isTrue(); - nativeConfig.builderImage = "mandrel"; - assertThat(nativeConfig.getEffectiveBuilderImage().contains("ubi-quarkus-mandrel")).isTrue(); - nativeConfig.builderImage = "Mandrel"; - assertThat(nativeConfig.getEffectiveBuilderImage().contains("ubi-quarkus-mandrel")).isTrue(); - nativeConfig.builderImage = "MANDREL"; - assertThat(nativeConfig.getEffectiveBuilderImage().contains("ubi-quarkus-mandrel")).isTrue(); - nativeConfig.builderImage = "aRandomString"; - assertThat(nativeConfig.getEffectiveBuilderImage().contains("aRandomString")).isTrue(); - nativeConfig.builderImage = "aRandomStr32ng"; - assertThat(nativeConfig.getEffectiveBuilderImage().contains("aRandomString")).isFalse(); + nativeConfig.builderImage = configValue; + return nativeConfig; } } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java index 200352653c202..9dc768340e95f 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java @@ -7,11 +7,15 @@ import java.util.Optional; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfSystemProperty; import io.quarkus.deployment.pkg.NativeConfig; +import io.quarkus.deployment.pkg.builditem.CompiledJavaVersionBuildItem; class NativeImageBuildContainerRunnerTest { + // This will default to false in the maven build and true in the IDE, so this will still run if invoked explicitly + @DisabledIfSystemProperty(named = "avoid-containers", matches = "true") @Test void testBuilderImageBeingPickedUp() { NativeConfig nativeConfig = new NativeConfig(); @@ -21,7 +25,8 @@ void testBuilderImageBeingPickedUp() { String[] command; nativeConfig.builderImage = "graalvm"; - localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp")); + localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp"), + CompiledJavaVersionBuildItem.JavaVersion.Unknown.INSTANCE); command = localRunner.buildCommand("docker", Collections.emptyList(), Collections.emptyList()); found = false; for (String part : command) { @@ -32,7 +37,8 @@ void testBuilderImageBeingPickedUp() { assertThat(found).isTrue(); nativeConfig.builderImage = "mandrel"; - localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp")); + localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp"), + CompiledJavaVersionBuildItem.JavaVersion.Unknown.INSTANCE); command = localRunner.buildCommand("docker", Collections.emptyList(), Collections.emptyList()); found = false; for (String part : command) { @@ -43,7 +49,8 @@ void testBuilderImageBeingPickedUp() { assertThat(found).isTrue(); nativeConfig.builderImage = "RandomString"; - localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp")); + localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp"), + CompiledJavaVersionBuildItem.JavaVersion.Unknown.INSTANCE); command = localRunner.buildCommand("docker", Collections.emptyList(), Collections.emptyList()); found = false; for (String part : command) { diff --git a/core/devmode-spi/src/main/java/io/quarkus/dev/appstate/ApplicationStartException.java b/core/devmode-spi/src/main/java/io/quarkus/dev/appstate/ApplicationStartException.java new file mode 100644 index 0000000000000..8d62afe8e78df --- /dev/null +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/appstate/ApplicationStartException.java @@ -0,0 +1,14 @@ +package io.quarkus.dev.appstate; + +/** + * Exception that is reported if the application fails to start + * + * This exception has already been logged when this exception is generated, + * so should not be logged again + */ +public class ApplicationStartException extends RuntimeException { + + public ApplicationStartException(Throwable cause) { + super(cause); + } +} diff --git a/core/devmode-spi/src/main/java/io/quarkus/dev/appstate/ApplicationStateNotification.java b/core/devmode-spi/src/main/java/io/quarkus/dev/appstate/ApplicationStateNotification.java index 287cd44fabddd..37d963e37ea35 100644 --- a/core/devmode-spi/src/main/java/io/quarkus/dev/appstate/ApplicationStateNotification.java +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/appstate/ApplicationStateNotification.java @@ -33,6 +33,13 @@ public static synchronized void notifyApplicationStopped() { ApplicationStateNotification.class.notifyAll(); } + /** + * Notify of startup failure. + * + * Before this method is called the exception should be logged. + * + * @param t The exception + */ public static synchronized void notifyStartupFailed(Throwable t) { startupProblem = t; state = State.STOPPED; @@ -48,7 +55,7 @@ public static synchronized void waitForApplicationStart() { } } if (startupProblem != null) { - throw new RuntimeException(startupProblem); + throw new ApplicationStartException(startupProblem); } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/Constants.java b/core/processor/src/main/java/io/quarkus/annotation/processor/Constants.java index 39e80c573252b..5cb5214cd72a1 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/Constants.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/Constants.java @@ -23,6 +23,7 @@ final public class Constants { }; public static final char DOT = '.'; + public static final String CODE_DELIMITER = "`"; public static final String EMPTY = ""; public static final String DASH = "-"; public static final String ADOC_EXTENSION = ".adoc"; @@ -114,4 +115,9 @@ final public class Constants { "If no suffix is given, assume bytes.\n" + "====\n"; + /** + * Tooltip is custom AsciiDoc inline macro that transforms inputs to a CSS Tooltip. + */ + public static final String TOOLTIP = "tooltip:%s[%s]"; + } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java index 09a61691b41ab..4c1e464be260c 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java @@ -409,6 +409,13 @@ private void recordConfigJavadoc(TypeElement clazz) { } break; } + case ENUM: + e + .getEnclosedElements() + .stream() + .filter(e1 -> e1.getKind() == ElementKind.ENUM_CONSTANT) + .forEach(ec -> processEnumConstant(ec, javadocProps, className)); + break; default: } } @@ -465,6 +472,13 @@ private void processFieldConfigItem(VariableElement field, Properties javadocPro javadocProps.put(className + Constants.DOT + field.getSimpleName().toString(), getRequiredJavadoc(field)); } + private void processEnumConstant(Element field, Properties javadocProps, String className) { + String javaDoc = getJavadoc(field); + if (javaDoc != null && !javaDoc.isBlank()) { + javadocProps.put(className + Constants.DOT + field.getSimpleName().toString(), javaDoc); + } + } + private void processCtorConfigItem(ExecutableElement ctor, Properties javadocProps, String className) { final String docComment = getRequiredJavadoc(ctor); final StringBuilder buf = new StringBuilder(); @@ -703,13 +717,22 @@ private void appendParamTypes(ExecutableElement ex, final StringBuilder buf) { } private String getRequiredJavadoc(Element e) { - String docComment = processingEnv.getElementUtils().getDocComment(e); + String javaDoc = getJavadoc(e); - if (docComment == null) { + if (javaDoc == null) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Unable to find javadoc for config item " + e.getEnclosingElement() + " " + e, e); return ""; } + return javaDoc; + } + + private String getJavadoc(Element e) { + String docComment = processingEnv.getElementUtils().getDocComment(e); + + if (docComment == null) { + return null; + } // javax.lang.model keeps the leading space after the "*" so we need to remove it. diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDoItemFinder.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDoItemFinder.java index 577e5774512cd..97fabbccfa6ae 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDoItemFinder.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDoItemFinder.java @@ -46,6 +46,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; +import io.quarkus.annotation.processor.Constants; import io.quarkus.annotation.processor.generate_doc.JavaDocParser.SectionHolder; class ConfigDoItemFinder { @@ -57,6 +58,7 @@ class ConfigDoItemFinder { Arrays.asList("byte", "short", "int", "long", "float", "double", "boolean", "char")); private final JavaDocParser javaDocParser = new JavaDocParser(); + private final JavaDocParser enumJavaDocParser = new JavaDocParser(true); private final ScannedConfigDocsItemHolder holder = new ScannedConfigDocsItemHolder(); private final Set configRoots; @@ -327,7 +329,9 @@ private List recursivelyFindConfigItems(Element element, String r .map(defaultEnumValue -> hyphenateEnumValue(defaultEnumValue.trim())) .collect(Collectors.joining(COMMA)); } - acceptedValues = extractEnumValues(realTypeMirror, useHyphenateEnumValue); + acceptedValues = extractEnumValues(realTypeMirror, useHyphenateEnumValue, + clazz.getQualifiedName().toString()); + configDocKey.setEnum(true); } } } else { @@ -336,7 +340,9 @@ private List recursivelyFindConfigItems(Element element, String r if (useHyphenateEnumValue) { defaultValue = hyphenateEnumValue(defaultValue); } - acceptedValues = extractEnumValues(declaredType, useHyphenateEnumValue); + acceptedValues = extractEnumValues(declaredType, useHyphenateEnumValue, + clazz.getQualifiedName().toString()); + configDocKey.setEnum(true); } else if (isDurationType(declaredType) && !defaultValue.isEmpty()) { defaultValue = DocGeneratorUtil.normalizeDurationValue(defaultValue); } @@ -394,14 +400,27 @@ private String simpleTypeToString(TypeMirror typeMirror) { return typeMirror.toString(); } - private List extractEnumValues(TypeMirror realTypeMirror, boolean useHyphenatedEnumValue) { + private List extractEnumValues(TypeMirror realTypeMirror, boolean useHyphenatedEnumValue, String javaDocKey) { Element declaredTypeElement = ((DeclaredType) realTypeMirror).asElement(); List acceptedValues = new ArrayList<>(); for (Element field : declaredTypeElement.getEnclosedElements()) { if (field.getKind() == ElementKind.ENUM_CONSTANT) { String enumValue = field.getSimpleName().toString(); - acceptedValues.add(useHyphenatedEnumValue ? hyphenateEnumValue(enumValue) : enumValue); + + // Find enum constant description + final String constantJavaDocKey = javaDocKey + DOT + enumValue; + final String rawJavaDoc = javaDocProperties.getProperty(constantJavaDocKey); + + enumValue = useHyphenatedEnumValue ? hyphenateEnumValue(enumValue) : enumValue; + if (rawJavaDoc != null && !rawJavaDoc.isBlank()) { + // Show enum constant description as a Tooltip + String javaDoc = enumJavaDocParser.parseConfigDescription(rawJavaDoc); + acceptedValues.add(String.format(Constants.TOOLTIP, enumValue, javaDoc)); + } else { + acceptedValues.add(Constants.CODE_DELIMITER + + enumValue + Constants.CODE_DELIMITER); + } } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKey.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKey.java index ad30c610afc71..f961b4399b2ba 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKey.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocKey.java @@ -24,6 +24,7 @@ final public class ConfigDocKey implements ConfigDocElement, Comparable acceptedValues) { return ""; } - return acceptedValues.stream().collect(Collectors.joining("`, `", "`", "`")); + return acceptedValues.stream().collect(Collectors.joining("`, `", Constants.CODE_DELIMITER, Constants.CODE_DELIMITER)); + } + + static String joinEnumValues(List enumValues) { + if (enumValues == null || enumValues.isEmpty()) { + return Constants.EMPTY; + } + + // nested macros are only detected when cell starts with a new line, e.g. a|\n myMacro::[] + return NEW_LINE + String.join(", ", enumValues); } static String getTypeFormatInformationNote(ConfigDocKey configDocKey) { diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/JavaDocParser.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/JavaDocParser.java index 1b54e370e6e8d..3d590afcf8ae5 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/JavaDocParser.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/JavaDocParser.java @@ -61,6 +61,16 @@ final class JavaDocParser { private static final String UNDERLINE_ASCIDOC_STYLE = "[.underline]"; private static final String LINE_THROUGH_ASCIDOC_STYLE = "[.line-through]"; + private final boolean inlineMacroMode; + + public JavaDocParser(boolean inlineMacroMode) { + this.inlineMacroMode = inlineMacroMode; + } + + public JavaDocParser() { + this(false); + } + public String parseConfigDescription(String javadocComment) { if (javadocComment == null || javadocComment.trim().isEmpty()) { return Constants.EMPTY; @@ -268,18 +278,28 @@ private void appendHtml(StringBuilder sb, Node node) { } } - static StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text) { + private StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text) { boolean escaping = false; for (int i = 0; i < text.length(); i++) { final char ch = text.charAt(i); switch (ch) { + case ']': + // don't escape closing square bracket in the attribute list of an inline element with passThrough + // https://docs.asciidoctor.org/asciidoc/latest/attributes/positional-and-named-attributes/#substitutions + if (inlineMacroMode) { + if (escaping) { + sb.append("++"); + escaping = false; + } + sb.append("]"); + break; + } case '#': case '*': case '\\': case '{': case '}': case '[': - case ']': case '|': if (!escaping) { sb.append("++"); diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/SummaryTableDocFormatter.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/SummaryTableDocFormatter.java index 031a89066b06f..b2d3feda5c680 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/SummaryTableDocFormatter.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/SummaryTableDocFormatter.java @@ -10,7 +10,7 @@ final class SummaryTableDocFormatter implements DocFormatter { private static final String TABLE_CLOSING_TAG = "\n|==="; public static final String SEARCHABLE_TABLE_CLASS = ".searchable"; // a css class indicating if a table is searchable public static final String CONFIGURATION_TABLE_CLASS = ".configuration-reference"; - private static final String TABLE_ROW_FORMAT = "\n\na|%s [[%s]]`link:#%s[%s]`\n\n[.description]\n--\n%s\n--|%s %s\n|%s\n"; + private static final String TABLE_ROW_FORMAT = "\n\na|%s [[%s]]`link:#%s[%s]`\n\n[.description]\n--\n%s\n--%s|%s %s\n|%s\n"; private static final String SECTION_TITLE = "[[%s]]link:#%s[%s]"; private static final String TABLE_SECTION_ROW_FORMAT = "\n\nh|%s\n%s\nh|Type\nh|Default"; private static final String TABLE_HEADER_FORMAT = "[.configuration-legend]%s\n[%s, cols=\"80,.^10,.^10\"]\n|==="; @@ -53,7 +53,11 @@ public void format(Writer writer, String initialAnchorPrefix, boolean activateSe public void format(Writer writer, ConfigDocKey configDocKey) throws IOException { String typeContent = ""; if (configDocKey.hasAcceptedValues()) { - typeContent = DocGeneratorUtil.joinAcceptedValues(configDocKey.getAcceptedValues()); + if (configDocKey.isEnum()) { + typeContent = DocGeneratorUtil.joinEnumValues(configDocKey.getAcceptedValues()); + } else { + typeContent = DocGeneratorUtil.joinAcceptedValues(configDocKey.getAcceptedValues()); + } } else if (configDocKey.hasType()) { typeContent = configDocKey.computeTypeSimpleName(); final String javaDocLink = configDocKey.getJavaDocSiteLink(); @@ -84,6 +88,8 @@ public void format(Writer writer, ConfigDocKey configDocKey) throws IOException key, // make sure nobody inserts a table cell separator here doc.replace("|", "\\|"), + // if ConfigDocKey is enum, cell style operator must support block elements + configDocKey.isEnum() ? " a" : Constants.EMPTY, typeContent, typeDetail, defaultValue.isEmpty() ? required : String.format("`%s`", defaultValue.replace("|", "\\|") diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/JavaDocConfigDescriptionParserTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/JavaDocConfigDescriptionParserTest.java index fc7ada22d847f..292162160a273 100644 --- a/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/JavaDocConfigDescriptionParserTest.java +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/generate_doc/JavaDocConfigDescriptionParserTest.java @@ -249,11 +249,28 @@ public void asciidocLists() { public void escape(String ch) { final String javaDoc = "Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch + ", {@code JavaDoc tag " + ch + " " + ch + ch + "}"; + + final String asciiDoc = parser.parseConfigDescription(javaDoc); + final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); final String expected = "

\n

Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch + ", JavaDoc tag " + ch + " " + ch + ch + "

\n
"; + assertEquals(expected, actual); + } - final String asciiDoc = parser.parseConfigDescription(javaDoc); + @ParameterizedTest + @ValueSource(strings = { "#", "*", "\\", "[", "]", "|" }) + public void escapeInsideInlineElement(String ch) { + final String javaDoc = "Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch + + ", {@code JavaDoc tag " + ch + " " + ch + ch + "}"; + + final String asciiDoc = new JavaDocParser(true).parseConfigDescription(javaDoc); final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); + + if (ch.equals("]")) { + ch = "]"; + } + final String expected = "
\n

Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + + " " + ch + ch + ", JavaDoc tag " + ch + " " + ch + ch + "

\n
"; assertEquals(expected, actual); } diff --git a/core/runtime/pom.xml b/core/runtime/pom.xml index 907d8d0ce36ff..2ce5f62185477 100644 --- a/core/runtime/pom.xml +++ b/core/runtime/pom.xml @@ -210,7 +210,6 @@ au.com.dius.pact.core:matcher au.com.dius.pact.consumer:junit5 au.com.dius.pact:consumer - org.graalvm.sdk:graal-sdk @@ -223,6 +222,8 @@ org.wildfly.common:wildfly-common io.smallrye.common:smallrye-common-io + + io.github.crac:org-crac io.smallrye:smallrye-config diff --git a/core/runtime/src/main/java/io/quarkus/logging/Log.java b/core/runtime/src/main/java/io/quarkus/logging/Log.java index e72b9977f489e..2c0132299f03b 100644 --- a/core/runtime/src/main/java/io/quarkus/logging/Log.java +++ b/core/runtime/src/main/java/io/quarkus/logging/Log.java @@ -8,6 +8,7 @@ * of the same methods on a generated instance of {@link Logger}. */ public final class Log { + // automatically generated by io.quarkus.logging.GenerateLog /** * Check to see if the given level is enabled for this logger. @@ -16,7 +17,10 @@ public final class Log { * @return {@code true} if messages may be logged at the given level, {@code false} otherwise */ public static boolean isEnabled(Logger.Level level) { - throw fail(); + if (always()) { + throw fail(); + } + return always(); } /** @@ -26,7 +30,10 @@ public static boolean isEnabled(Logger.Level level) { * otherwise */ public static boolean isTraceEnabled() { - throw fail(); + if (always()) { + throw fail(); + } + return always(); } /** @@ -35,7 +42,9 @@ public static boolean isTraceEnabled() { * @param message the message */ public static void trace(Object message) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -45,7 +54,9 @@ public static void trace(Object message) { * @param t the throwable */ public static void trace(Object message, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -56,7 +67,9 @@ public static void trace(Object message, Throwable t) { * @param t the throwable */ public static void trace(String loggerFqcn, Object message, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -68,7 +81,9 @@ public static void trace(String loggerFqcn, Object message, Throwable t) { * @param t the throwable */ public static void trace(String loggerFqcn, Object message, Object[] params, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -78,7 +93,9 @@ public static void trace(String loggerFqcn, Object message, Object[] params, Thr * @param params the parameters */ public static void tracev(String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -88,7 +105,9 @@ public static void tracev(String format, Object... params) { * @param param1 the sole parameter */ public static void tracev(String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -99,7 +118,9 @@ public static void tracev(String format, Object param1) { * @param param2 the second parameter */ public static void tracev(String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -111,7 +132,9 @@ public static void tracev(String format, Object param1, Object param2) { * @param param3 the third parameter */ public static void tracev(String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -122,7 +145,9 @@ public static void tracev(String format, Object param1, Object param2, Object pa * @param params the parameters */ public static void tracev(Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -133,7 +158,9 @@ public static void tracev(Throwable t, String format, Object... params) { * @param param1 the sole parameter */ public static void tracev(Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -145,7 +172,9 @@ public static void tracev(Throwable t, String format, Object param1) { * @param param2 the second parameter */ public static void tracev(Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -158,7 +187,9 @@ public static void tracev(Throwable t, String format, Object param1, Object para * @param param3 the third parameter */ public static void tracev(Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -168,7 +199,9 @@ public static void tracev(Throwable t, String format, Object param1, Object para * @param params the parameters */ public static void tracef(String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -178,7 +211,9 @@ public static void tracef(String format, Object... params) { * @param param1 the sole parameter */ public static void tracef(String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -189,7 +224,9 @@ public static void tracef(String format, Object param1) { * @param param2 the second parameter */ public static void tracef(String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -201,7 +238,9 @@ public static void tracef(String format, Object param1, Object param2) { * @param param3 the third parameter */ public static void tracef(String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -212,7 +251,9 @@ public static void tracef(String format, Object param1, Object param2, Object pa * @param params the parameters */ public static void tracef(Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -223,7 +264,9 @@ public static void tracef(Throwable t, String format, Object... params) { * @param param1 the sole parameter */ public static void tracef(Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -235,7 +278,9 @@ public static void tracef(Throwable t, String format, Object param1) { * @param param2 the second parameter */ public static void tracef(Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -248,7 +293,9 @@ public static void tracef(Throwable t, String format, Object param1, Object para * @param param3 the third parameter */ public static void tracef(Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -258,7 +305,9 @@ public static void tracef(Throwable t, String format, Object param1, Object para * @param arg the parameter */ public static void tracef(String format, int arg) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -269,7 +318,9 @@ public static void tracef(String format, int arg) { * @param arg2 the second parameter */ public static void tracef(String format, int arg1, int arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -280,7 +331,9 @@ public static void tracef(String format, int arg1, int arg2) { * @param arg2 the second parameter */ public static void tracef(String format, int arg1, Object arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -292,7 +345,9 @@ public static void tracef(String format, int arg1, Object arg2) { * @param arg3 the third parameter */ public static void tracef(String format, int arg1, int arg2, int arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -304,7 +359,9 @@ public static void tracef(String format, int arg1, int arg2, int arg3) { * @param arg3 the third parameter */ public static void tracef(String format, int arg1, int arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -316,7 +373,9 @@ public static void tracef(String format, int arg1, int arg2, Object arg3) { * @param arg3 the third parameter */ public static void tracef(String format, int arg1, Object arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -327,7 +386,9 @@ public static void tracef(String format, int arg1, Object arg2, Object arg3) { * @param arg the parameter */ public static void tracef(Throwable t, String format, int arg) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -339,7 +400,9 @@ public static void tracef(Throwable t, String format, int arg) { * @param arg2 the second parameter */ public static void tracef(Throwable t, String format, int arg1, int arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -351,7 +414,9 @@ public static void tracef(Throwable t, String format, int arg1, int arg2) { * @param arg2 the second parameter */ public static void tracef(Throwable t, String format, int arg1, Object arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -364,7 +429,9 @@ public static void tracef(Throwable t, String format, int arg1, Object arg2) { * @param arg3 the third parameter */ public static void tracef(Throwable t, String format, int arg1, int arg2, int arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -377,7 +444,9 @@ public static void tracef(Throwable t, String format, int arg1, int arg2, int ar * @param arg3 the third parameter */ public static void tracef(Throwable t, String format, int arg1, int arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -390,7 +459,9 @@ public static void tracef(Throwable t, String format, int arg1, int arg2, Object * @param arg3 the third parameter */ public static void tracef(Throwable t, String format, int arg1, Object arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -400,7 +471,9 @@ public static void tracef(Throwable t, String format, int arg1, Object arg2, Obj * @param arg the parameter */ public static void tracef(String format, long arg) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -411,7 +484,9 @@ public static void tracef(String format, long arg) { * @param arg2 the second parameter */ public static void tracef(String format, long arg1, long arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -422,7 +497,9 @@ public static void tracef(String format, long arg1, long arg2) { * @param arg2 the second parameter */ public static void tracef(String format, long arg1, Object arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -434,7 +511,9 @@ public static void tracef(String format, long arg1, Object arg2) { * @param arg3 the third parameter */ public static void tracef(String format, long arg1, long arg2, long arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -446,7 +525,9 @@ public static void tracef(String format, long arg1, long arg2, long arg3) { * @param arg3 the third parameter */ public static void tracef(String format, long arg1, long arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -458,7 +539,9 @@ public static void tracef(String format, long arg1, long arg2, Object arg3) { * @param arg3 the third parameter */ public static void tracef(String format, long arg1, Object arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -469,7 +552,9 @@ public static void tracef(String format, long arg1, Object arg2, Object arg3) { * @param arg the parameter */ public static void tracef(Throwable t, String format, long arg) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -481,7 +566,9 @@ public static void tracef(Throwable t, String format, long arg) { * @param arg2 the second parameter */ public static void tracef(Throwable t, String format, long arg1, long arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -493,7 +580,9 @@ public static void tracef(Throwable t, String format, long arg1, long arg2) { * @param arg2 the second parameter */ public static void tracef(Throwable t, String format, long arg1, Object arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -506,7 +595,9 @@ public static void tracef(Throwable t, String format, long arg1, Object arg2) { * @param arg3 the third parameter */ public static void tracef(Throwable t, String format, long arg1, long arg2, long arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -519,7 +610,9 @@ public static void tracef(Throwable t, String format, long arg1, long arg2, long * @param arg3 the third parameter */ public static void tracef(Throwable t, String format, long arg1, long arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -532,7 +625,9 @@ public static void tracef(Throwable t, String format, long arg1, long arg2, Obje * @param arg3 the third parameter */ public static void tracef(Throwable t, String format, long arg1, Object arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -542,7 +637,10 @@ public static void tracef(Throwable t, String format, long arg1, Object arg2, Ob * otherwise */ public static boolean isDebugEnabled() { - throw fail(); + if (always()) { + throw fail(); + } + return always(); } /** @@ -551,7 +649,9 @@ public static boolean isDebugEnabled() { * @param message the message */ public static void debug(Object message) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -561,7 +661,9 @@ public static void debug(Object message) { * @param t the throwable */ public static void debug(Object message, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -572,7 +674,9 @@ public static void debug(Object message, Throwable t) { * @param t the throwable */ public static void debug(String loggerFqcn, Object message, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -584,7 +688,9 @@ public static void debug(String loggerFqcn, Object message, Throwable t) { * @param t the throwable */ public static void debug(String loggerFqcn, Object message, Object[] params, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -594,7 +700,9 @@ public static void debug(String loggerFqcn, Object message, Object[] params, Thr * @param params the parameters */ public static void debugv(String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -604,7 +712,9 @@ public static void debugv(String format, Object... params) { * @param param1 the sole parameter */ public static void debugv(String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -615,7 +725,9 @@ public static void debugv(String format, Object param1) { * @param param2 the second parameter */ public static void debugv(String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -627,7 +739,9 @@ public static void debugv(String format, Object param1, Object param2) { * @param param3 the third parameter */ public static void debugv(String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -638,7 +752,9 @@ public static void debugv(String format, Object param1, Object param2, Object pa * @param params the parameters */ public static void debugv(Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -649,7 +765,9 @@ public static void debugv(Throwable t, String format, Object... params) { * @param param1 the sole parameter */ public static void debugv(Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -661,7 +779,9 @@ public static void debugv(Throwable t, String format, Object param1) { * @param param2 the second parameter */ public static void debugv(Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -674,7 +794,9 @@ public static void debugv(Throwable t, String format, Object param1, Object para * @param param3 the third parameter */ public static void debugv(Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -684,7 +806,9 @@ public static void debugv(Throwable t, String format, Object param1, Object para * @param params the parameters */ public static void debugf(String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -694,7 +818,9 @@ public static void debugf(String format, Object... params) { * @param param1 the sole parameter */ public static void debugf(String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -705,7 +831,9 @@ public static void debugf(String format, Object param1) { * @param param2 the second parameter */ public static void debugf(String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -717,7 +845,9 @@ public static void debugf(String format, Object param1, Object param2) { * @param param3 the third parameter */ public static void debugf(String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -728,7 +858,9 @@ public static void debugf(String format, Object param1, Object param2, Object pa * @param params the parameters */ public static void debugf(Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -739,7 +871,9 @@ public static void debugf(Throwable t, String format, Object... params) { * @param param1 the sole parameter */ public static void debugf(Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -751,7 +885,9 @@ public static void debugf(Throwable t, String format, Object param1) { * @param param2 the second parameter */ public static void debugf(Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -764,7 +900,9 @@ public static void debugf(Throwable t, String format, Object param1, Object para * @param param3 the third parameter */ public static void debugf(Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -774,7 +912,9 @@ public static void debugf(Throwable t, String format, Object param1, Object para * @param arg the parameter */ public static void debugf(String format, int arg) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -785,7 +925,9 @@ public static void debugf(String format, int arg) { * @param arg2 the second parameter */ public static void debugf(String format, int arg1, int arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -796,7 +938,9 @@ public static void debugf(String format, int arg1, int arg2) { * @param arg2 the second parameter */ public static void debugf(String format, int arg1, Object arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -808,7 +952,9 @@ public static void debugf(String format, int arg1, Object arg2) { * @param arg3 the third parameter */ public static void debugf(String format, int arg1, int arg2, int arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -820,7 +966,9 @@ public static void debugf(String format, int arg1, int arg2, int arg3) { * @param arg3 the third parameter */ public static void debugf(String format, int arg1, int arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -832,7 +980,9 @@ public static void debugf(String format, int arg1, int arg2, Object arg3) { * @param arg3 the third parameter */ public static void debugf(String format, int arg1, Object arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -843,7 +993,9 @@ public static void debugf(String format, int arg1, Object arg2, Object arg3) { * @param arg the parameter */ public static void debugf(Throwable t, String format, int arg) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -855,7 +1007,9 @@ public static void debugf(Throwable t, String format, int arg) { * @param arg2 the second parameter */ public static void debugf(Throwable t, String format, int arg1, int arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -867,7 +1021,9 @@ public static void debugf(Throwable t, String format, int arg1, int arg2) { * @param arg2 the second parameter */ public static void debugf(Throwable t, String format, int arg1, Object arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -880,7 +1036,9 @@ public static void debugf(Throwable t, String format, int arg1, Object arg2) { * @param arg3 the third parameter */ public static void debugf(Throwable t, String format, int arg1, int arg2, int arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -893,7 +1051,9 @@ public static void debugf(Throwable t, String format, int arg1, int arg2, int ar * @param arg3 the third parameter */ public static void debugf(Throwable t, String format, int arg1, int arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -906,7 +1066,9 @@ public static void debugf(Throwable t, String format, int arg1, int arg2, Object * @param arg3 the third parameter */ public static void debugf(Throwable t, String format, int arg1, Object arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -916,7 +1078,9 @@ public static void debugf(Throwable t, String format, int arg1, Object arg2, Obj * @param arg the parameter */ public static void debugf(String format, long arg) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -927,7 +1091,9 @@ public static void debugf(String format, long arg) { * @param arg2 the second parameter */ public static void debugf(String format, long arg1, long arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -938,7 +1104,9 @@ public static void debugf(String format, long arg1, long arg2) { * @param arg2 the second parameter */ public static void debugf(String format, long arg1, Object arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -950,7 +1118,9 @@ public static void debugf(String format, long arg1, Object arg2) { * @param arg3 the third parameter */ public static void debugf(String format, long arg1, long arg2, long arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -962,7 +1132,9 @@ public static void debugf(String format, long arg1, long arg2, long arg3) { * @param arg3 the third parameter */ public static void debugf(String format, long arg1, long arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -974,7 +1146,9 @@ public static void debugf(String format, long arg1, long arg2, Object arg3) { * @param arg3 the third parameter */ public static void debugf(String format, long arg1, Object arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -985,7 +1159,9 @@ public static void debugf(String format, long arg1, Object arg2, Object arg3) { * @param arg the parameter */ public static void debugf(Throwable t, String format, long arg) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -997,7 +1173,9 @@ public static void debugf(Throwable t, String format, long arg) { * @param arg2 the second parameter */ public static void debugf(Throwable t, String format, long arg1, long arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1009,7 +1187,9 @@ public static void debugf(Throwable t, String format, long arg1, long arg2) { * @param arg2 the second parameter */ public static void debugf(Throwable t, String format, long arg1, Object arg2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1022,7 +1202,9 @@ public static void debugf(Throwable t, String format, long arg1, Object arg2) { * @param arg3 the third parameter */ public static void debugf(Throwable t, String format, long arg1, long arg2, long arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1035,7 +1217,9 @@ public static void debugf(Throwable t, String format, long arg1, long arg2, long * @param arg3 the third parameter */ public static void debugf(Throwable t, String format, long arg1, long arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1048,7 +1232,9 @@ public static void debugf(Throwable t, String format, long arg1, long arg2, Obje * @param arg3 the third parameter */ public static void debugf(Throwable t, String format, long arg1, Object arg2, Object arg3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1058,7 +1244,10 @@ public static void debugf(Throwable t, String format, long arg1, Object arg2, Ob * otherwise */ public static boolean isInfoEnabled() { - throw fail(); + if (always()) { + throw fail(); + } + return always(); } /** @@ -1067,7 +1256,9 @@ public static boolean isInfoEnabled() { * @param message the message */ public static void info(Object message) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1077,7 +1268,9 @@ public static void info(Object message) { * @param t the throwable */ public static void info(Object message, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1088,7 +1281,9 @@ public static void info(Object message, Throwable t) { * @param t the throwable */ public static void info(String loggerFqcn, Object message, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1100,7 +1295,9 @@ public static void info(String loggerFqcn, Object message, Throwable t) { * @param t the throwable */ public static void info(String loggerFqcn, Object message, Object[] params, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1110,7 +1307,9 @@ public static void info(String loggerFqcn, Object message, Object[] params, Thro * @param params the parameters */ public static void infov(String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1120,7 +1319,9 @@ public static void infov(String format, Object... params) { * @param param1 the sole parameter */ public static void infov(String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1131,7 +1332,9 @@ public static void infov(String format, Object param1) { * @param param2 the second parameter */ public static void infov(String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1143,7 +1346,9 @@ public static void infov(String format, Object param1, Object param2) { * @param param3 the third parameter */ public static void infov(String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1154,7 +1359,9 @@ public static void infov(String format, Object param1, Object param2, Object par * @param params the parameters */ public static void infov(Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1165,7 +1372,9 @@ public static void infov(Throwable t, String format, Object... params) { * @param param1 the sole parameter */ public static void infov(Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1177,7 +1386,9 @@ public static void infov(Throwable t, String format, Object param1) { * @param param2 the second parameter */ public static void infov(Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1190,7 +1401,9 @@ public static void infov(Throwable t, String format, Object param1, Object param * @param param3 the third parameter */ public static void infov(Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1200,7 +1413,9 @@ public static void infov(Throwable t, String format, Object param1, Object param * @param params the parameters */ public static void infof(String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1210,7 +1425,9 @@ public static void infof(String format, Object... params) { * @param param1 the sole parameter */ public static void infof(String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1221,7 +1438,9 @@ public static void infof(String format, Object param1) { * @param param2 the second parameter */ public static void infof(String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1233,7 +1452,9 @@ public static void infof(String format, Object param1, Object param2) { * @param param3 the third parameter */ public static void infof(String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1244,7 +1465,9 @@ public static void infof(String format, Object param1, Object param2, Object par * @param params the parameters */ public static void infof(Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1255,7 +1478,9 @@ public static void infof(Throwable t, String format, Object... params) { * @param param1 the sole parameter */ public static void infof(Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1267,7 +1492,9 @@ public static void infof(Throwable t, String format, Object param1) { * @param param2 the second parameter */ public static void infof(Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1280,7 +1507,9 @@ public static void infof(Throwable t, String format, Object param1, Object param * @param param3 the third parameter */ public static void infof(Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1289,7 +1518,9 @@ public static void infof(Throwable t, String format, Object param1, Object param * @param message the message */ public static void warn(Object message) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1299,7 +1530,9 @@ public static void warn(Object message) { * @param t the throwable */ public static void warn(Object message, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1310,7 +1543,9 @@ public static void warn(Object message, Throwable t) { * @param t the throwable */ public static void warn(String loggerFqcn, Object message, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1322,7 +1557,9 @@ public static void warn(String loggerFqcn, Object message, Throwable t) { * @param t the throwable */ public static void warn(String loggerFqcn, Object message, Object[] params, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1332,7 +1569,9 @@ public static void warn(String loggerFqcn, Object message, Object[] params, Thro * @param params the parameters */ public static void warnv(String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1342,7 +1581,9 @@ public static void warnv(String format, Object... params) { * @param param1 the sole parameter */ public static void warnv(String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1353,7 +1594,9 @@ public static void warnv(String format, Object param1) { * @param param2 the second parameter */ public static void warnv(String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1365,7 +1608,9 @@ public static void warnv(String format, Object param1, Object param2) { * @param param3 the third parameter */ public static void warnv(String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1376,7 +1621,9 @@ public static void warnv(String format, Object param1, Object param2, Object par * @param params the parameters */ public static void warnv(Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1387,7 +1634,9 @@ public static void warnv(Throwable t, String format, Object... params) { * @param param1 the sole parameter */ public static void warnv(Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1399,7 +1648,9 @@ public static void warnv(Throwable t, String format, Object param1) { * @param param2 the second parameter */ public static void warnv(Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1412,7 +1663,9 @@ public static void warnv(Throwable t, String format, Object param1, Object param * @param param3 the third parameter */ public static void warnv(Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1422,7 +1675,9 @@ public static void warnv(Throwable t, String format, Object param1, Object param * @param params the parameters */ public static void warnf(String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1432,7 +1687,9 @@ public static void warnf(String format, Object... params) { * @param param1 the sole parameter */ public static void warnf(String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1443,7 +1700,9 @@ public static void warnf(String format, Object param1) { * @param param2 the second parameter */ public static void warnf(String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1455,7 +1714,9 @@ public static void warnf(String format, Object param1, Object param2) { * @param param3 the third parameter */ public static void warnf(String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1466,7 +1727,9 @@ public static void warnf(String format, Object param1, Object param2, Object par * @param params the parameters */ public static void warnf(Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1477,7 +1740,9 @@ public static void warnf(Throwable t, String format, Object... params) { * @param param1 the sole parameter */ public static void warnf(Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1489,7 +1754,9 @@ public static void warnf(Throwable t, String format, Object param1) { * @param param2 the second parameter */ public static void warnf(Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1502,7 +1769,9 @@ public static void warnf(Throwable t, String format, Object param1, Object param * @param param3 the third parameter */ public static void warnf(Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1511,7 +1780,9 @@ public static void warnf(Throwable t, String format, Object param1, Object param * @param message the message */ public static void error(Object message) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1521,7 +1792,9 @@ public static void error(Object message) { * @param t the throwable */ public static void error(Object message, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1532,7 +1805,9 @@ public static void error(Object message, Throwable t) { * @param t the throwable */ public static void error(String loggerFqcn, Object message, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1544,7 +1819,9 @@ public static void error(String loggerFqcn, Object message, Throwable t) { * @param t the throwable */ public static void error(String loggerFqcn, Object message, Object[] params, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1554,7 +1831,9 @@ public static void error(String loggerFqcn, Object message, Object[] params, Thr * @param params the parameters */ public static void errorv(String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1564,7 +1843,9 @@ public static void errorv(String format, Object... params) { * @param param1 the sole parameter */ public static void errorv(String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1575,7 +1856,9 @@ public static void errorv(String format, Object param1) { * @param param2 the second parameter */ public static void errorv(String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1587,7 +1870,9 @@ public static void errorv(String format, Object param1, Object param2) { * @param param3 the third parameter */ public static void errorv(String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1598,7 +1883,9 @@ public static void errorv(String format, Object param1, Object param2, Object pa * @param params the parameters */ public static void errorv(Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1609,7 +1896,9 @@ public static void errorv(Throwable t, String format, Object... params) { * @param param1 the sole parameter */ public static void errorv(Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1621,7 +1910,9 @@ public static void errorv(Throwable t, String format, Object param1) { * @param param2 the second parameter */ public static void errorv(Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1634,7 +1925,9 @@ public static void errorv(Throwable t, String format, Object param1, Object para * @param param3 the third parameter */ public static void errorv(Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1644,7 +1937,9 @@ public static void errorv(Throwable t, String format, Object param1, Object para * @param params the parameters */ public static void errorf(String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1654,7 +1949,9 @@ public static void errorf(String format, Object... params) { * @param param1 the sole parameter */ public static void errorf(String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1665,7 +1962,9 @@ public static void errorf(String format, Object param1) { * @param param2 the second parameter */ public static void errorf(String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1677,7 +1976,9 @@ public static void errorf(String format, Object param1, Object param2) { * @param param3 the third parameter */ public static void errorf(String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1688,7 +1989,9 @@ public static void errorf(String format, Object param1, Object param2, Object pa * @param params the parameters */ public static void errorf(Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1699,7 +2002,9 @@ public static void errorf(Throwable t, String format, Object... params) { * @param param1 the sole parameter */ public static void errorf(Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1711,7 +2016,9 @@ public static void errorf(Throwable t, String format, Object param1) { * @param param2 the second parameter */ public static void errorf(Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1724,7 +2031,9 @@ public static void errorf(Throwable t, String format, Object param1, Object para * @param param3 the third parameter */ public static void errorf(Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1733,7 +2042,9 @@ public static void errorf(Throwable t, String format, Object param1, Object para * @param message the message */ public static void fatal(Object message) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1743,7 +2054,9 @@ public static void fatal(Object message) { * @param t the throwable */ public static void fatal(Object message, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1754,7 +2067,9 @@ public static void fatal(Object message, Throwable t) { * @param t the throwable */ public static void fatal(String loggerFqcn, Object message, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1766,7 +2081,9 @@ public static void fatal(String loggerFqcn, Object message, Throwable t) { * @param t the throwable */ public static void fatal(String loggerFqcn, Object message, Object[] params, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1776,7 +2093,9 @@ public static void fatal(String loggerFqcn, Object message, Object[] params, Thr * @param params the parameters */ public static void fatalv(String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1786,7 +2105,9 @@ public static void fatalv(String format, Object... params) { * @param param1 the sole parameter */ public static void fatalv(String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1797,7 +2118,9 @@ public static void fatalv(String format, Object param1) { * @param param2 the second parameter */ public static void fatalv(String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1809,7 +2132,9 @@ public static void fatalv(String format, Object param1, Object param2) { * @param param3 the third parameter */ public static void fatalv(String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1820,7 +2145,9 @@ public static void fatalv(String format, Object param1, Object param2, Object pa * @param params the parameters */ public static void fatalv(Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1831,7 +2158,9 @@ public static void fatalv(Throwable t, String format, Object... params) { * @param param1 the sole parameter */ public static void fatalv(Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1843,7 +2172,9 @@ public static void fatalv(Throwable t, String format, Object param1) { * @param param2 the second parameter */ public static void fatalv(Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1856,7 +2187,9 @@ public static void fatalv(Throwable t, String format, Object param1, Object para * @param param3 the third parameter */ public static void fatalv(Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1866,7 +2199,9 @@ public static void fatalv(Throwable t, String format, Object param1, Object para * @param params the parameters */ public static void fatalf(String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1876,7 +2211,9 @@ public static void fatalf(String format, Object... params) { * @param param1 the sole parameter */ public static void fatalf(String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1887,7 +2224,9 @@ public static void fatalf(String format, Object param1) { * @param param2 the second parameter */ public static void fatalf(String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1899,7 +2238,9 @@ public static void fatalf(String format, Object param1, Object param2) { * @param param3 the third parameter */ public static void fatalf(String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1910,7 +2251,9 @@ public static void fatalf(String format, Object param1, Object param2, Object pa * @param params the parameters */ public static void fatalf(Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1921,7 +2264,9 @@ public static void fatalf(Throwable t, String format, Object... params) { * @param param1 the sole parameter */ public static void fatalf(Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1933,7 +2278,9 @@ public static void fatalf(Throwable t, String format, Object param1) { * @param param2 the second parameter */ public static void fatalf(Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1946,7 +2293,9 @@ public static void fatalf(Throwable t, String format, Object param1, Object para * @param param3 the third parameter */ public static void fatalf(Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1956,7 +2305,9 @@ public static void fatalf(Throwable t, String format, Object param1, Object para * @param message the message */ public static void log(Logger.Level level, Object message) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1967,7 +2318,9 @@ public static void log(Logger.Level level, Object message) { * @param t the throwable */ public static void log(Logger.Level level, Object message, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1979,7 +2332,9 @@ public static void log(Logger.Level level, Object message, Throwable t) { * @param t the throwable */ public static void log(Logger.Level level, String loggerFqcn, Object message, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -1992,7 +2347,9 @@ public static void log(Logger.Level level, String loggerFqcn, Object message, Th * @param t the throwable */ public static void log(String loggerFqcn, Logger.Level level, Object message, Object[] params, Throwable t) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2003,7 +2360,9 @@ public static void log(String loggerFqcn, Logger.Level level, Object message, Ob * @param params the parameters */ public static void logv(Logger.Level level, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2014,7 +2373,9 @@ public static void logv(Logger.Level level, String format, Object... params) { * @param param1 the sole parameter */ public static void logv(Logger.Level level, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2026,7 +2387,9 @@ public static void logv(Logger.Level level, String format, Object param1) { * @param param2 the second parameter */ public static void logv(Logger.Level level, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2039,7 +2402,9 @@ public static void logv(Logger.Level level, String format, Object param1, Object * @param param3 the third parameter */ public static void logv(Logger.Level level, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2051,7 +2416,9 @@ public static void logv(Logger.Level level, String format, Object param1, Object * @param params the parameters */ public static void logv(Logger.Level level, Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2063,7 +2430,9 @@ public static void logv(Logger.Level level, Throwable t, String format, Object.. * @param param1 the sole parameter */ public static void logv(Logger.Level level, Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2076,7 +2445,9 @@ public static void logv(Logger.Level level, Throwable t, String format, Object p * @param param2 the second parameter */ public static void logv(Logger.Level level, Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2090,7 +2461,9 @@ public static void logv(Logger.Level level, Throwable t, String format, Object p * @param param3 the third parameter */ public static void logv(Logger.Level level, Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2103,7 +2476,9 @@ public static void logv(Logger.Level level, Throwable t, String format, Object p * @param params the parameters */ public static void logv(String loggerFqcn, Logger.Level level, Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2116,7 +2491,9 @@ public static void logv(String loggerFqcn, Logger.Level level, Throwable t, Stri * @param param1 the sole parameter */ public static void logv(String loggerFqcn, Logger.Level level, Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2130,7 +2507,9 @@ public static void logv(String loggerFqcn, Logger.Level level, Throwable t, Stri * @param param2 the second parameter */ public static void logv(String loggerFqcn, Logger.Level level, Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2146,7 +2525,9 @@ public static void logv(String loggerFqcn, Logger.Level level, Throwable t, Stri */ public static void logv(String loggerFqcn, Logger.Level level, Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2157,7 +2538,9 @@ public static void logv(String loggerFqcn, Logger.Level level, Throwable t, Stri * @param params the parameters */ public static void logf(Logger.Level level, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2168,7 +2551,9 @@ public static void logf(Logger.Level level, String format, Object... params) { * @param param1 the sole parameter */ public static void logf(Logger.Level level, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2180,7 +2565,9 @@ public static void logf(Logger.Level level, String format, Object param1) { * @param param2 the second parameter */ public static void logf(Logger.Level level, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2193,7 +2580,9 @@ public static void logf(Logger.Level level, String format, Object param1, Object * @param param3 the third parameter */ public static void logf(Logger.Level level, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2205,7 +2594,9 @@ public static void logf(Logger.Level level, String format, Object param1, Object * @param params the parameters */ public static void logf(Logger.Level level, Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2217,7 +2608,9 @@ public static void logf(Logger.Level level, Throwable t, String format, Object.. * @param param1 the sole parameter */ public static void logf(Logger.Level level, Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2230,7 +2623,9 @@ public static void logf(Logger.Level level, Throwable t, String format, Object p * @param param2 the second parameter */ public static void logf(Logger.Level level, Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2244,7 +2639,9 @@ public static void logf(Logger.Level level, Throwable t, String format, Object p * @param param3 the third parameter */ public static void logf(Logger.Level level, Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2257,7 +2654,9 @@ public static void logf(Logger.Level level, Throwable t, String format, Object p * @param param1 the sole parameter */ public static void logf(String loggerFqcn, Logger.Level level, Throwable t, String format, Object param1) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2271,7 +2670,9 @@ public static void logf(String loggerFqcn, Logger.Level level, Throwable t, Stri * @param param2 the second parameter */ public static void logf(String loggerFqcn, Logger.Level level, Throwable t, String format, Object param1, Object param2) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2287,7 +2688,9 @@ public static void logf(String loggerFqcn, Logger.Level level, Throwable t, Stri */ public static void logf(String loggerFqcn, Logger.Level level, Throwable t, String format, Object param1, Object param2, Object param3) { - throw fail(); + if (always()) { + throw fail(); + } } /** @@ -2300,7 +2703,13 @@ public static void logf(String loggerFqcn, Logger.Level level, Throwable t, Stri * @param params the message parameters */ public static void logf(String loggerFqcn, Logger.Level level, Throwable t, String format, Object... params) { - throw fail(); + if (always()) { + throw fail(); + } + } + + private static boolean always() { + return true; } private static UnsupportedOperationException fail() { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigRecorder.java index aaec129f11ed1..e898813ee4517 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigRecorder.java @@ -34,8 +34,8 @@ public void handleConfigChange(Map buildTimeConfig) { if (mismatches == null) { mismatches = new ArrayList<>(); } - mismatches.add(" - " + entry.getKey() + " is set to '" + entry.getValue() - + "' but it is build time fixed to '" + val.get() + "'. Did you change the property " + mismatches.add(" - " + entry.getKey() + " is set to '" + val.get() + + "' but it is build time fixed to '" + entry.getValue() + "'. Did you change the property " + entry.getKey() + " after building the application?"); } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java index 890376d39b132..787af1ecf8279 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java @@ -142,6 +142,8 @@ public ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorConte if (profileValue != null) { List profiles = ProfileConfigSourceInterceptor.convertProfile(profileValue.getValue()); for (String profile : profiles) { + relocations.put("%" + profile + "." + SMALLRYE_CONFIG_LOCATIONS, + "%" + profile + "." + "quarkus.config.locations"); relocations.put("%" + profile + "." + SMALLRYE_CONFIG_PROFILE_PARENT, "%" + profile + "." + "quarkus.config.profile.parent"); } @@ -181,7 +183,7 @@ public OptionalInt getPriority() { @SuppressWarnings("unchecked") public static SmallRyeConfigBuilder configBuilder(SmallRyeConfigBuilder builder, List configBuilders) { - configBuilders.sort(Comparator.comparing(ConfigBuilder::priority)); + configBuilders.sort(ConfigBuilderComparator.INSTANCE); for (ConfigBuilder configBuilder : configBuilders) { builder = configBuilder.configBuilder(builder); @@ -331,4 +333,17 @@ public Set getPropertyNames() { return Collections.emptySet(); } } + + private static class ConfigBuilderComparator implements Comparator { + + private static final ConfigBuilderComparator INSTANCE = new ConfigBuilderComparator(); + + private ConfigBuilderComparator() { + } + + @Override + public int compare(ConfigBuilder o1, ConfigBuilder o2) { + return Integer.compare(o1.priority(), o2.priority()); + } + } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java index 78bcdb7c7368c..5e9b9239daa78 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java @@ -58,13 +58,13 @@ public static class RotationConfig { /** * The maximum file size of the log file after which a rotation is executed. */ - @ConfigItem(defaultValueDocumentation = "10") - Optional maxFileSize; + @ConfigItem(defaultValue = "10M") + MemorySize maxFileSize; /** * The maximum number of backups to keep. */ - @ConfigItem(defaultValue = "1") + @ConfigItem(defaultValue = "5") int maxBackupIndex; /** 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 3e2affb05babd..4fd55ae3d2028 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 @@ -33,7 +33,6 @@ import org.jboss.logmanager.handlers.AsyncHandler; import org.jboss.logmanager.handlers.ConsoleHandler; import org.jboss.logmanager.handlers.FileHandler; -import org.jboss.logmanager.handlers.PeriodicRotatingFileHandler; import org.jboss.logmanager.handlers.PeriodicSizeRotatingFileHandler; import org.jboss.logmanager.handlers.SizeRotatingFileHandler; import org.jboss.logmanager.handlers.SyslogHandler; @@ -75,6 +74,7 @@ public static void handleFailedStart(RuntimeValue>> ba ConsoleRuntimeConfig consoleRuntimeConfig = new ConsoleRuntimeConfig(); ConfigInstantiator.handleObject(consoleRuntimeConfig); new LoggingSetupRecorder(new RuntimeValue<>(consoleRuntimeConfig)).initializeLogging(config, buildConfig, false, null, + Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), banner, LaunchMode.DEVELOPMENT); @@ -85,7 +85,8 @@ public void initializeLogging(LogConfig config, LogBuildTimeConfig buildConfig, final RuntimeValue> devUiConsoleHandler, final List>> additionalHandlers, final List>> additionalNamedHandlers, - final List>> possibleFormatters, + final List>> possibleConsoleFormatters, + final List>> possibleFileFormatters, final RuntimeValue>> possibleBannerSupplier, LaunchMode launchMode) { final Map categories = config.categories; @@ -126,7 +127,7 @@ public void accept(String loggerName, CleanupFilterConfig config) { if (config.console.enable) { final Handler consoleHandler = configureConsoleHandler(config.console, consoleRuntimeConfig.getValue(), errorManager, cleanupFiler, - possibleFormatters, possibleBannerSupplier, launchMode); + possibleConsoleFormatters, possibleBannerSupplier, launchMode); errorManager = consoleHandler.getErrorManager(); handlers.add(consoleHandler); } @@ -150,7 +151,7 @@ public void close() throws SecurityException { } if (config.file.enable) { - handlers.add(configureFileHandler(config.file, errorManager, cleanupFiler)); + handlers.add(configureFileHandler(config.file, errorManager, cleanupFiler, possibleFileFormatters)); } if (config.syslog.enable) { @@ -178,7 +179,7 @@ public void close() throws SecurityException { if (!categories.isEmpty()) { Map namedHandlers = createNamedHandlers(config, consoleRuntimeConfig.getValue(), - possibleFormatters, errorManager, + possibleConsoleFormatters, possibleFileFormatters, errorManager, cleanupFiler, launchMode); Map additionalNamedHandlersMap; @@ -229,7 +230,8 @@ public void accept(String categoryName, CategoryConfig config) { } public static void initializeBuildTimeLogging(LogConfig config, LogBuildTimeConfig buildConfig, - ConsoleRuntimeConfig consoleConfig, LaunchMode launchMode) { + ConsoleRuntimeConfig consoleConfig, List>> possibleFileFormatters, + LaunchMode launchMode) { final Map categories = config.categories; final LogContext logContext = LogContext.getLogContext(); @@ -256,8 +258,8 @@ public static void initializeBuildTimeLogging(LogConfig config, LogBuildTimeConf handlers.add(consoleHandler); } - Map namedHandlers = createNamedHandlers(config, consoleConfig, Collections.emptyList(), errorManager, - logCleanupFilter, launchMode); + Map namedHandlers = createNamedHandlers(config, consoleConfig, Collections.emptyList(), + possibleFileFormatters, errorManager, logCleanupFilter, launchMode); for (Map.Entry entry : categories.entrySet()) { final String categoryName = entry.getKey(); @@ -308,7 +310,9 @@ public static Level getLogLevel(String categoryName, Map categori } private static Map createNamedHandlers(LogConfig config, ConsoleRuntimeConfig consoleRuntimeConfig, - List>> possibleFormatters, ErrorManager errorManager, + List>> possibleConsoleFormatters, + List>> possibleFileFormatters, + ErrorManager errorManager, LogCleanupFilter cleanupFilter, LaunchMode launchMode) { Map namedHandlers = new HashMap<>(); for (Entry consoleConfigEntry : config.consoleHandlers.entrySet()) { @@ -318,7 +322,7 @@ private static Map createNamedHandlers(LogConfig config, Consol } final Handler consoleHandler = configureConsoleHandler(namedConsoleConfig, consoleRuntimeConfig, errorManager, cleanupFilter, - possibleFormatters, null, launchMode); + possibleConsoleFormatters, null, launchMode); addToNamedHandlers(namedHandlers, consoleHandler, consoleConfigEntry.getKey()); } for (Entry fileConfigEntry : config.fileHandlers.entrySet()) { @@ -326,7 +330,8 @@ private static Map createNamedHandlers(LogConfig config, Consol if (!namedFileConfig.enable) { continue; } - final Handler fileHandler = configureFileHandler(namedFileConfig, errorManager, cleanupFilter); + final Handler fileHandler = configureFileHandler(namedFileConfig, errorManager, cleanupFilter, + possibleFileFormatters); addToNamedHandlers(namedHandlers, fileHandler, fileConfigEntry.getKey()); } for (Entry sysLogConfigEntry : config.syslogHandlers.entrySet()) { @@ -462,38 +467,46 @@ public void close() throws SecurityException { } if (formatterWarning) { - handler.getErrorManager().error("Multiple formatters were activated", null, ErrorManager.GENERIC_FAILURE); + handler.getErrorManager().error("Multiple console formatters were activated", null, ErrorManager.GENERIC_FAILURE); } return handler; } private static Handler configureFileHandler(final FileConfig config, final ErrorManager errorManager, - final LogCleanupFilter cleanupFilter) { - FileHandler handler = new FileHandler(); + final LogCleanupFilter cleanupFilter, final List>> possibleFileFormatters) { + FileHandler handler; FileConfig.RotationConfig rotationConfig = config.rotation; - if ((rotationConfig.maxFileSize.isPresent() || rotationConfig.rotateOnBoot) - && rotationConfig.fileSuffix.isPresent()) { + if (rotationConfig.fileSuffix.isPresent()) { PeriodicSizeRotatingFileHandler periodicSizeRotatingFileHandler = new PeriodicSizeRotatingFileHandler(); periodicSizeRotatingFileHandler.setSuffix(rotationConfig.fileSuffix.get()); - rotationConfig.maxFileSize - .ifPresent(memorySize -> periodicSizeRotatingFileHandler.setRotateSize(memorySize.asLongValue())); + periodicSizeRotatingFileHandler.setRotateSize(rotationConfig.maxFileSize.asLongValue()); periodicSizeRotatingFileHandler.setRotateOnBoot(rotationConfig.rotateOnBoot); periodicSizeRotatingFileHandler.setMaxBackupIndex(rotationConfig.maxBackupIndex); handler = periodicSizeRotatingFileHandler; - } else if (rotationConfig.maxFileSize.isPresent()) { + } else { SizeRotatingFileHandler sizeRotatingFileHandler = new SizeRotatingFileHandler( - rotationConfig.maxFileSize.get().asLongValue(), rotationConfig.maxBackupIndex); + rotationConfig.maxFileSize.asLongValue(), rotationConfig.maxBackupIndex); sizeRotatingFileHandler.setRotateOnBoot(rotationConfig.rotateOnBoot); handler = sizeRotatingFileHandler; - } else if (rotationConfig.fileSuffix.isPresent()) { - PeriodicRotatingFileHandler periodicRotatingFileHandler = new PeriodicRotatingFileHandler(); - periodicRotatingFileHandler.setSuffix(rotationConfig.fileSuffix.get()); - handler = periodicRotatingFileHandler; } - final PatternFormatter formatter = new PatternFormatter(config.format); + Formatter formatter = null; + boolean formatterWarning = false; + for (RuntimeValue> value : possibleFileFormatters) { + if (formatter != null) { + formatterWarning = true; + } + final Optional val = value.getValue(); + if (val.isPresent()) { + formatter = val.get(); + } + } + if (formatter == null) { + formatter = new PatternFormatter(config.format); + } handler.setFormatter(formatter); + handler.setAppend(true); try { handler.setFile(config.path); @@ -503,6 +516,11 @@ private static Handler configureFileHandler(final FileConfig config, final Error handler.setErrorManager(errorManager); handler.setLevel(config.level); handler.setFilter(cleanupFilter); + + if (formatterWarning) { + handler.getErrorManager().error("Multiple file formatters were activated", null, ErrorManager.GENERIC_FAILURE); + } + if (config.async.enable) { return createAsyncHandler(config.async, config.level, handler); } diff --git a/core/runtime/src/test/java/io/quarkus/logging/GenerateLog.java b/core/runtime/src/test/java/io/quarkus/logging/GenerateLog.java index af56239be10d4..d8830d2d7a508 100644 --- a/core/runtime/src/test/java/io/quarkus/logging/GenerateLog.java +++ b/core/runtime/src/test/java/io/quarkus/logging/GenerateLog.java @@ -24,11 +24,25 @@ public class GenerateLog { " * Invocations of all {@code static} methods of this class are, during build time, replaced by invocations\n" + " * of the same methods on a generated instance of {@link Logger}.\n" + " */\n" + - "public final class Log"; - private static final String LOG_METHOD_BODY = ") {\n" + - " throw fail();\n" + + "public final class Log {\n" + + " // automatically generated by io.quarkus.logging.GenerateLog"; + // the conditions here are an attempt to stop IntelliJ flagging the Log methods as always throwing + private static final String VOID_METHOD_BODY = ") {\n" + + " if (always()) {\n" + + " throw fail();\n" + + " }\n" + + " }"; + private static final String BOOLEAN_METHOD_BODY = ") {\n" + + " if (always()) {\n" + + " throw fail();\n" + + " }\n" + + " return always();\n" + " }"; private static final String FAIL_METHOD = "" + + "\n" + + " private static boolean always() {\n" + + " return true;\n" + + " }\n" + "\n" + " private static UnsupportedOperationException fail() {\n" + " return new UnsupportedOperationException(\"Using \" + Log.class.getName()\n" + @@ -61,10 +75,9 @@ public static void main(String[] args) throws Exception { private static void generateLogClass(String basicLogger) { String quarkusLog = basicLogger .replaceFirst("(?s).*?package org.jboss.logging;", PACKAGE_AND_IMPORT) - .replaceFirst("(?s)/\\*\\*.*?public interface BasicLogger", CLASS_HEADER) - .replaceAll("void", "public static void") - .replaceAll("boolean", "public static boolean") - .replaceAll("\\);", LOG_METHOD_BODY) + .replaceFirst("(?s)/\\*\\*.*?public interface BasicLogger \\{", CLASS_HEADER) + .replaceAll("void (.*?)\\);", "public static void $1" + VOID_METHOD_BODY) + .replaceAll("boolean (.*?)\\);", "public static boolean $1" + BOOLEAN_METHOD_BODY) .replaceFirst("}\\s*$", FAIL_METHOD + "}\n"); System.out.println(quarkusLog); diff --git a/core/runtime/src/test/java/io/quarkus/logging/LoggingApiCompletenessTest.java b/core/runtime/src/test/java/io/quarkus/logging/LoggingApiCompletenessTest.java index d4cf29662b0e5..49d8e52d32511 100644 --- a/core/runtime/src/test/java/io/quarkus/logging/LoggingApiCompletenessTest.java +++ b/core/runtime/src/test/java/io/quarkus/logging/LoggingApiCompletenessTest.java @@ -62,7 +62,7 @@ public void compareWithJbossLogging() { private static boolean isPrivateStaticFail(Method method) { return Modifier.isPrivate(method.getModifiers()) && Modifier.isStatic(method.getModifiers()) - && "fail".equals(method.getName()); + && ("fail".equals(method.getName()) || "always".equals(method.getName())); } private static boolean areEquivalent(Method jbossLoggingMethod, Method quarkusLogMethod) { diff --git a/devtools/Devtools_Specification.md b/devtools/Devtools_Specification.md deleted file mode 100644 index b51c6ee6ed7e3..0000000000000 --- a/devtools/Devtools_Specification.md +++ /dev/null @@ -1,73 +0,0 @@ -# Devtools Specification - -This document will attempt to specify the expected behavior of the various tools (æsh, maven, forge, etc.) developed to help developers onboard and manage their quarkus based work. Much of this will presume maven based projects as gradle has not even come up in discussion as yet. There are a few scenarios to cover: - -1. Creating a new project -2. Updating an existing pom to support quarkus - 3. To a deployable project (a "jar" project) - 4. To a parent project (a "pom" project) -3. Adding extensions to an existing project - 4. To a pom that doesn't have quarkus support yet - 5. To a pom already configured for quarkus support - -## The Initial Conditions -1. Path to project defaulting to the current folder (may not exist yet) -2. 7 values for the project -2. Optional list of additional extensions -3. Optional prefix name the names generated Resource and Application classes (default: Quarkus) -4. Optional package name -3. ??? - -## The Endgame -Regardless of the initial conditions, the final product should include: - -1. Dependencies on: - 2. The quarkus BOM in dependenciesManagement - 3. quarkus-jaxrs - 4. quarkus-junit5 -5. The quarkus-maven-plugin -5. The `native` profile -6. A common property to track quarkus versions (Debatable!) - -## Target applications: - -1. An æsh based command line - * jar and native image based -1. A maven plugin -2. A forge plugin - -## Creating a new project -Creating a project from scratch introduces the fewest barriers of course. A basic template containing these is trivial to produce. - -## Updating an existing pom -Updating existing poms falls, generally, in to three categories: 1) adding initial quarkus support to a pristine pom, 2) running `create` on a pom already configured for quarkus, and 3) adding extensions to a pom that already supports quarkus. - -### Initial quarkus support -In these cases, some of the initial conditions no longer apply. Even though collected because the various interfaces will have them marked as required, the GAV values are now redundant. These values are to be ignored in favor of the extant values found in the pom. - -1. The pom should be updated to include the bom in the dependenciesManagement section. One exception to this could be if a parent pom defines this bom then it could be skipped here. Resolving that could be complicated and/or time consuming, however, so declaring it locally might still be the simpler solution especially as it's largely harmless -1. The quarkus-maven-plugin should be added to the plugins section of the build section. Both sections should be created if they do not already exist. -1. A property should be created to store the quarkus version (named `quarkus.version`) for convenience. -1. References to the "core" quarkus extensions should be added to the dependencies section creating that section if necessary -2. Any extensions listed as one of the options should be added -3. None of these extensions should directly reference ${quarkus.version} but should rely on the bom definitions. - -### Quarkus already configured -If the bom and the plugin have already been configured, the update process should terminate. An argument could be made for checking the version and potentially updating it but this would be inappropriate. Some versions might require more boilerplate and such an update might be unexpected and unwanted. - -### Differing pom types -If the packaging type is "jar" work should proceed as described above. If the packaging type is "pom," however, things change slightly. In this case the process should do the following: - -1. Add the bom to the dependencesManagement section as described above. -2. Add the plugin to the pluginManagement section creating any missing nesting sections as necessary. -3. It is unnecessary to add any dependencies on this level as that will be handled by updating the appropriate modules to use the quarkus plugin directly. - -### Adding Extensions -Adding extensions should presume the existence of a bom and the plugin. The added extensions should not explicitly declare a version. If not bom is present in the dependencyManagement section, one should be added. - -## Resolving conflicting definitions - -1. GAV values passed in should always be discarded in favor of values in the pom. -2. When passing in a package name and a class names, if the class name is fully qualified, always use the package in the FQCN. In the absence of a FQCN, use the specified package name given by the user. -3. Existing files are never to be overwritten. - * _Should alternative paths then be computed? simply log a warning?_ diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index 5440493aa0308..3c749aff0120f 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -292,6 +292,19 @@
+ + io.quarkus + quarkus-confluent-registry-avro + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-container-image @@ -643,6 +656,19 @@ + + io.quarkus + quarkus-hal + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-hibernate-envers diff --git a/devtools/bom-descriptor-json/src/main/resources/catalog-overrides.json b/devtools/bom-descriptor-json/src/main/resources/catalog-overrides.json index 13c522e4a5aaa..1c102e474cb8e 100644 --- a/devtools/bom-descriptor-json/src/main/resources/catalog-overrides.json +++ b/devtools/bom-descriptor-json/src/main/resources/catalog-overrides.json @@ -12,6 +12,7 @@ "io.quarkus:quarkus-resteasy-reactive-jaxb", "io.quarkus:quarkus-resteasy-reactive-kotlin-serialization", "io.quarkus:quarkus-resteasy-reactive-qute", + "io.quarkus:quarkus-resteasy-reactive-links", "io.quarkus:quarkus-rest-client-reactive", "io.quarkus:quarkus-rest-client-reactive-jackson", "io.quarkus:quarkus-rest-client-reactive-jsonb", @@ -24,6 +25,7 @@ "io.quarkus:quarkus-resteasy-multipart", "io.quarkus:quarkus-resteasy-mutiny", "io.quarkus:quarkus-resteasy-qute", + "io.quarkus:quarkus-resteasy-links", "io.quarkus:quarkus-rest-client-jackson", "io.quarkus:quarkus-rest-client-jsonb", "io.quarkus:quarkus-rest-client-jaxb" @@ -52,7 +54,7 @@ { "name": "Messaging", "id": "messaging", - "description": "Send and receives message to various messaging systems (AMQP, KAfka etc)", + "description": "Send and receives message to various messaging systems (AMQP, Kafka etc)", "metadata": { "pinned": [ "io.quarkus:quarkus-smallrye-reactive-messaging", 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 f6a82cd38d555..5ffe927fb9e75 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/CreateApp.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/CreateApp.java @@ -64,7 +64,7 @@ public Integer call() throws Exception { setCodegenOptions(codeGeneration); QuarkusCommandInvocation invocation = build(buildTool, targetQuarkusVersion, - propertiesOptions.properties); + propertiesOptions.properties, extensions); boolean success = true; 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 0ef260cecf6c2..5fa97306b226c 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/CreateCli.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/CreateCli.java @@ -66,7 +66,7 @@ public Integer call() throws Exception { setCodegenOptions(codeGeneration); QuarkusCommandInvocation invocation = build(buildTool, targetQuarkusVersion, - propertiesOptions.properties); + propertiesOptions.properties, extensions); boolean success = true; if (runMode.isDryRun()) { diff --git a/devtools/cli/src/main/java/io/quarkus/cli/build/GradleRunner.java b/devtools/cli/src/main/java/io/quarkus/cli/build/GradleRunner.java index eab458b20a68b..014edef1ff244 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/build/GradleRunner.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/build/GradleRunner.java @@ -194,6 +194,10 @@ public List> prepareDevMode(DevOptions devOptions, De setSkipTests(args); } + if (devOptions.offline) { + args.add("--offline"); + } + debugOptions.addDebugArguments(args, jvmArgs); propertiesOptions.flattenJvmArgs(jvmArgs, args); diff --git a/devtools/cli/src/main/java/io/quarkus/cli/build/MavenRunner.java b/devtools/cli/src/main/java/io/quarkus/cli/build/MavenRunner.java index 2c8e927fccd4c..d8bcb2b29ab90 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/build/MavenRunner.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/build/MavenRunner.java @@ -205,6 +205,10 @@ public List> prepareDevMode(DevOptions devOptions, De setSkipTests(args); } + if (devOptions.offline) { + args.add("--offline"); + } + debugOptions.addDebugArguments(args, jvmArgs); propertiesOptions.flattenJvmArgs(jvmArgs, args); diff --git a/devtools/cli/src/main/java/io/quarkus/cli/common/DevOptions.java b/devtools/cli/src/main/java/io/quarkus/cli/common/DevOptions.java index 4a0741ea6f448..4db0f8f3770ee 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/common/DevOptions.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/common/DevOptions.java @@ -18,6 +18,9 @@ public class DevOptions { "--no-tests" }, description = "Toggle continuous testing mode. Enabled by default.", negatable = true, hidden = true) public boolean runTests = true; // TODO: does this make sense re: continuous test? + @CommandLine.Option(order = 5, names = { "--offline" }, description = "Work offline.", defaultValue = "false") + public boolean offline = false; + public boolean skipTests() { return !runTests; } @@ -28,6 +31,6 @@ public boolean isDryRun() { @Override public String toString() { - return "DevOptions [clean=" + clean + ", tests=" + runTests + "]"; + return "DevOptions [clean=" + clean + ", tests=" + runTests + ", offline=" + offline + "]"; } } diff --git a/devtools/cli/src/main/java/io/quarkus/cli/create/BaseCreateCommand.java b/devtools/cli/src/main/java/io/quarkus/cli/create/BaseCreateCommand.java index cc6ad0b945f6b..be317fa731390 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/create/BaseCreateCommand.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/create/BaseCreateCommand.java @@ -1,6 +1,7 @@ package io.quarkus.cli.create; import java.nio.file.Path; +import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -197,18 +198,17 @@ private void setValue(String name, Object value) { * @param buildTool The build tool the project should use (maven, gradle, jbang) * @param targetVersion The target quarkus version * @param properties Additional properties that should be used whiel creating the properties + * @param extensions requested extensions * @return Quarkus command invocation that can be printed (dry-run) or run to create the project * @throws RegistryResolutionException */ public QuarkusCommandInvocation build(BuildTool buildTool, TargetQuarkusVersionGroup targetVersion, - Map properties) + Map properties, Collection extensions) throws RegistryResolutionException { CreateProjectHelper.handleSpringConfiguration(values); output.debug("Creating an app using the following settings: %s", values); - QuarkusProject qp = registryClient.createQuarkusProject(projectRoot(), targetVersion, buildTool, output); - // TODO: knock on effect with properties.. here? properties.entrySet().forEach(x -> { if (x.getValue().length() > 0) { @@ -219,6 +219,9 @@ public QuarkusCommandInvocation build(BuildTool buildTool, TargetQuarkusVersionG output.info("property: %s", x.getKey()); } }); + + QuarkusProject qp = registryClient.createQuarkusProject(projectRoot(), targetVersion, buildTool, output, extensions); + return new QuarkusCommandInvocation(qp, values); } diff --git a/devtools/cli/src/main/java/io/quarkus/cli/create/TargetLanguageGroup.java b/devtools/cli/src/main/java/io/quarkus/cli/create/TargetLanguageGroup.java index 2ac56d3063b01..4248ba04c6c86 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/create/TargetLanguageGroup.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/create/TargetLanguageGroup.java @@ -44,7 +44,7 @@ public SourceType getSourceType(CommandSpec spec, BuildTool buildTool, Set extensions) throws RegistryResolutionException { ExtensionCatalog catalog = getExtensionCatalog(targetVersion, log); if (VALIDATE && catalog.getQuarkusCoreVersion().startsWith("1.")) { throw new UnsupportedOperationException("The version 2 CLI can not be used with Quarkus 1.x projects.\n" + "Use the maven/gradle plugins when working with Quarkus 1.x projects."); } + catalog = CreateProjectHelper.completeCatalog(catalog, extensions, QuarkusProjectHelper.artifactResolver()); return QuarkusProjectHelper.getProject(projectRoot, catalog, buildTool, log); } diff --git a/devtools/cli/src/test/java/io/quarkus/cli/CliHelpTest.java b/devtools/cli/src/test/java/io/quarkus/cli/CliHelpTest.java index da05c677f5a5f..fd69bffdeffdc 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliHelpTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliHelpTest.java @@ -79,6 +79,7 @@ public void testDevHelp() throws Exception { CliDriver.Result result = CliDriver.execute(workspaceRoot, "dev", "--help"); result.echoSystemOut(); assertThat(result.stdout).contains("Usage"); + assertThat(result.stdout).contains("--offline"); } @Test 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 ec4fe18daf7d2..2f95193f74be7 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java @@ -106,7 +106,7 @@ public void testCreateAppDefaults() throws Exception { Assertions.assertTrue(project.resolve("gradlew").toFile().exists(), "Wrapper should exist by default"); - String buildGradleContent = validateBasicIdentifiers(project, CreateProjectHelper.DEFAULT_GROUP_ID, + String buildGradleContent = validateBasicGradleGroovyIdentifiers(project, CreateProjectHelper.DEFAULT_GROUP_ID, CreateProjectHelper.DEFAULT_ARTIFACT_ID, CreateProjectHelper.DEFAULT_VERSION); Assertions.assertTrue(buildGradleContent.contains("quarkus-resteasy"), @@ -117,6 +117,31 @@ public void testCreateAppDefaults() throws Exception { CliDriver.invokeValidateBuild(project); } + @Test + public void testCreateAppDefaultsWithKotlinDSL() throws Exception { + CliDriver.Result result = CliDriver.execute(workspaceRoot, "create", "app", "--gradle-kotlin-dsl", "--verbose", "-e", + "-B"); + Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); + Assertions.assertTrue(result.stdout.contains("SUCCESS"), + "Expected confirmation that the project has been created." + result); + + Assertions.assertTrue(project.resolve("gradlew").toFile().exists(), + "Wrapper should exist by default"); + String buildGradleContent = validateBasicGradleKotlinIdentifiers(project, CreateProjectHelper.DEFAULT_GROUP_ID, + CreateProjectHelper.DEFAULT_ARTIFACT_ID, + CreateProjectHelper.DEFAULT_VERSION); + Assertions.assertTrue(buildGradleContent.contains("quarkus-resteasy"), + "build/gradle should contain quarkus-resteasy:\n" + buildGradleContent); + + Path packagePath = wrapperRoot.resolve("src/main/java/"); + Assertions.assertTrue(packagePath.toFile().isDirectory(), + "Java Source directory should exist: " + packagePath.toAbsolutePath()); + + CliDriver.valdiateGeneratedSourcePackage(project, "org/acme"); + + CliDriver.invokeValidateBuild(project); + } + @Test public void testCreateAppOverrides() throws Exception { Path nested = workspaceRoot.resolve("cli-nested"); @@ -140,7 +165,7 @@ public void testCreateAppOverrides() throws Exception { Assertions.assertTrue(project.resolve("gradlew").toFile().exists(), "Wrapper should exist by default"); - String buildGradleContent = validateBasicIdentifiers(project, "silly", "my-project", "0.1.0"); + String buildGradleContent = validateBasicGradleGroovyIdentifiers(project, "silly", "my-project", "0.1.0"); Assertions.assertTrue(buildGradleContent.contains("quarkus-resteasy-reactive"), "build.gradle should contain quarkus-resteasy-reactive:\n" + buildGradleContent); @@ -163,7 +188,7 @@ public void testCreateCliDefaults() throws Exception { Assertions.assertTrue(project.resolve("gradlew").toFile().exists(), "Wrapper should exist by default"); - String buildGradleContent = validateBasicIdentifiers(project, CreateProjectHelper.DEFAULT_GROUP_ID, + String buildGradleContent = validateBasicGradleGroovyIdentifiers(project, CreateProjectHelper.DEFAULT_GROUP_ID, CreateProjectHelper.DEFAULT_ARTIFACT_ID, CreateProjectHelper.DEFAULT_VERSION); Assertions.assertFalse(buildGradleContent.contains("quarkus-resteasy"), @@ -248,9 +273,9 @@ public void testDevOptions() throws Exception { CliDriver.Result result = CliDriver.execute(workspaceRoot, "create", "app", "--gradle", "-e", "-B", "--verbose"); Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); - // 1 --clean --tests --suspend + // 1 --clean --tests --suspend --offline result = CliDriver.execute(project, "dev", "-e", "--dry-run", - "--clean", "--tests", "--debug", "--suspend", "--debug-mode=listen"); + "--clean", "--tests", "--debug", "--suspend", "--debug-mode=listen", "--offline"); Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code. Result:\n" + result); @@ -269,6 +294,9 @@ public void testDevOptions() throws Exception { Assertions.assertTrue(result.stdout.contains("-Dsuspend"), "gradle command should specify '-Dsuspend'\n" + result); + Assertions.assertTrue(result.stdout.contains("--offline"), + "gradle command should specify --offline\n" + result); + // 2 --no-clean --no-tests --no-debug result = CliDriver.execute(project, "dev", "-e", "--dry-run", "--no-clean", "--no-tests", "--no-debug"); @@ -374,7 +402,7 @@ public void testCreateArgJava17() throws Exception { "Java 17 should be used when specified. Found:\n" + buildGradleContent); } - String validateBasicIdentifiers(Path project, String group, String artifact, String version) throws Exception { + String validateBasicGradleGroovyIdentifiers(Path project, String group, String artifact, String version) throws Exception { Path buildGradle = project.resolve("build.gradle"); Assertions.assertTrue(buildGradle.toFile().exists(), "build.gradle should exist: " + buildGradle.toAbsolutePath().toString()); @@ -394,4 +422,25 @@ String validateBasicIdentifiers(Path project, String group, String artifact, Str return buildContent; } + + String validateBasicGradleKotlinIdentifiers(Path project, String group, String artifact, String version) throws Exception { + Path buildGradle = project.resolve("build.gradle.kts"); + Assertions.assertTrue(buildGradle.toFile().exists(), + "build.gradle.kts should exist: " + buildGradle.toAbsolutePath().toString()); + + String buildContent = CliDriver.readFileAsString(project, buildGradle); + Assertions.assertTrue(buildContent.contains("group = \"" + group + "\""), + "build.gradle.kts should include the group id:\n" + buildContent); + Assertions.assertTrue(buildContent.contains("version = \"" + version + "\""), + "build.gradle.kts should include the version:\n" + buildContent); + + 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); + Assertions.assertTrue(settingsContent.contains(artifact), + "settings.gradle.kts should include the artifact id:\n" + settingsContent); + + return buildContent; + } } 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 102944d06f218..1388198a2e5a0 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java @@ -183,9 +183,9 @@ public void testDevOptions() throws Exception { CliDriver.Result result = CliDriver.execute(workspaceRoot, "create", "app", "-e", "-B", "--verbose"); Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code." + result); - // 1 --clean --tests --suspend + // 1 --clean --tests --suspend --offline result = CliDriver.execute(project, "dev", "-e", "--dry-run", - "--clean", "--tests", "--debug", "--suspend", "--debug-mode=listen"); + "--clean", "--tests", "--debug", "--suspend", "--debug-mode=listen", "--offline"); Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code. Result:\n" + result); @@ -206,6 +206,9 @@ public void testDevOptions() throws Exception { Assertions.assertTrue(result.stdout.contains("-Dsuspend"), "mvn command should specify '-Dsuspend'\n" + result); + Assertions.assertTrue(result.stdout.contains("--offline"), + "mvn command should specify --offline\n" + result); + // 2 --no-clean --no-tests --no-debug result = CliDriver.execute(project, "dev", "-e", "--dry-run", "--no-clean", "--no-tests", "--no-debug"); diff --git a/devtools/cli/src/test/java/io/quarkus/cli/MavenProjectInfoAndUpdateTest.java b/devtools/cli/src/test/java/io/quarkus/cli/MavenProjectInfoAndUpdateTest.java index e7b79081b9658..7c216d99154c9 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/MavenProjectInfoAndUpdateTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/MavenProjectInfoAndUpdateTest.java @@ -2,52 +2,24 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.StringWriter; -import java.net.MalformedURLException; -import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; -import org.apache.maven.settings.Profile; -import org.apache.maven.settings.Repository; -import org.apache.maven.settings.RepositoryPolicy; -import org.apache.maven.settings.Settings; -import org.apache.maven.settings.io.DefaultSettingsReader; -import org.apache.maven.settings.io.DefaultSettingsWriter; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; -import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; -import io.quarkus.devtools.project.QuarkusProjectHelper; import io.quarkus.devtools.testing.registry.client.TestRegistryClientBuilder; import io.quarkus.maven.dependency.ArtifactCoords; -import io.quarkus.maven.dependency.GACTV; -import io.quarkus.registry.config.RegistriesConfigLocator; import picocli.CommandLine; -public class MavenProjectInfoAndUpdateTest { - - private static Path workDir; - private static Path settingsXml; - private static Path testRepo; - private static String prevConfigPath; - private static String prevRegistryClient; +public class MavenProjectInfoAndUpdateTest extends RegistryClientBuilderTestBase { @BeforeAll - static void setup() throws Exception { - workDir = Path.of(System.getProperty("user.dir")).resolve("target").resolve("test-work-dir"); - final Path registryConfigDir = workDir.resolve("registry"); - - final BootstrapMavenContext mavenContext = new BootstrapMavenContext( - BootstrapMavenContext.config().setWorkspaceDiscovery(false)); - + static void configureRegistryAndMavenRepo() { TestRegistryClientBuilder.newInstance() - .baseDir(registryConfigDir) + .baseDir(registryConfigDir()) .newRegistry("registry.acme.org") .newPlatform("org.acme.quarkus.platform") .newStream("2.0") @@ -73,142 +45,67 @@ static void setup() throws Exception { .registry() .clientBuilder() .build(); - - prevConfigPath = System.setProperty(RegistriesConfigLocator.CONFIG_FILE_PATH_PROPERTY, - registryConfigDir.resolve("config.yaml").toString()); - prevRegistryClient = System.setProperty("quarkusRegistryClient", "true"); - QuarkusProjectHelper.reset(); - - final Settings settings = getBaseMavenSettings(mavenContext.getUserSettings().toPath()); - - Profile profile = new Profile(); - settings.addActiveProfile("qs-test-registry"); - profile.setId("qs-test-registry"); - - Repository repo = configureRepo("original-local", - Path.of(mavenContext.getLocalRepo()).toUri().toURL().toExternalForm()); - profile.addRepository(repo); - profile.addPluginRepository(repo); - - settings.addProfile(profile); - repo = configureRepo("qs-test-registry", - TestRegistryClientBuilder.getMavenRepoDir(registryConfigDir).toUri().toURL().toExternalForm()); - profile.addRepository(repo); - profile.addPluginRepository(repo); - - settingsXml = workDir.resolve("settings.xml"); - try (BufferedWriter writer = Files.newBufferedWriter(settingsXml)) { - new DefaultSettingsWriter().write(writer, Collections.emptyMap(), settings); - } - testRepo = registryConfigDir.resolve("test-repo"); - } - - private static Repository configureRepo(String id, String url) - throws MalformedURLException, BootstrapMavenException { - final Repository repo = new Repository(); - repo.setId(id); - repo.setLayout("default"); - repo.setUrl(url); - RepositoryPolicy policy = new RepositoryPolicy(); - policy.setEnabled(true); - policy.setChecksumPolicy("ignore"); - policy.setUpdatePolicy("never"); - repo.setReleases(policy); - repo.setSnapshots(policy); - return repo; - } - - private static String getCurrentQuarkusVersion() { - String v = System.getProperty("project.version"); - if (v == null) { - throw new IllegalStateException("project.version property isn't available"); - } - return v; - } - - private static Settings getBaseMavenSettings(Path mavenSettings) throws IOException { - if (Files.exists(mavenSettings)) { - try (BufferedReader reader = Files.newBufferedReader(mavenSettings)) { - return new DefaultSettingsReader().read(reader, Collections.emptyMap()); - } - } - return new Settings(); - } - - private static void resetProperty(String name, String value) { - if (value == null) { - System.clearProperty(name); - } else { - System.setProperty(name, value); - } - } - - @AfterAll - static void cleanup() throws Exception { - //CliDriver.deleteDir(workDir); - resetProperty(RegistriesConfigLocator.CONFIG_FILE_PATH_PROPERTY, prevConfigPath); - resetProperty("quarkusRegistryClient", prevRegistryClient); } @Test void testClean() throws Exception { - final CliDriver.Result createResult = execute(workDir, "create", "acme-clean", + final CliDriver.Result createResult = run(workDir(), "create", "acme-clean", "-x supersonic,acme-quarkiverse-extension"); assertThat(createResult.exitCode).isEqualTo(CommandLine.ExitCode.OK) .as(() -> "Expected OK return code." + createResult); assertThat(createResult.stdout).contains("SUCCESS") .as(() -> "Expected confirmation that the project has been created." + createResult); - final Path projectDir = workDir.resolve("acme-clean"); - final CliDriver.Result infoResult = execute(projectDir, "info"); + final Path projectDir = workDir().resolve("acme-clean"); + final CliDriver.Result infoResult = run(projectDir, "info"); assertQuarkusPlatformBoms(infoResult.stdout, - GACTV.pom("org.acme.quarkus.platform", "quarkus-bom", "2.0.0"), - GACTV.pom("org.acme.quarkus.platform", "acme-bom", "2.0.0")); - assertPlatformBomExtensions(infoResult.stdout, GACTV.pom("org.acme.quarkus.platform", "quarkus-bom", "2.0.0"), - GACTV.jar("io.quarkus", "quarkus-arc", null)); - assertPlatformBomExtensions(infoResult.stdout, GACTV.pom("org.acme.quarkus.platform", "acme-bom", "2.0.0"), - GACTV.jar("org.acme.quarkus.platform", "acme-quarkus-supersonic", null)); + ArtifactCoords.pom("org.acme.quarkus.platform", "quarkus-bom", "2.0.0"), + ArtifactCoords.pom("org.acme.quarkus.platform", "acme-bom", "2.0.0")); + assertPlatformBomExtensions(infoResult.stdout, ArtifactCoords.pom("org.acme.quarkus.platform", "quarkus-bom", "2.0.0"), + ArtifactCoords.jar("io.quarkus", "quarkus-arc", null)); + assertPlatformBomExtensions(infoResult.stdout, ArtifactCoords.pom("org.acme.quarkus.platform", "acme-bom", "2.0.0"), + ArtifactCoords.jar("org.acme.quarkus.platform", "acme-quarkus-supersonic", null)); assertRegistryExtensions(infoResult.stdout, "registry.acme.org", - GACTV.jar("org.acme", "acme-quarkiverse-extension", "1.0")); + ArtifactCoords.jar("org.acme", "acme-quarkiverse-extension", "1.0")); - final CliDriver.Result updateResult = execute(projectDir, "update"); + final CliDriver.Result updateResult = run(projectDir, "update"); assertThat(updateResult.stdout).contains("[INFO] The project is up-to-date"); } @Test void testMissalignedPlatformExtensionVersion() throws Exception { - final CliDriver.Result createResult = execute(workDir, "create", "acme-misaligned-ext-version", + final CliDriver.Result createResult = run(workDir(), "create", "acme-misaligned-ext-version", "-x supersonic,acme-quarkiverse-extension,org.acme.quarkus.platform:acme-quarkus-subatomic:1.0.0"); assertThat(createResult.exitCode).isEqualTo(CommandLine.ExitCode.OK) .as(() -> "Expected OK return code." + createResult); assertThat(createResult.stdout).contains("SUCCESS") .as(() -> "Expected confirmation that the project has been created." + createResult); - Path projectDir = workDir.resolve("acme-misaligned-ext-version"); - final CliDriver.Result infoResult = execute(projectDir, "info"); + Path projectDir = workDir().resolve("acme-misaligned-ext-version"); + final CliDriver.Result infoResult = run(projectDir, "info"); assertQuarkusPlatformBoms(infoResult.stdout, - GACTV.pom("org.acme.quarkus.platform", "quarkus-bom", "2.0.0"), - GACTV.pom("org.acme.quarkus.platform", "acme-bom", "2.0.0")); - assertPlatformBomExtensions(infoResult.stdout, GACTV.pom("org.acme.quarkus.platform", "quarkus-bom", "2.0.0"), - GACTV.jar("io.quarkus", "quarkus-arc", null)); - assertPlatformBomExtensions(infoResult.stdout, GACTV.pom("org.acme.quarkus.platform", "acme-bom", "2.0.0"), - GACTV.jar("org.acme.quarkus.platform", "acme-quarkus-supersonic", null), - GACTV.jar("org.acme.quarkus.platform", "acme-quarkus-subatomic", "1.0.0 | misaligned")); + ArtifactCoords.pom("org.acme.quarkus.platform", "quarkus-bom", "2.0.0"), + ArtifactCoords.pom("org.acme.quarkus.platform", "acme-bom", "2.0.0")); + assertPlatformBomExtensions(infoResult.stdout, ArtifactCoords.pom("org.acme.quarkus.platform", "quarkus-bom", "2.0.0"), + ArtifactCoords.jar("io.quarkus", "quarkus-arc", null)); + assertPlatformBomExtensions(infoResult.stdout, ArtifactCoords.pom("org.acme.quarkus.platform", "acme-bom", "2.0.0"), + ArtifactCoords.jar("org.acme.quarkus.platform", "acme-quarkus-supersonic", null), + ArtifactCoords.jar("org.acme.quarkus.platform", "acme-quarkus-subatomic", "1.0.0 | misaligned")); assertRegistryExtensions(infoResult.stdout, "registry.acme.org", - GACTV.jar("org.acme", "acme-quarkiverse-extension", "1.0")); + ArtifactCoords.jar("org.acme", "acme-quarkiverse-extension", "1.0")); - final CliDriver.Result rectifyResult = execute(projectDir, "update", "--rectify"); + final CliDriver.Result rectifyResult = run(projectDir, "update", "--rectify"); assertThat(rectifyResult.stdout) .contains("[INFO] Update: org.acme.quarkus.platform:acme-quarkus-subatomic:1.0.0 -> remove version (managed)"); - final CliDriver.Result updateResult = execute(projectDir, "update", "-Dquarkus.platform.version=1.0.0"); + final CliDriver.Result updateResult = run(projectDir, "update", "-Dquarkus.platform.version=1.0.0"); assertQuarkusPlatformBomUpdates(updateResult.stdout, - GACTV.pom("org.acme.quarkus.platform", "quarkus-bom", "1.0.0 -> 2.0.0"), - GACTV.pom("org.acme.quarkus.platform", "acme-bom", "1.0.0 -> 2.0.0")); + ArtifactCoords.pom("org.acme.quarkus.platform", "quarkus-bom", "1.0.0 -> 2.0.0"), + ArtifactCoords.pom("org.acme.quarkus.platform", "acme-bom", "1.0.0 -> 2.0.0")); } private static void assertPlatformBomExtensions(String output, ArtifactCoords bom, ArtifactCoords... extensions) { @@ -298,13 +195,4 @@ private static void assertQuarkusPlatformBomUpdates(String output, ArtifactCoord } assertThat(output).contains(buf.getBuffer().toString()); } - - private CliDriver.Result execute(Path dir, String... args) throws Exception { - return CliDriver.builder() - .setStartingDir(dir) - .setMavenRepoLocal(testRepo.toString()) - .setMavenSettings(settingsXml.toString()) - .addArgs(args) - .execute(); - } } diff --git a/devtools/cli/src/test/java/io/quarkus/cli/NotRegisteredExtensionWithCodestartTest.java b/devtools/cli/src/test/java/io/quarkus/cli/NotRegisteredExtensionWithCodestartTest.java new file mode 100644 index 0000000000000..6b2df13dfaf04 --- /dev/null +++ b/devtools/cli/src/test/java/io/quarkus/cli/NotRegisteredExtensionWithCodestartTest.java @@ -0,0 +1,47 @@ +package io.quarkus.cli; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.quarkus.devtools.testing.registry.client.TestRegistryClientBuilder; +import picocli.CommandLine; + +public class NotRegisteredExtensionWithCodestartTest extends RegistryClientBuilderTestBase { + + @BeforeAll + static void configureRegistryAndMavenRepo() { + TestRegistryClientBuilder.newInstance() + .baseDir(registryConfigDir()) + .newRegistry("registry.acme.org") + .newPlatform("org.acme.quarkus.platform") + .newStream("2.0") + .newRelease("2.0.0") + .quarkusVersion(getCurrentQuarkusVersion()) + .addCoreMember() + .alignPluginsOnQuarkusVersion() + .addDefaultCodestartExtensions() + .registry() + .clientBuilder() + .addExternalExtensionWithCodestart("org.acme.quarkus", "acme-outlaw", "6.6.6") + .clientBuilder() + .build(); + } + + @Test + void test() throws Exception { + final CliDriver.Result createResult = run(workDir(), "create", "acme-outlaw-codestart", + "-x org.acme.quarkus:acme-outlaw:6.6.6"); + assertThat(createResult.exitCode).isEqualTo(CommandLine.ExitCode.OK) + .as(() -> "Expected OK return code." + createResult); + assertThat(createResult.stdout).contains("SUCCESS") + .as(() -> "Expected confirmation that the project has been created." + createResult); + + final Path acmeOutlawJava = workDir().resolve("acme-outlaw-codestart") + .resolve("src/main/java/org/acme/AcmeOutlaw.java"); + assertThat(acmeOutlawJava).exists(); + } +} diff --git a/devtools/cli/src/test/java/io/quarkus/cli/RegistryClientBuilderTestBase.java b/devtools/cli/src/test/java/io/quarkus/cli/RegistryClientBuilderTestBase.java new file mode 100644 index 0000000000000..8553d60ce7311 --- /dev/null +++ b/devtools/cli/src/test/java/io/quarkus/cli/RegistryClientBuilderTestBase.java @@ -0,0 +1,144 @@ +package io.quarkus.cli; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.net.MalformedURLException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; + +import org.apache.maven.settings.Profile; +import org.apache.maven.settings.Repository; +import org.apache.maven.settings.RepositoryPolicy; +import org.apache.maven.settings.Settings; +import org.apache.maven.settings.io.DefaultSettingsReader; +import org.apache.maven.settings.io.DefaultSettingsWriter; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; + +import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; +import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; +import io.quarkus.devtools.project.QuarkusProjectHelper; +import io.quarkus.devtools.testing.registry.client.TestRegistryClientBuilder; +import io.quarkus.registry.config.RegistriesConfigLocator; + +public abstract class RegistryClientBuilderTestBase { + + private static Path workDir; + private static Path settingsXml; + private static Path testRepo; + private static String prevConfigPath; + private static String prevRegistryClient; + + static Path workDir() { + if (workDir == null) { + var p = Path.of(System.getProperty("user.dir")).resolve("target").resolve("test-work-dir"); + try { + Files.createDirectories(p); + } catch (IOException e) { + throw new IllegalStateException("Failed to create work dir " + p); + } + workDir = p; + } + return workDir; + } + + static Path registryConfigDir() { + return workDir().resolve("registry"); + } + + @BeforeAll + static void setup() throws Exception { + final Path registryConfigDir = registryConfigDir(); + + prevConfigPath = System.setProperty(RegistriesConfigLocator.CONFIG_FILE_PATH_PROPERTY, + registryConfigDir.resolve("config.yaml").toString()); + prevRegistryClient = System.setProperty("quarkusRegistryClient", "true"); + QuarkusProjectHelper.reset(); + + final BootstrapMavenContext mavenContext = new BootstrapMavenContext( + BootstrapMavenContext.config().setWorkspaceDiscovery(false)); + final Settings settings = getBaseMavenSettings(mavenContext.getUserSettings().toPath()); + + Profile profile = new Profile(); + settings.addActiveProfile("qs-test-registry"); + profile.setId("qs-test-registry"); + + Repository repo = configureRepo("original-local", + Path.of(mavenContext.getLocalRepo()).toUri().toURL().toExternalForm()); + profile.addRepository(repo); + profile.addPluginRepository(repo); + + settings.addProfile(profile); + repo = configureRepo("qs-test-registry", + TestRegistryClientBuilder.getMavenRepoDir(registryConfigDir).toUri().toURL().toExternalForm()); + profile.addRepository(repo); + profile.addPluginRepository(repo); + + settingsXml = workDir().resolve("settings.xml"); + try (BufferedWriter writer = Files.newBufferedWriter(settingsXml)) { + new DefaultSettingsWriter().write(writer, Collections.emptyMap(), settings); + } + testRepo = registryConfigDir.resolve("test-repo"); + } + + private static Repository configureRepo(String id, String url) + throws MalformedURLException, BootstrapMavenException { + final Repository repo = new Repository(); + repo.setId(id); + repo.setLayout("default"); + repo.setUrl(url); + RepositoryPolicy policy = new RepositoryPolicy(); + policy.setEnabled(true); + policy.setChecksumPolicy("ignore"); + policy.setUpdatePolicy("never"); + repo.setReleases(policy); + repo.setSnapshots(policy); + return repo; + } + + protected static String getCurrentQuarkusVersion() { + String v = System.getProperty("project.version"); + if (v == null) { + throw new IllegalStateException("project.version property isn't available"); + } + return v; + } + + private static Settings getBaseMavenSettings(Path mavenSettings) throws IOException { + if (Files.exists(mavenSettings)) { + try (BufferedReader reader = Files.newBufferedReader(mavenSettings)) { + return new DefaultSettingsReader().read(reader, Collections.emptyMap()); + } + } + return new Settings(); + } + + private static void resetProperty(String name, String value) { + if (value == null) { + System.clearProperty(name); + } else { + System.setProperty(name, value); + } + } + + @AfterAll + static void cleanup() throws Exception { + CliDriver.deleteDir(workDir); + resetProperty(RegistriesConfigLocator.CONFIG_FILE_PATH_PROPERTY, prevConfigPath); + resetProperty("quarkusRegistryClient", prevRegistryClient); + workDir = null; + settingsXml = null; + testRepo = null; + } + + protected CliDriver.Result run(Path dir, String... args) throws Exception { + return CliDriver.builder() + .setStartingDir(dir) + .setMavenRepoLocal(testRepo.toString()) + .setMavenSettings(settingsXml.toString()) + .addArgs(args) + .execute(); + } +} diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/AppModelGradleResolver.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/AppModelGradleResolver.java index 478c34ea9cc4a..5aef8898fc840 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/AppModelGradleResolver.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/AppModelGradleResolver.java @@ -167,8 +167,8 @@ public ApplicationModel resolveManagedModel(ArtifactCoords appArtifact, protected void ensureProjectCoords(ArtifactCoords appArtifact) throws AppModelResolverException { if (project.getName().equals(appArtifact.getArtifactId()) - && appArtifact.getGroupId().equals(appArtifact.getGroupId()) - && appArtifact.getVersion().equals(appArtifact.getVersion())) { + && project.getGroup().toString().equals(appArtifact.getGroupId()) + && project.getVersion().toString().equals(appArtifact.getVersion())) { return; } throw new AppModelResolverException( diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java index c3d56ef86049b..ca27c9449fdfd 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java @@ -105,6 +105,9 @@ public void apply(Project project) { // register extension final QuarkusPluginExtension quarkusExt = project.getExtensions().create(EXTENSION_NAME, QuarkusPluginExtension.class, project); + // register plugin + project.getPluginManager().apply(JavaPlugin.class); + registerTasks(project, quarkusExt); } @@ -332,6 +335,11 @@ private void createConfigurations(Project project) { .extendsFrom(configContainer.findByName(JavaPlugin.TEST_RUNTIME_ONLY_CONFIGURATION_NAME)); ApplicationDeploymentClasspathBuilder.initConfigurations(project); + + // Also initialize the configurations that are specific to a LaunchMode + for (LaunchMode launchMode : LaunchMode.values()) { + new ApplicationDeploymentClasspathBuilder(project, launchMode); + } } private Set getSourcesParents(SourceSet mainSourceSet) { diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/dsl/CompilerOption.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/dsl/CompilerOption.java new file mode 100644 index 0000000000000..9f740a34017f8 --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/dsl/CompilerOption.java @@ -0,0 +1,28 @@ +package io.quarkus.gradle.dsl; + +import java.util.ArrayList; +import java.util.List; + +public class CompilerOption { + + private final String name; + private final List opts = new ArrayList<>(0); + + public CompilerOption(String name) { + this.name = name; + } + + public CompilerOption args(List options) { + opts.addAll(options); + return this; + } + + public String getName() { + return name; + } + + public List getArgs() { + return opts; + } + +} diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/dsl/CompilerOptions.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/dsl/CompilerOptions.java new file mode 100644 index 0000000000000..f4d7169524d59 --- /dev/null +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/dsl/CompilerOptions.java @@ -0,0 +1,20 @@ +package io.quarkus.gradle.dsl; + +import java.util.ArrayList; +import java.util.List; + +public class CompilerOptions { + + private final List compilerOptions = new ArrayList<>(1); + + public CompilerOption compiler(String name) { + CompilerOption compilerOption = new CompilerOption(name); + compilerOptions.add(compilerOption); + return compilerOption; + } + + public List getCompilerOptions() { + return compilerOptions; + } + +} diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuild.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuild.java index eb36e4f63558e..4e0ebddde3e86 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuild.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuild.java @@ -124,8 +124,6 @@ public File getFastJar() { @TaskAction public void buildQuarkus() { - getLogger().lifecycle("building quarkus jar"); - final ApplicationModel appModel; try { appModel = extension().getAppModelResolver().resolveModel(new GACTV(getProject().getGroup().toString(), 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 c2e481183a7bd..c87d690f71ff6 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 @@ -20,6 +20,7 @@ import javax.inject.Inject; import org.apache.tools.ant.types.Commandline; +import org.gradle.api.Action; import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; @@ -33,6 +34,7 @@ import org.gradle.api.tasks.CompileClasspath; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputDirectory; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.compile.JavaCompile; @@ -52,6 +54,8 @@ import io.quarkus.deployment.dev.DevModeContext; import io.quarkus.deployment.dev.DevModeMain; import io.quarkus.deployment.dev.QuarkusDevModeLauncher; +import io.quarkus.gradle.dsl.CompilerOption; +import io.quarkus.gradle.dsl.CompilerOptions; import io.quarkus.gradle.tooling.ToolingUtils; import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.ResolvedDependency; @@ -80,6 +84,8 @@ public class QuarkusDev extends QuarkusTask { private List compilerArgs = new LinkedList<>(); + private CompilerOptions compilerOptions = new CompilerOptions(); + private boolean shouldPropagateJavaCompilerArgs = true; @Inject @@ -192,6 +198,16 @@ public void setCompilerArgs(List compilerArgs) { this.compilerArgs = compilerArgs; } + @Internal + public CompilerOptions getCompilerOptions() { + return this.compilerOptions; + } + + public QuarkusDev compilerOptions(Action action) { + action.execute(compilerOptions); + return this; + } + @TaskAction public void startDev() { if (!getSourceDir().isDirectory()) { @@ -313,12 +329,16 @@ private QuarkusDevModeLauncher newLauncher() throws Exception { builder.targetJavaVersion(javaPluginConvention.getTargetCompatibility().toString()); } + for (CompilerOption compilerOptions : compilerOptions.getCompilerOptions()) { + builder.compilerOptions(compilerOptions.getName(), compilerOptions.getArgs()); + } + if (getCompilerArgs().isEmpty() && shouldPropagateJavaCompilerArgs) { getJavaCompileTask() .map(compileTask -> compileTask.getOptions().getCompilerArgs()) - .ifPresent(builder::compilerOptions); + .ifPresent(args -> builder.compilerOptions("java", args)); } else { - builder.compilerOptions(getCompilerArgs()); + builder.compilerOptions("java", getCompilerArgs()); } modifyDevModeContext(builder); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusGenerateCode.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusGenerateCode.java index 0021c5ea00556..8a3889f0424f0 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusGenerateCode.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusGenerateCode.java @@ -92,8 +92,6 @@ public File getGeneratedOutputDirectory() { @TaskAction public void prepareQuarkus() { - getLogger().lifecycle("preparing quarkus application"); - LaunchMode launchMode = test ? LaunchMode.TEST : devMode ? LaunchMode.DEVELOPMENT : LaunchMode.NORMAL; final ApplicationModel appModel = extension().getApplicationModel(launchMode); final Properties realProperties = getBuildSystemProperties(appModel.getAppArtifact()); diff --git a/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/QuarkusExtensionConfiguration.java b/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/QuarkusExtensionConfiguration.java index 219c5afe1479e..e93d892da4479 100644 --- a/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/QuarkusExtensionConfiguration.java +++ b/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/QuarkusExtensionConfiguration.java @@ -4,99 +4,113 @@ import org.gradle.api.Action; import org.gradle.api.Project; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; import io.quarkus.extension.gradle.dsl.Capabilities; import io.quarkus.extension.gradle.dsl.Capability; public class QuarkusExtensionConfiguration { - private boolean disableValidation; - private String deploymentArtifact; - private String deploymentModule; - private List excludedArtifacts; - private List parentFirstArtifacts; - private List runnerParentFirstArtifacts; - private List lesserPriorityArtifacts; - private List conditionalDependencies; - private List dependencyCondition; + private Property disableValidation; + private Property deploymentArtifact; + private Property deploymentModule; + private ListProperty excludedArtifacts; + private ListProperty parentFirstArtifacts; + private ListProperty runnerParentFirstArtifacts; + private ListProperty lesserPriorityArtifacts; + private ListProperty conditionalDependencies; + private ListProperty dependencyCondition; private Capabilities capabilities = new Capabilities(); private Project project; public QuarkusExtensionConfiguration(Project project) { this.project = project; + disableValidation = project.getObjects().property(Boolean.class); + disableValidation.convention(false); + deploymentArtifact = project.getObjects().property(String.class); + deploymentModule = project.getObjects().property(String.class); + deploymentModule.convention("deployment"); + + excludedArtifacts = project.getObjects().listProperty(String.class); + parentFirstArtifacts = project.getObjects().listProperty(String.class); + runnerParentFirstArtifacts = project.getObjects().listProperty(String.class); + lesserPriorityArtifacts = project.getObjects().listProperty(String.class); + conditionalDependencies = project.getObjects().listProperty(String.class); + dependencyCondition = project.getObjects().listProperty(String.class); } public void setDisableValidation(boolean disableValidation) { - this.disableValidation = disableValidation; + this.disableValidation.set(disableValidation); } - public boolean isValidationDisabled() { + public Property isValidationDisabled() { return disableValidation; } - public String getDeploymentArtifact() { + public Property getDeploymentArtifact() { return deploymentArtifact; } public void setDeploymentArtifact(String deploymentArtifact) { - this.deploymentArtifact = deploymentArtifact; + this.deploymentArtifact.set(deploymentArtifact); } - public String getDeploymentModule() { + public Property getDeploymentModule() { return deploymentModule; } public void setDeploymentModule(String deploymentModule) { - this.deploymentModule = deploymentModule; + this.deploymentModule.set(deploymentModule); } - public List getExcludedArtifacts() { + public ListProperty getExcludedArtifacts() { return excludedArtifacts; } public void setExcludedArtifacts(List excludedArtifacts) { - this.excludedArtifacts = excludedArtifacts; + this.excludedArtifacts.addAll(excludedArtifacts); } - public List getParentFirstArtifacts() { + public ListProperty getParentFirstArtifacts() { return parentFirstArtifacts; } public void setParentFirstArtifacts(List parentFirstArtifacts) { - this.parentFirstArtifacts = parentFirstArtifacts; + this.parentFirstArtifacts.addAll(parentFirstArtifacts); } - public List getRunnerParentFirstArtifacts() { + public ListProperty getRunnerParentFirstArtifacts() { return runnerParentFirstArtifacts; } public void setRunnerParentFirstArtifacts(List runnerParentFirstArtifacts) { - this.runnerParentFirstArtifacts = runnerParentFirstArtifacts; + this.runnerParentFirstArtifacts.addAll(runnerParentFirstArtifacts); } - public List getLesserPriorityArtifacts() { + public ListProperty getLesserPriorityArtifacts() { return lesserPriorityArtifacts; } public void setLesserPriorityArtifacts(List lesserPriorityArtifacts) { - this.lesserPriorityArtifacts = lesserPriorityArtifacts; + this.lesserPriorityArtifacts.addAll(lesserPriorityArtifacts); } - public List getConditionalDependencies() { + public ListProperty getConditionalDependencies() { return conditionalDependencies; } public void setConditionalDependencies(List conditionalDependencies) { - this.conditionalDependencies = conditionalDependencies; + this.conditionalDependencies.addAll(conditionalDependencies); } - public List getDependencyConditions() { + public ListProperty getDependencyConditions() { return dependencyCondition; } public void setDependencyConditions(List dependencyCondition) { - this.dependencyCondition = dependencyCondition; + this.dependencyCondition.addAll(dependencyCondition); } public List getProvidedCapabilities() { diff --git a/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/QuarkusExtensionPlugin.java b/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/QuarkusExtensionPlugin.java index a60551dc5956f..53748e12a4481 100644 --- a/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/QuarkusExtensionPlugin.java +++ b/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/QuarkusExtensionPlugin.java @@ -38,30 +38,23 @@ public class QuarkusExtensionPlugin implements Plugin { public void apply(Project project) { final QuarkusExtensionConfiguration quarkusExt = project.getExtensions().create(EXTENSION_CONFIGURATION_NAME, QuarkusExtensionConfiguration.class); + project.getPluginManager().apply(JavaPlugin.class); registerTasks(project, quarkusExt); } private void registerTasks(Project project, QuarkusExtensionConfiguration quarkusExt) { TaskContainer tasks = project.getTasks(); + + JavaPluginConvention convention = project.getConvention().getPlugin(JavaPluginConvention.class); + SourceSet mainSourceSet = convention.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME); Configuration runtimeModuleClasspath = project.getConfigurations() .getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); TaskProvider extensionDescriptorTask = tasks.register(EXTENSION_DESCRIPTOR_TASK_NAME, - ExtensionDescriptorTask.class, task -> { - JavaPluginConvention convention = project.getConvention().getPlugin(JavaPluginConvention.class); - SourceSet mainSourceSet = convention.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME); - task.setOutputResourcesDir(mainSourceSet.getOutput().getResourcesDir()); - task.setInputResourcesDir(mainSourceSet.getResources().getSourceDirectories().getAsPath()); - task.setQuarkusExtensionConfiguration(quarkusExt); - task.setClasspath(runtimeModuleClasspath); - }); + ExtensionDescriptorTask.class, quarkusExt, mainSourceSet, runtimeModuleClasspath); TaskProvider validateExtensionTask = tasks.register(VALIDATE_EXTENSION_TASK_NAME, - ValidateExtensionTask.class, task -> { - task.setRuntimeModuleClasspath(runtimeModuleClasspath); - task.setQuarkusExtensionConfiguration(quarkusExt); - task.onlyIf(t -> !quarkusExt.isValidationDisabled()); - }); + ValidateExtensionTask.class, quarkusExt, runtimeModuleClasspath); project.getPlugins().withType( JavaPlugin.class, @@ -130,7 +123,7 @@ private void addAnnotationProcessorDependency(Project project) { private Project findDeploymentProject(Project project, QuarkusExtensionConfiguration configuration) { - String deploymentProjectName = configuration.getDeploymentModule(); + String deploymentProjectName = configuration.getDeploymentModule().get(); if (deploymentProjectName == null) { deploymentProjectName = DEFAULT_DEPLOYMENT_PROJECT_NAME; } @@ -142,7 +135,7 @@ private Project findDeploymentProject(Project project, QuarkusExtensionConfigura } if (deploymentProject == null) { project.getLogger().warn("Unable to find deployment project with name: " + deploymentProjectName - + ". You can configure the deployment project name by setting the 'deploymentArtifact' property in the plugin extension."); + + ". You can configure the deployment project name by setting the 'deploymentModule' property in the plugin extension."); } } return deploymentProject; diff --git a/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ExtensionDescriptorTask.java b/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ExtensionDescriptorTask.java index 4532277251b2b..2c70cae042424 100644 --- a/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ExtensionDescriptorTask.java +++ b/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ExtensionDescriptorTask.java @@ -13,12 +13,15 @@ import java.util.Properties; import java.util.Set; +import javax.inject.Inject; + import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ModuleVersionIdentifier; import org.gradle.api.artifacts.ResolvedArtifact; import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.TaskAction; import com.fasterxml.jackson.core.util.DefaultIndenter; @@ -44,34 +47,26 @@ */ public class ExtensionDescriptorTask extends DefaultTask { - private QuarkusExtensionConfiguration quarkusExtensionConfiguration; - private Configuration classpath; - private File outputResourcesDir; - private String inputResourcesDir; + private final QuarkusExtensionConfiguration quarkusExtensionConfiguration; + private final Configuration classpath; + private final File outputResourcesDir; + private final String inputResourcesDir; private static final String GROUP_ID = "group-id"; private static final String ARTIFACT_ID = "artifact-id"; private static final String METADATA = "metadata"; - public ExtensionDescriptorTask() { + @Inject + public ExtensionDescriptorTask(QuarkusExtensionConfiguration quarkusExtensionConfiguration, SourceSet mainSourceSet, + Configuration runtimeClasspath) { + setDescription("Generate extension descriptor file"); setGroup("quarkus"); - } - public void setOutputResourcesDir(File outputResourcesDir) { - this.outputResourcesDir = outputResourcesDir; - } - - public void setInputResourcesDir(String inputResourcesDir) { - this.inputResourcesDir = inputResourcesDir; - } - - public void setQuarkusExtensionConfiguration(QuarkusExtensionConfiguration quarkusExtensionConfiguration) { this.quarkusExtensionConfiguration = quarkusExtensionConfiguration; - } - - public void setClasspath(Configuration classpath) { - this.classpath = classpath; + this.outputResourcesDir = mainSourceSet.getOutput().getResourcesDir(); + this.inputResourcesDir = mainSourceSet.getResources().getSourceDirectories().getAsPath(); + this.classpath = runtimeClasspath; } @Classpath @@ -90,13 +85,12 @@ public void generateExtensionDescriptor() throws IOException { private void generateQuarkusExtensionProperties(Path metaInfDir) { final Properties props = new Properties(); - String deploymentArtifact = quarkusExtensionConfiguration.getDeploymentArtifact(); - if (quarkusExtensionConfiguration.getDeploymentArtifact() == null) { - deploymentArtifact = quarkusExtensionConfiguration.getDefaultDeployementArtifactName(); - } + String deploymentArtifact = quarkusExtensionConfiguration.getDeploymentArtifact() + .getOrElse(quarkusExtensionConfiguration.getDefaultDeployementArtifactName()); + props.setProperty(BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT, deploymentArtifact); - List conditionalDependencies = quarkusExtensionConfiguration.getConditionalDependencies(); + List conditionalDependencies = quarkusExtensionConfiguration.getConditionalDependencies().get(); if (conditionalDependencies != null && !conditionalDependencies.isEmpty()) { final StringBuilder buf = new StringBuilder(); int i = 0; @@ -107,7 +101,7 @@ private void generateQuarkusExtensionProperties(Path metaInfDir) { props.setProperty(BootstrapConstants.CONDITIONAL_DEPENDENCIES, buf.toString()); } - List dependencyConditions = quarkusExtensionConfiguration.getDependencyConditions(); + List dependencyConditions = quarkusExtensionConfiguration.getDependencyConditions().get(); if (dependencyConditions != null && !dependencyConditions.isEmpty()) { final StringBuilder buf = new StringBuilder(); int i = 0; @@ -118,25 +112,25 @@ private void generateQuarkusExtensionProperties(Path metaInfDir) { props.setProperty(BootstrapConstants.DEPENDENCY_CONDITION, buf.toString()); } - List parentFirstArtifacts = quarkusExtensionConfiguration.getParentFirstArtifacts(); + List parentFirstArtifacts = quarkusExtensionConfiguration.getParentFirstArtifacts().get(); if (parentFirstArtifacts != null && !parentFirstArtifacts.isEmpty()) { String val = String.join(",", parentFirstArtifacts); props.put(AppModel.PARENT_FIRST_ARTIFACTS, val); } - List runnerParentFirstArtifacts = quarkusExtensionConfiguration.getRunnerParentFirstArtifacts(); + List runnerParentFirstArtifacts = quarkusExtensionConfiguration.getRunnerParentFirstArtifacts().get(); if (runnerParentFirstArtifacts != null && !runnerParentFirstArtifacts.isEmpty()) { String val = String.join(",", runnerParentFirstArtifacts); props.put(AppModel.RUNNER_PARENT_FIRST_ARTIFACTS, val); } - List excludedArtifacts = quarkusExtensionConfiguration.getExcludedArtifacts(); + List excludedArtifacts = quarkusExtensionConfiguration.getExcludedArtifacts().get(); if (excludedArtifacts != null && !excludedArtifacts.isEmpty()) { String val = String.join(",", excludedArtifacts); props.put(AppModel.EXCLUDED_ARTIFACTS, val); } - List lesserPriorityArtifacts = quarkusExtensionConfiguration.getLesserPriorityArtifacts(); + List lesserPriorityArtifacts = quarkusExtensionConfiguration.getLesserPriorityArtifacts().get(); if (lesserPriorityArtifacts != null && !lesserPriorityArtifacts.isEmpty()) { String val = String.join(",", lesserPriorityArtifacts); props.put(AppModel.LESSER_PRIORITY_ARTIFACTS, val); diff --git a/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ValidateExtensionTask.java b/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ValidateExtensionTask.java index 1eb0f28d56d0d..be96802daf78d 100644 --- a/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ValidateExtensionTask.java +++ b/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ValidateExtensionTask.java @@ -4,6 +4,8 @@ import java.util.List; import java.util.Set; +import javax.inject.Inject; + import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; import org.gradle.api.artifacts.Configuration; @@ -20,17 +22,17 @@ public class ValidateExtensionTask extends DefaultTask { - private QuarkusExtensionConfiguration extensionConfiguration; private Configuration runtimeModuleClasspath; private Configuration deploymentModuleClasspath; - public ValidateExtensionTask() { + @Inject + public ValidateExtensionTask(QuarkusExtensionConfiguration quarkusExtensionConfiguration, + Configuration runtimeModuleClasspath) { setDescription("Validate extension dependencies"); setGroup("quarkus"); - } - public void setQuarkusExtensionConfiguration(QuarkusExtensionConfiguration extensionConfiguration) { - this.extensionConfiguration = extensionConfiguration; + this.runtimeModuleClasspath = runtimeModuleClasspath; + this.onlyIf(t -> !quarkusExtensionConfiguration.isValidationDisabled().get()); } @Internal @@ -38,10 +40,6 @@ public Configuration getRuntimeModuleClasspath() { return this.runtimeModuleClasspath; } - public void setRuntimeModuleClasspath(Configuration runtimeModuleClasspath) { - this.runtimeModuleClasspath = runtimeModuleClasspath; - } - @Internal public Configuration getDeploymentModuleClasspath() { return this.deploymentModuleClasspath; diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java index 8b6e31b5feeba..c456a82b2aa4d 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java @@ -2,24 +2,35 @@ import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; +import java.util.stream.Collectors; +import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ModuleDependency; +import org.gradle.api.artifacts.ModuleIdentifier; import org.gradle.api.artifacts.ModuleVersionIdentifier; import org.gradle.api.artifacts.ResolvedArtifact; import org.gradle.api.artifacts.ResolvedDependency; import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.internal.artifacts.dependencies.DefaultDependencyArtifact; +import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency; import org.gradle.api.plugins.JavaPlugin; +import io.quarkus.bootstrap.BootstrapConstants; +import io.quarkus.bootstrap.model.PlatformImports; +import io.quarkus.bootstrap.model.PlatformImportsImpl; +import io.quarkus.bootstrap.resolver.AppModelResolverException; import io.quarkus.gradle.tooling.ToolingUtils; import io.quarkus.gradle.tooling.dependency.DependencyUtils; import io.quarkus.gradle.tooling.dependency.ExtensionDependency; import io.quarkus.gradle.tooling.dependency.LocalExtensionDependency; -import io.quarkus.maven.dependency.Dependency; import io.quarkus.runtime.LaunchMode; public class ApplicationDeploymentClasspathBuilder { @@ -88,70 +99,163 @@ public static void initConfigurations(Project project) { } private final Project project; - private final Collection enforcedPlatforms; private final LaunchMode mode; - private ConditionalDependenciesEnabler cdEnabler; - private Configuration runtimeConfig; - public ApplicationDeploymentClasspathBuilder(Project project, LaunchMode mode, - Collection enforcedPlatforms) { + private final String runtimeConfigurationName; + private final String platformConfigurationName; + private final String deploymentConfigurationName; + /** + * The platform configuration updates the PlatformImports, but since the PlatformImports don't + * have a place to be stored in the project, they're stored here. The way that extensions are + * tracked and conditional dependencies needs some attention, which will likely resolve this. + */ + private static final HashMap platformImports = new HashMap<>(); + /** + * The key used to look up the correct PlatformImports that matches the platformConfigurationName + */ + private final String platformImportName; + + public ApplicationDeploymentClasspathBuilder(Project project, LaunchMode mode) { this.project = project; this.mode = mode; - this.enforcedPlatforms = enforcedPlatforms; + this.runtimeConfigurationName = getFinalRuntimeConfigName(mode); + this.platformConfigurationName = ToolingUtils.toPlatformConfigurationName(this.runtimeConfigurationName); + this.deploymentConfigurationName = ToolingUtils.toDeploymentConfigurationName(this.runtimeConfigurationName); + this.platformImportName = project.getPath() + ":" + this.platformConfigurationName; + + setUpPlatformConfiguration(); + setUpRuntimeConfiguration(); + setUpDeploymentConfiguration(); } - public Configuration getRuntimeConfiguration() { - if (runtimeConfig == null) { - final String configName = getFinalRuntimeConfigName(mode); - runtimeConfig = project.getConfigurations().findByName(configName); - if (runtimeConfig == null) { - runtimeConfig = DependencyUtils.duplicateConfiguration(project, configName, - getConditionalDependenciesEnabler().getBaseRuntimeConfiguration()); - } + private void setUpPlatformConfiguration() { + if (project.getConfigurations().findByName(this.platformConfigurationName) == null) { + PlatformImportsImpl platformImports = ApplicationDeploymentClasspathBuilder.platformImports + .computeIfAbsent(this.platformImportName, (ignored) -> new PlatformImportsImpl()); + + project.getConfigurations().create(this.platformConfigurationName, configuration -> { + // Platform configuration is just implementation, filtered to platform dependencies + configuration.getDependencies().addAllLater(project.provider(() -> project.getConfigurations() + .getByName(JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME) + .getAllDependencies() + .stream() + .filter(dependency -> dependency instanceof ModuleDependency && + ToolingUtils.isEnforcedPlatform((ModuleDependency) dependency)) + .collect(Collectors.toList()))); + // Configures PlatformImportsImpl once the platform configuration is resolved + configuration.getResolutionStrategy().eachDependency(d -> { + ModuleIdentifier identifier = d.getTarget().getModule(); + final String group = identifier.getGroup(); + final String name = identifier.getName(); + if (name.endsWith(BootstrapConstants.PLATFORM_DESCRIPTOR_ARTIFACT_ID_SUFFIX)) { + platformImports.addPlatformDescriptor(group, name, d.getTarget().getVersion(), "json", + d.getTarget().getVersion()); + } else if (name.endsWith(BootstrapConstants.PLATFORM_PROPERTIES_ARTIFACT_ID_SUFFIX)) { + final DefaultDependencyArtifact dep = new DefaultDependencyArtifact(); + dep.setExtension("properties"); + dep.setType("properties"); + dep.setName(name); + + final DefaultExternalModuleDependency gradleDep = new DefaultExternalModuleDependency( + group, name, d.getTarget().getVersion(), null); + gradleDep.addArtifact(dep); + + for (ResolvedArtifact a : project.getConfigurations().detachedConfiguration(gradleDep) + .getResolvedConfiguration().getResolvedArtifacts()) { + if (a.getName().equals(name)) { + try { + platformImports.addPlatformProperties(group, name, null, "properties", + d.getTarget().getVersion(), + a.getFile().toPath()); + } catch (AppModelResolverException e) { + throw new GradleException("Failed to import platform properties " + a.getFile(), e); + } + break; + } + } + } + }); + }); } - return runtimeConfig; } - public Configuration getDeploymentConfiguration() { - String deploymentConfigurationName = ToolingUtils.toDeploymentConfigurationName(runtimeConfig.getName()); - Configuration deploymentConfig = project.getConfigurations().findByName(deploymentConfigurationName); - if (deploymentConfig != null) { - return deploymentConfig; + private void setUpRuntimeConfiguration() { + if (project.getConfigurations().findByName(this.runtimeConfigurationName) == null) { + project.getConfigurations().create(this.runtimeConfigurationName, configuration -> configuration.extendsFrom( + project.getConfigurations() + .getByName(ApplicationDeploymentClasspathBuilder.getBaseRuntimeConfigName(mode)))); } + } - final Collection allExtensions = getConditionalDependenciesEnabler().getAllExtensions(); - Set extensions = collectFirstMetQuarkusExtensions(getRuntimeConfiguration(), allExtensions); - // Add conditional extensions - for (ExtensionDependency knownExtension : allExtensions) { - if (knownExtension.isConditional()) { - extensions.add(knownExtension); - } - } + private void setUpDeploymentConfiguration() { + if (project.getConfigurations().findByName(this.deploymentConfigurationName) == null) { + project.getConfigurations().create(this.deploymentConfigurationName, configuration -> { + Configuration enforcedPlatforms = this.getPlatformConfiguration(); + configuration.extendsFrom(enforcedPlatforms); + configuration.getDependencies().addAllLater(project.provider(() -> { + ConditionalDependenciesEnabler cdEnabler = new ConditionalDependenciesEnabler(project, mode, + enforcedPlatforms); + final Collection allExtensions = cdEnabler.getAllExtensions(); + Set extensions = collectFirstMetQuarkusExtensions(getRawRuntimeConfiguration(), + allExtensions); + // Add conditional extensions + for (ExtensionDependency knownExtension : allExtensions) { + if (knownExtension.isConditional()) { + extensions.add(knownExtension); + } + } - return project.getConfigurations().create(deploymentConfigurationName, config -> { - config.withDependencies(ds -> ds.addAll(enforcedPlatforms)); - - final Set alreadyProcessed = new HashSet<>(extensions.size()); - final DependencyHandler dependencies = project.getDependencies(); - for (ExtensionDependency extension : extensions) { - if (extension instanceof LocalExtensionDependency) { - DependencyUtils.addLocalDeploymentDependency(deploymentConfigurationName, - (LocalExtensionDependency) extension, dependencies); - } else { - if (!alreadyProcessed.add(extension.getExtensionId())) { - continue; + final Set alreadyProcessed = new HashSet<>(extensions.size()); + final DependencyHandler dependencies = project.getDependencies(); + final Set deploymentDependencies = new HashSet<>(); + for (ExtensionDependency extension : extensions) { + if (extension instanceof LocalExtensionDependency) { + LocalExtensionDependency localExtensionDependency = (LocalExtensionDependency) extension; + deploymentDependencies.add( + dependencies.project(Collections.singletonMap("path", + localExtensionDependency.findDeploymentModulePath()))); + } else { + if (!alreadyProcessed.add(extension.getExtensionId())) { + continue; + } + deploymentDependencies.add(dependencies.create( + extension.getDeploymentModule().getGroupId() + ":" + + extension.getDeploymentModule().getArtifactId() + ":" + + extension.getDeploymentModule().getVersion())); + } } - DependencyUtils.requireDeploymentDependency(deploymentConfigurationName, extension, dependencies); - } - } - }); + return deploymentDependencies; + })); + }); + } } - private ConditionalDependenciesEnabler getConditionalDependenciesEnabler() { - if (cdEnabler == null) { - cdEnabler = new ConditionalDependenciesEnabler(project, mode, enforcedPlatforms); - } - return cdEnabler; + public Configuration getPlatformConfiguration() { + return project.getConfigurations().getByName(this.platformConfigurationName); + } + + private Configuration getRawRuntimeConfiguration() { + return project.getConfigurations().getByName(this.runtimeConfigurationName); + } + + /** + * Forces deployment configuration to resolve to discover conditional dependencies. + */ + public Configuration getRuntimeConfiguration() { + this.getDeploymentConfiguration().resolve(); + return project.getConfigurations().getByName(this.runtimeConfigurationName); + } + + public Configuration getDeploymentConfiguration() { + return project.getConfigurations().getByName(this.deploymentConfigurationName); + } + + /** + * Forces the platform configuration to resolve and then uses that to populate platform imports. + */ + public PlatformImports getPlatformImports() { + this.getPlatformConfiguration().getResolvedConfiguration(); + return platformImports.get(this.platformImportName); } private Set collectFirstMetQuarkusExtensions(Configuration configuration, diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java index 8a86bff297c30..d70a4861227ae 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java @@ -23,37 +23,51 @@ public class ConditionalDependenciesEnabler { + /** + * Links dependencies to extensions + */ private final Map> featureVariants = new HashMap<>(); - private final Configuration baseRuntimeConfig; + /** + * Despite its name, only contains extensions which have no conditional dependencies, or have + * resolved their conditional dependencies. + */ private final Map allExtensions = new HashMap<>(); private final Project project; - private final Collection enforcedPlatforms; + private final Configuration enforcedPlatforms; private final Set existingArtifacts = new HashSet<>(); private final List unsatisfiedConditionalDeps = new ArrayList<>(); public ConditionalDependenciesEnabler(Project project, LaunchMode mode, - Collection platforms) { + Configuration platforms) { this.project = project; this.enforcedPlatforms = platforms; - baseRuntimeConfig = project.getConfigurations() + // Get runtimeClasspath (quarkusProdBaseRuntimeClasspathConfiguration to be exact) + Configuration baseRuntimeConfig = project.getConfigurations() .getByName(ApplicationDeploymentClasspathBuilder.getBaseRuntimeConfigName(mode)); if (!baseRuntimeConfig.getIncoming().getDependencies().isEmpty()) { + // Gather all extensions from the full resolved dependency tree collectConditionalDependencies(baseRuntimeConfig.getResolvedConfiguration().getResolvedArtifacts()); + // If there are any extensions which had unresolved conditional dependencies: while (!unsatisfiedConditionalDeps.isEmpty()) { boolean satisfiedConditionalDeps = false; final int originalUnsatisfiedCount = unsatisfiedConditionalDeps.size(); int i = 0; + // Go through each unsatisfied/unresolved dependency once: while (i < unsatisfiedConditionalDeps.size()) { final Dependency conditionalDep = unsatisfiedConditionalDeps.get(i); + // Try to resolve it with the latest evolved graph available if (resolveConditionalDependency(conditionalDep)) { + // Mark the resolution as a success so we know the graph evolved satisfiedConditionalDeps = true; unsatisfiedConditionalDeps.remove(i); } else { + // No resolution (yet) or graph evolution; move on to the next ++i; } } + // If we didn't resolve any dependencies and the graph did not evolve, give up. if (!satisfiedConditionalDeps && unsatisfiedConditionalDeps.size() == originalUnsatisfiedCount) { break; } @@ -63,10 +77,6 @@ public ConditionalDependenciesEnabler(Project project, LaunchMode mode, } - public Configuration getBaseRuntimeConfiguration() { - return baseRuntimeConfig; - } - public Collection getAllExtensions() { return allExtensions.values(); } @@ -78,12 +88,17 @@ private void reset() { } private void collectConditionalDependencies(Set runtimeArtifacts) { + // For every artifact in the dependency graph: for (ResolvedArtifact artifact : runtimeArtifacts) { + // Add to master list of artifacts: existingArtifacts.add(getKey(artifact)); ExtensionDependency extension = DependencyUtils.getExtensionInfoOrNull(project, artifact); + // If this artifact represents an extension: if (extension != null) { + // Add to master list of accepted extensions: allExtensions.put(extension.getExtensionId(), extension); for (Dependency conditionalDep : extension.getConditionalDependencies()) { + // If the dependency is not present yet in the graph, queue it for resolution later if (!exists(conditionalDep)) { queueConditionalDependency(extension, conditionalDep); } @@ -98,11 +113,16 @@ private boolean resolveConditionalDependency(Dependency conditionalDep) { Set resolvedArtifacts = conditionalDeps.getResolvedConfiguration().getResolvedArtifacts(); boolean satisfied = false; + // Resolved artifacts don't have great linking back to the original artifact, so I think + // this loop is trying to find the artifact that represents the original conditional + // dependency for (ResolvedArtifact artifact : resolvedArtifacts) { if (conditionalDep.getName().equals(artifact.getName()) && conditionalDep.getVersion().equals(artifact.getModuleVersion().getId().getVersion()) && artifact.getModuleVersion().getId().getGroup().equals(conditionalDep.getGroup())) { + // Once the dependency is found, reload the extension info from within final ExtensionDependency extensionDependency = DependencyUtils.getExtensionInfoOrNull(project, artifact); + // Now check if this conditional dependency is resolved given the latest graph evolution if (extensionDependency != null && (extensionDependency.getDependencyConditions().isEmpty() || exist(extensionDependency.getDependencyConditions()))) { satisfied = true; @@ -112,19 +132,25 @@ private boolean resolveConditionalDependency(Dependency conditionalDep) { } } + // No resolution (yet); give up. if (!satisfied) { return false; } + // The conditional dependency resolved! Let's now add all of /its/ dependencies for (ResolvedArtifact artifact : resolvedArtifacts) { + // First add the artifact to the master list existingArtifacts.add(getKey(artifact)); ExtensionDependency extensionDependency = DependencyUtils.getExtensionInfoOrNull(project, artifact); if (extensionDependency == null) { continue; } + // If this artifact represents an extension, mark this one as a conditional extension extensionDependency.setConditional(true); + // Add to the master list of accepted extensions allExtensions.put(extensionDependency.getExtensionId(), extensionDependency); for (Dependency cd : extensionDependency.getConditionalDependencies()) { + // Add any unsatisfied/unresolved conditional dependencies of this dependency to the queue if (!exists(cd)) { queueConditionalDependency(extensionDependency, cd); } @@ -134,6 +160,8 @@ private boolean resolveConditionalDependency(Dependency conditionalDep) { } private void queueConditionalDependency(ExtensionDependency extension, Dependency conditionalDep) { + // 1. Add to master list of unresolved/unsatisfied dependencies + // 2. Add map entry to link dependency to extension featureVariants.computeIfAbsent(getFeatureKey(conditionalDep), k -> { unsatisfiedConditionalDeps.add(conditionalDep); return new HashSet<>(); @@ -141,10 +169,11 @@ private void queueConditionalDependency(ExtensionDependency extension, Dependenc } private Configuration createConditionalDependenciesConfiguration(Project project, Dependency conditionalDep) { - final List deps = new ArrayList<>(enforcedPlatforms.size() + 1); - deps.addAll(enforcedPlatforms); - deps.add(conditionalDep); - return project.getConfigurations().detachedConfiguration(deps.toArray(new Dependency[0])); + Configuration conditionalDepConfiguration = project.getConfigurations() + .detachedConfiguration() + .extendsFrom(enforcedPlatforms); + conditionalDepConfiguration.getDependencies().add(conditionalDep); + return conditionalDepConfiguration; } private void enableConditionalDependency(ModuleVersionIdentifier dependency) { 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 66b5372437bc1..9e13a8996bf6f 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 @@ -26,8 +26,6 @@ import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileTree; import org.gradle.api.initialization.IncludedBuild; -import org.gradle.api.internal.artifacts.dependencies.DefaultDependencyArtifact; -import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency; import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.compile.AbstractCompile; @@ -39,10 +37,8 @@ import io.quarkus.bootstrap.model.ApplicationModelBuilder; import io.quarkus.bootstrap.model.CapabilityContract; import io.quarkus.bootstrap.model.PlatformImports; -import io.quarkus.bootstrap.model.PlatformImportsImpl; import io.quarkus.bootstrap.model.gradle.ModelParameter; import io.quarkus.bootstrap.model.gradle.impl.ModelParameterImpl; -import io.quarkus.bootstrap.resolver.AppModelResolverException; import io.quarkus.bootstrap.workspace.ArtifactSources; import io.quarkus.bootstrap.workspace.DefaultArtifactSources; import io.quarkus.bootstrap.workspace.DefaultSourceDir; @@ -54,7 +50,6 @@ import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactDependency; import io.quarkus.maven.dependency.ArtifactKey; -import io.quarkus.maven.dependency.Dependency; import io.quarkus.maven.dependency.DependencyFlags; import io.quarkus.maven.dependency.GACT; import io.quarkus.maven.dependency.GACTV; @@ -98,8 +93,10 @@ public Object buildAll(String modelName, Project project) { public Object buildAll(String modelName, ModelParameter parameter, Project project) { final LaunchMode mode = LaunchMode.valueOf(parameter.getMode()); - final List enforcedPlatforms = ToolingUtils.getEnforcedPlatforms(project); - final PlatformImports platformImports = resolvePlatformImports(project, enforcedPlatforms); + final ApplicationDeploymentClasspathBuilder classpathBuilder = new ApplicationDeploymentClasspathBuilder(project, mode); + final Configuration classpathConfig = classpathBuilder.getRuntimeConfiguration(); + final Configuration deploymentConfig = classpathBuilder.getDeploymentConfiguration(); + final PlatformImports platformImports = classpathBuilder.getPlatformImports(); final ResolvedDependency appArtifact = getProjectArtifact(project, mode); final ApplicationModelBuilder modelBuilder = new ApplicationModelBuilder() @@ -107,11 +104,6 @@ public Object buildAll(String modelName, ModelParameter parameter, Project proje .addReloadableWorkspaceModule(appArtifact.getKey()) .setPlatformImports(platformImports); - final ApplicationDeploymentClasspathBuilder classpathBuilder = new ApplicationDeploymentClasspathBuilder(project, mode, - enforcedPlatforms); - final Configuration classpathConfig = classpathBuilder.getRuntimeConfiguration(); - final Configuration deploymentConfig = classpathBuilder.getDeploymentConfiguration(); - final Map appDependencies = new LinkedHashMap<>(); collectDependencies(classpathConfig.getResolvedConfiguration(), mode, project, appDependencies, modelBuilder, appArtifact.getWorkspaceModule().mutable()); @@ -167,46 +159,6 @@ private static void collectDestinationDirs(Collection sources, final } } - private PlatformImports resolvePlatformImports(Project project, - List deploymentDeps) { - final Configuration boms = project.getConfigurations() - .detachedConfiguration(deploymentDeps.toArray(new org.gradle.api.artifacts.Dependency[0])); - final PlatformImportsImpl platformImports = new PlatformImportsImpl(); - boms.getResolutionStrategy().eachDependency(d -> { - final String group = d.getTarget().getGroup(); - final String name = d.getTarget().getName(); - if (name.endsWith(BootstrapConstants.PLATFORM_DESCRIPTOR_ARTIFACT_ID_SUFFIX)) { - platformImports.addPlatformDescriptor(group, name, d.getTarget().getVersion(), "json", - d.getTarget().getVersion()); - } else if (name.endsWith(BootstrapConstants.PLATFORM_PROPERTIES_ARTIFACT_ID_SUFFIX)) { - final DefaultDependencyArtifact dep = new DefaultDependencyArtifact(); - dep.setExtension("properties"); - dep.setType("properties"); - dep.setName(name); - - final DefaultExternalModuleDependency gradleDep = new DefaultExternalModuleDependency( - group, name, d.getTarget().getVersion(), null); - gradleDep.addArtifact(dep); - - for (ResolvedArtifact a : project.getConfigurations().detachedConfiguration(gradleDep) - .getResolvedConfiguration().getResolvedArtifacts()) { - if (a.getName().equals(name)) { - try { - platformImports.addPlatformProperties(group, name, null, "properties", d.getTarget().getVersion(), - a.getFile().toPath()); - } catch (AppModelResolverException e) { - throw new GradleException("Failed to import platform properties " + a.getFile(), e); - } - break; - } - } - } - - }); - boms.getResolvedConfiguration(); - return platformImports; - } - private void collectExtensionDependencies(Project project, Configuration deploymentConfiguration, Map appDependencies) { final ResolvedConfiguration rc = deploymentConfiguration.getResolvedConfiguration(); diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java index e66d138e39abe..9e7e501ae811a 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/ToolingUtils.java @@ -4,18 +4,13 @@ import java.io.ObjectOutputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.UnknownDomainObjectException; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.Dependency; import org.gradle.api.artifacts.ModuleDependency; import org.gradle.api.attributes.Category; import org.gradle.api.initialization.IncludedBuild; -import org.gradle.api.plugins.JavaPlugin; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.model.gradle.ModelParameter; @@ -25,29 +20,15 @@ public class ToolingUtils { private static final String DEPLOYMENT_CONFIGURATION_SUFFIX = "Deployment"; + private static final String PLATFORM_CONFIGURATION_SUFFIX = "Platform"; public static final String DEV_MODE_CONFIGURATION_NAME = "quarkusDev"; public static String toDeploymentConfigurationName(String baseConfigurationName) { return baseConfigurationName + DEPLOYMENT_CONFIGURATION_SUFFIX; } - public static List getEnforcedPlatforms(Project project) { - return getEnforcedPlatforms(project.getConfigurations() - .getByName(JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME)); - } - - public static List getEnforcedPlatforms(Configuration config) { - final List directExtension = new ArrayList<>(); - for (Dependency d : config.getAllDependencies()) { - if (!(d instanceof ModuleDependency)) { - continue; - } - final ModuleDependency module = (ModuleDependency) d; - if (isEnforcedPlatform(module)) { - directExtension.add(d); - } - } - return directExtension; + public static String toPlatformConfigurationName(String baseConfigurationName) { + return baseConfigurationName + PLATFORM_CONFIGURATION_SUFFIX; } public static boolean isEnforcedPlatform(ModuleDependency module) { diff --git a/devtools/gradle/settings.gradle b/devtools/gradle/settings.gradle index 987f1cfbac2e0..747988914609d 100644 --- a/devtools/gradle/settings.gradle +++ b/devtools/gradle/settings.gradle @@ -1,5 +1,5 @@ plugins { - id "com.gradle.enterprise" version "3.9" + id "com.gradle.enterprise" version "3.10.1" } gradleEnterprise { diff --git a/devtools/maven/pom.xml b/devtools/maven/pom.xml index 8332ebae9bee9..b0c7e5ad3d6b7 100644 --- a/devtools/maven/pom.xml +++ b/devtools/maven/pom.xml @@ -237,5 +237,28 @@ + + + jakarta-rewrite + + + jakarta-rewrite + + + + + + org.openrewrite.maven + rewrite-maven-plugin + + + io.quarkus.jakarta-json-switch + + + + + + + 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 b4f94a9767b54..3dbe6b3931e78 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java @@ -218,8 +218,7 @@ public void execute() throws MojoExecutionException { // fall back to the default platform catalogResolver = ExtensionCatalogResolver.empty(); } - - final ExtensionCatalog catalog = resolveExtensionsCatalog(this, bomGroupId, bomArtifactId, bomVersion, catalogResolver, + ExtensionCatalog catalog = resolveExtensionsCatalog(this, bomGroupId, bomArtifactId, bomVersion, catalogResolver, mvn, log); File projectRoot = outputDirectory; @@ -273,6 +272,7 @@ public void execute() throws MojoExecutionException { final Path projectDirPath = projectRoot.toPath(); try { extensions = CreateProjectHelper.sanitizeExtensions(extensions); + catalog = CreateProjectHelper.completeCatalog(catalog, extensions, mvn); final SourceType sourceType = CreateProjectHelper.determineSourceType(extensions); sanitizeOptions(sourceType); diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index bf11d1cc7990e..b60ba3e7999d9 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -94,6 +94,7 @@ import io.quarkus.deployment.dev.DevModeMain; import io.quarkus.deployment.dev.QuarkusDevModeLauncher; import io.quarkus.maven.MavenDevModeLauncher.Builder; +import io.quarkus.maven.components.CompilerOptions; import io.quarkus.maven.components.MavenVersionEnforcer; import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.GACT; @@ -307,6 +308,12 @@ public class DevMojo extends AbstractMojo { @Parameter private List compilerArgs; + /** + * Additional compiler arguments + */ + @Parameter + private List compilerOptions; + /** * The --release argument to javac. */ @@ -723,12 +730,16 @@ private String getSourceEncoding() { private void addProject(MavenDevModeLauncher.Builder builder, ResolvedDependency module, boolean root) throws Exception { + if (!ArtifactCoords.TYPE_JAR.equals(module.getType())) { + return; + } + String projectDirectory; Set sourcePaths; String classesPath = null; Set resourcePaths; Set testSourcePaths; - String testClassesPath; + String testClassesPath = null; Set testResourcePaths; List activeProfiles = Collections.emptyList(); @@ -738,6 +749,10 @@ private void addProject(MavenDevModeLauncher.Builder builder, ResolvedDependency : null; final ArtifactSources sources = module.getSources(); if (mavenProject == null) { + if (sources == null) { + getLog().debug("Local dependency " + module.toCompactCoords() + " does not appear to have any sources"); + return; + } projectDirectory = module.getWorkspaceModule().getModuleDir().getAbsolutePath(); sourcePaths = new LinkedHashSet<>(); for (SourceDir src : sources.getSourceDirs()) { @@ -768,28 +783,33 @@ private void addProject(MavenDevModeLauncher.Builder builder, ResolvedDependency } final Path sourceParent; - if (sources.getSourceDirs() == null) { - if (sources.getResourceDirs() == null) { - throw new MojoExecutionException("The project does not appear to contain any sources or resources"); + if (sourcePaths.isEmpty()) { + if (sources == null || sources.getResourceDirs() == null) { + throw new MojoExecutionException( + "Local dependency " + module.toCompactCoords() + " does not appear to have any sources"); } sourceParent = sources.getResourceDirs().iterator().next().getDir().toAbsolutePath().getParent(); } else { - sourceParent = sources.getSourceDirs().iterator().next().getDir().toAbsolutePath().getParent(); + sourceParent = sourcePaths.iterator().next().toAbsolutePath().getParent(); } - Path classesDir = sources.getSourceDirs().iterator().next().getOutputDir().toAbsolutePath(); - if (Files.isDirectory(classesDir)) { - classesPath = classesDir.toString(); - } - Path testClassesDir = module.getWorkspaceModule().getTestSources().getSourceDirs().iterator().next().getOutputDir() - .toAbsolutePath(); - testClassesPath = testClassesDir.toString(); - + Path classesDir = null; resourcePaths = new LinkedHashSet<>(); - for (SourceDir src : sources.getResourceDirs()) { - for (Path p : src.getSourceTree().getRoots()) { - resourcePaths.add(p.toAbsolutePath()); + if (sources != null) { + classesDir = sources.getSourceDirs().iterator().next().getOutputDir().toAbsolutePath(); + if (Files.isDirectory(classesDir)) { + classesPath = classesDir.toString(); } + for (SourceDir src : sources.getResourceDirs()) { + for (Path p : src.getSourceTree().getRoots()) { + resourcePaths.add(p.toAbsolutePath()); + } + } + } + if (module.getWorkspaceModule().hasTestSources()) { + Path testClassesDir = module.getWorkspaceModule().getTestSources().getSourceDirs().iterator().next().getOutputDir() + .toAbsolutePath(); + testClassesPath = testClassesDir.toString(); } testResourcePaths = new LinkedHashSet<>(); @@ -809,13 +829,13 @@ private void addProject(MavenDevModeLauncher.Builder builder, ResolvedDependency resourcePaths.addAll( build.getResources().stream() .map(Resource::getDirectory) - .map(Paths::get) + .map(Path::of) .map(Path::toAbsolutePath) .collect(Collectors.toList())); testResourcePaths.addAll( build.getTestResources().stream() .map(Resource::getDirectory) - .map(Paths::get) + .map(Path::of) .map(Path::toAbsolutePath) .collect(Collectors.toList())); } @@ -823,10 +843,11 @@ private void addProject(MavenDevModeLauncher.Builder builder, ResolvedDependency if (classesPath == null && (!sourcePaths.isEmpty() || !resourcePaths.isEmpty())) { throw new MojoExecutionException("Hot reloadable dependency " + module.getWorkspaceModule().getId() - + " has not been compiled yet (the classes directory " + classesDir + " does not exist)"); + + " has not been compiled yet (the classes directory " + (classesDir == null ? "" : classesDir) + + " does not exist)"); } - Path targetDir = Paths.get(project.getBuild().getDirectory()); + Path targetDir = Path.of(project.getBuild().getDirectory()); DevModeContext.ModuleInfo moduleInfo = new DevModeContext.ModuleInfo.Builder() .setArtifactKey(module.getKey()) @@ -950,11 +971,17 @@ private QuarkusDevModeLauncher newLauncher() throws Exception { builder.sourceEncoding(getSourceEncoding()); + if (compilerOptions != null) { + for (CompilerOptions compilerOption : compilerOptions) { + builder.compilerOptions(compilerOption.getName(), compilerOption.getArgs()); + } + } + // Set compilation flags. Try the explicitly given configuration first. Otherwise, // refer to the configuration of the Maven Compiler Plugin. final Optional compilerPluginConfiguration = findCompilerPluginConfiguration(); if (compilerArgs != null) { - builder.compilerOptions(compilerArgs); + builder.compilerOptions("java", compilerArgs); } else if (compilerPluginConfiguration.isPresent()) { final Xpp3Dom compilerPluginArgsConfiguration = compilerPluginConfiguration.get().getChild("compilerArgs"); if (compilerPluginArgsConfiguration != null) { @@ -967,9 +994,10 @@ private QuarkusDevModeLauncher newLauncher() throws Exception { && !compilerPluginArgsConfiguration.getValue().isEmpty()) { compilerPluginArgs.add(compilerPluginArgsConfiguration.getValue().trim()); } - builder.compilerOptions(compilerPluginArgs); + builder.compilerOptions("java", compilerPluginArgs); } } + if (release != null) { builder.releaseJavaVersion(release); } else if (compilerPluginConfiguration.isPresent()) { @@ -997,7 +1025,6 @@ private QuarkusDevModeLauncher newLauncher() throws Exception { bootstrapProvider.close(); } else { final MavenArtifactResolver.Builder resolverBuilder = MavenArtifactResolver.builder() - .setRepositorySystem(repoSystem) .setRemoteRepositories(repos) .setRemoteRepositoryManager(remoteRepositoryManager) .setWorkspaceDiscovery(true) @@ -1009,16 +1036,18 @@ private QuarkusDevModeLauncher newLauncher() throws Exception { boolean reinitializeMavenSession = Files.exists(appModelLocation); if (reinitializeMavenSession) { Files.delete(appModelLocation); + // we can't re-use the repo system because we want to use our interpolating model builder + // a use-case where it fails with the original repo system is when dev mode is launched with -Dquarkus.platform.version=xxx + // overriding the version of the quarkus-bom in the pom.xml } else { // we can re-use the original Maven session - resolverBuilder.setRepositorySystemSession(repoSession); + resolverBuilder.setRepositorySystemSession(repoSession).setRepositorySystem(repoSystem); } appModel = new BootstrapAppModelResolver(resolverBuilder.build()) .setDevMode(true) .setCollectReloadableDependencies(!noDeps) - .resolveModel(new GACTV(project.getGroupId(), project.getArtifactId(), null, ArtifactCoords.TYPE_JAR, - project.getVersion())); + .resolveModel(GACTV.jar(project.getGroupId(), project.getArtifactId(), project.getVersion())); } // serialize the app model to avoid re-resolving it in the dev process diff --git a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectStateMojoBase.java b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectStateMojoBase.java index 8ab8dd012030e..13bc548363aa0 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectStateMojoBase.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectStateMojoBase.java @@ -29,7 +29,6 @@ import io.quarkus.devtools.project.QuarkusProject; import io.quarkus.devtools.project.QuarkusProjectHelper; import io.quarkus.maven.dependency.ArtifactCoords; -import io.quarkus.maven.dependency.GACTV; public abstract class QuarkusProjectStateMojoBase extends QuarkusProjectMojoBase { @@ -75,10 +74,10 @@ public void doExecute(QuarkusProject quarkusProject, MessageWriter log) throws M protected ApplicationModel resolveApplicationModel() throws MojoExecutionException { try { return new BootstrapAppModelResolver(artifactResolver()) - .resolveModel(GACTV.pom(project.getGroupId(), project.getArtifactId(), project.getVersion())); + .resolveModel(ArtifactCoords.pom(project.getGroupId(), project.getArtifactId(), project.getVersion())); } catch (AppModelResolverException e) { throw new MojoExecutionException("Failed to resolve the Quarkus application model for project " - + GACTV.pom(project.getGroupId(), project.getArtifactId(), project.getVersion()), e); + + ArtifactCoords.pom(project.getGroupId(), project.getArtifactId(), project.getVersion()), e); } } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/components/CompilerOptions.java b/devtools/maven/src/main/java/io/quarkus/maven/components/CompilerOptions.java new file mode 100644 index 0000000000000..bd41be53efcff --- /dev/null +++ b/devtools/maven/src/main/java/io/quarkus/maven/components/CompilerOptions.java @@ -0,0 +1,27 @@ +package io.quarkus.maven.components; + +import java.util.ArrayList; +import java.util.List; + +public class CompilerOptions { + + private String name = null; + private List args = new ArrayList<>(); + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setArgs(List options) { + this.args = options; + } + + public List getArgs() { + return args; + } + +} diff --git a/devtools/platform-descriptor-json-plugin/pom.xml b/devtools/platform-descriptor-json-plugin/pom.xml index 3959a47c3c77c..30de3e219b414 100644 --- a/devtools/platform-descriptor-json-plugin/pom.xml +++ b/devtools/platform-descriptor-json-plugin/pom.xml @@ -64,4 +64,28 @@ - \ No newline at end of file + + + + jakarta-rewrite + + + jakarta-rewrite + + + + + + org.openrewrite.maven + rewrite-maven-plugin + + + io.quarkus.jakarta-json-switch + + + + + + + + diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/amazon-lambda-example/java/src/test/java/org/acme/lambda/LambdaHandlerTest.java b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/amazon-lambda-example/java/src/test/java/org/acme/lambda/LambdaHandlerTest.java index d214926e70acf..95252e2a2e9b2 100644 --- a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/amazon-lambda-example/java/src/test/java/org/acme/lambda/LambdaHandlerTest.java +++ b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/amazon-lambda-example/java/src/test/java/org/acme/lambda/LambdaHandlerTest.java @@ -12,7 +12,7 @@ public class LambdaHandlerTest { @Test public void testSimpleLambdaSuccess() throws Exception { - // you test your lambas by invoking on http://localhost:8081 + // you test your lambdas by invoking on http://localhost:8081 // this works in dev mode too Person in = new Person(); diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/funqy-amazon-lambda-example/java/src/test/java/org/acme/funqy/FunqyTest.java b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/funqy-amazon-lambda-example/java/src/test/java/org/acme/funqy/FunqyTest.java index e3bb72f0eb24b..24d660e94898a 100644 --- a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/funqy-amazon-lambda-example/java/src/test/java/org/acme/funqy/FunqyTest.java +++ b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/funqy-amazon-lambda-example/java/src/test/java/org/acme/funqy/FunqyTest.java @@ -11,7 +11,7 @@ public class FunqyTest { @Test public void testFunqyLambda() throws Exception { - // you test your lambas by invoking on http://localhost:8081 + // you test your lambdas by invoking on http://localhost:8081 // this works in dev mode too Person in = new Person(); diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/google-cloud-functions-example/base/README.tpl.qute.md b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/google-cloud-functions-example/base/README.tpl.qute.md old mode 100755 new mode 100644 diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/google-cloud-functions-example/codestart.yml b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/google-cloud-functions-example/codestart.yml old mode 100755 new mode 100644 diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/google-cloud-functions-example/java/src/main/java/org/acme/googlecloudfunctions/HelloWorldBackgroundFunction.java b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/google-cloud-functions-example/java/src/main/java/org/acme/googlecloudfunctions/HelloWorldBackgroundFunction.java old mode 100755 new mode 100644 diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/google-cloud-functions-example/java/src/main/java/org/acme/googlecloudfunctions/HelloWorldCloudEventsFunction.java b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/google-cloud-functions-example/java/src/main/java/org/acme/googlecloudfunctions/HelloWorldCloudEventsFunction.java old mode 100755 new mode 100644 diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/google-cloud-functions-example/java/src/main/java/org/acme/googlecloudfunctions/HelloWorldHttpFunction.java b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/examples/google-cloud-functions-example/java/src/main/java/org/acme/googlecloudfunctions/HelloWorldHttpFunction.java old mode 100755 new mode 100644 diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/config-yaml-codestart/base/src/main/resources/application.yml b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/config-yaml-codestart/base/src/main/resources/application.yml old mode 100755 new mode 100644 diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/config-yaml-codestart/kotlin/src/main/kotlin/org/acme/GreetingConfig.kt b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/config-yaml-codestart/kotlin/src/main/kotlin/org/acme/GreetingConfig.kt old mode 100755 new mode 100644 diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/smallrye-health-codestart/kotlin/src/main/kotlin/org/acme/MyLivenessCheck.kt b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/smallrye-health-codestart/kotlin/src/main/kotlin/org/acme/MyLivenessCheck.kt old mode 100755 new mode 100644 diff --git a/docs/pom.xml b/docs/pom.xml index e9c1aaf4ef8cc..5af2c1b1781fe 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -54,6 +54,12 @@ + + + org.asciidoctor + asciidoctorj + ${asciidoctorj.version} + @@ -252,6 +258,19 @@ + + io.quarkus + quarkus-confluent-registry-avro-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-container-image-deployment @@ -603,6 +622,19 @@ + + io.quarkus + quarkus-hal-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-hibernate-envers-deployment @@ -2868,6 +2900,13 @@ true true + + + + io.quarkus.docs.generation.TooltipInlineMacroProcessor + tooltip + + @@ -2877,6 +2916,12 @@ asciidoctorj ${asciidoctorj.version} + + + ${project.groupId} + ${project.artifactId} + ${project.version} + diff --git a/docs/src/main/asciidoc/amazon-lambda.adoc b/docs/src/main/asciidoc/amazon-lambda.adoc index 9cb20bd7351e9..b4e73ed6f4bd5 100644 --- a/docs/src/main/asciidoc/amazon-lambda.adoc +++ b/docs/src/main/asciidoc/amazon-lambda.adoc @@ -390,7 +390,7 @@ sam local invoke --template target/sam.native.yaml --event payload.json == Modifying `function.zip` -The are times where you may have to add some additions to the `function.zip` lambda deployment that is generated +There are times where you may have to add some additions to the `function.zip` lambda deployment that is generated by the build. To do this create a `zip.jvm` or `zip.native` directory within `src/main`. Create `zip.jvm/` if you are doing a pure Java lambda. `zip.native/` if you are doing a native deployment. diff --git a/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc b/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc index beb991b3fc9ac..630bf70254a9b 100644 --- a/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc +++ b/docs/src/main/asciidoc/apicurio-registry-dev-services.adoc @@ -7,9 +7,18 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::./attributes.adoc[] -If the `quarkus-apicurio-registry-avro` extension is present, Dev Services for Apicurio Registry automatically starts an Apicurio Registry instance in dev mode and when running tests. +If an extension for schema registry, such as `quarkus-apicurio-registry-avro` or `quarkus-confluent-registry-avro`, is present, Dev Services for Apicurio Registry automatically starts an Apicurio Registry instance in dev mode and when running tests. Also, all Kafka channels in SmallRye Reactive Messaging are automatically configured to use this registry. -(This automatic configuration of course only applies to serializers and deserializers from the Apicurio Registry Avro library.) +This automatic configuration only applies to serializers and deserializers from Apicurio Registry serde libraries and Confluent Schema Registry serde libraries, because there properties are set: + +[source,properties] +---- +# for Apicurio Registry serde +mp.messaging.connector.smallrye-kafka.apicurio.registry.url=http://localhost:8081/apis/registry/v2 +# for Confluent Schema Registry serde +mp.messaging.connector.smallrye-kafka.schema.registry.url=http://localhost:8081/apis/ccompat/v6 +---- + == Enabling / Disabling Dev Services for Apicurio Registry @@ -17,15 +26,24 @@ Dev Services for Apicurio Registry is automatically enabled unless: - `quarkus.apicurio-registry.devservices.enabled` is set to `false` - `mp.messaging.connector.smallrye-kafka.apicurio.registry.url` is configured -- all the Reactive Messaging Kafka channels have the `apicurio.registry.url` attribute set +- `mp.messaging.connector.smallrye-kafka.schema.registry.url` is configured +- all the Reactive Messaging Kafka channels have either the `apicurio.registry.url` attribute or the `schema.registry.url` attribute set Dev Services for Apicurio Registry relies on Docker to start the registry. If your environment does not support Docker, you will need to start the registry manually, or use an already running registry. -You can configure the registry URL for all Kafka channels in SmallRye Reactive Messaging with a single property: +In such case, you can configure the registry URL for all Kafka channels in SmallRye Reactive Messaging with a single property. +For Apicurio Registry serde, that is: [source,properties] ---- -mp.messaging.connector.smallrye-kafka.apicurio.registry.url=http://localhost:8081/apis/registry/v2 +mp.messaging.connector.smallrye-kafka.apicurio.registry.url=... your Apicuio Registry URL... +---- + +For Confluent Schema Registry serde, that is: + +[source,properties] +---- +mp.messaging.connector.smallrye-kafka.schema.registry.url=... your Confluent Schema Registry URL... ---- == Shared registry diff --git a/docs/src/main/asciidoc/building-my-first-extension.adoc b/docs/src/main/asciidoc/building-my-first-extension.adoc index 761cbf2147bc9..7ae69185458bd 100644 --- a/docs/src/main/asciidoc/building-my-first-extension.adoc +++ b/docs/src/main/asciidoc/building-my-first-extension.adoc @@ -849,7 +849,7 @@ mvn io.quarkus.platform:quarkus-maven-plugin:{quarkus-version}:create \ -DprojectGroupId=org.acme \ -DprojectArtifactId=greeting-app \ -Dextensions="org.acme:greeting-extension:1.0.0-SNAPSHOT" \ - -DnoExamples + -DnoCode ---- `cd` into `greeting-app`. diff --git a/docs/src/main/asciidoc/building-native-image.adoc b/docs/src/main/asciidoc/building-native-image.adoc index 4f9114a929bad..91e5f1a366f2d 100644 --- a/docs/src/main/asciidoc/building-native-image.adoc +++ b/docs/src/main/asciidoc/building-native-image.adoc @@ -229,55 +229,7 @@ You can do so by prepending the flag with `-J` and passing it as additional nati == Testing the native executable -Producing a native executable can lead to a few issues, and so it's also a good idea to run some tests against the application running in the native file. - -In the `pom.xml` file, the `native` profile contains: - -[source, xml] ----- - - org.apache.maven.plugins - maven-failsafe-plugin - ${surefire-plugin.version} - - - - integration-test - verify - - - - ${project.build.directory}/${project.build.finalName}-runner - org.jboss.logmanager.LogManager - ${maven.home} - - - - - ----- - -This instructs the failsafe-maven-plugin to run integration-test and indicates the location of the produced native executable. - -Then, open the `src/test/java/org/acme/quickstart/GreetingResourceIT.java`. It contains: - -[source,java] ----- -package org.acme.quickstart; - - -import io.quarkus.test.junit.QuarkusIntegrationTest; - -@QuarkusIntegrationTest // <1> -public class GreetingResourceIT extends GreetingResourceTest { // <2> - - // Run the same tests - -} ----- -<1> Use another test runner that starts the application from the native file before the tests. -The executable is retrieved using the `native.image.path` system property configured in the _Failsafe Maven Plugin_. -<2> We extend our previous tests, but you can also implement your tests +Producing a native executable can lead to a few issues, and so it's also a good idea to run some tests against the application running in the native file. The reasoning is explained in the link:getting-started-testing#quarkus-integration-test[Testing Guide]. To see the `GreetingResourceIT` run against the native executable, use `./mvnw verify -Pnative`: [source,shell] @@ -319,12 +271,38 @@ This procedure was formerly accomplished using the `@NativeImageTest` annotation capabilities of `@NativeImageTest`. More information about `@QuarkusIntegrationTest` can be found in the xref:getting-started-testing.adoc#quarkus-integration-test[Testing Guide]. ==== -By default, integration tests runs using the `prod` profile. -This can be overridden using the `quarkus.test.native-image-profile` property. -For example, in your `application.properties` file, add: `quarkus.test.native-image-profile=test`. -Alternatively, you can run your tests with: `./mvnw verify -Pnative -Dquarkus.test.native-image-profile=test`. -However, don't forget that when the native executable is built the `prod` profile is enabled. -So, the profile you enable this way must be compatible with the produced executable. +=== Profiles +By default, integration tests both *build* and *run* the native executable using the `prod` profile. + +You can override the profile the executable *runs* with during the test using the `quarkus.test.native-image-profile` property. +Either by adding it to `application.properties` or by appending it to the command line: +`./mvnw verify -Pnative -Dquarkus.test.native-image-profile=test`. +Your `%test.` prefixed properties will be used at the test runtime. + + +You can override the profile the executable is *built* with and *runs* with using the `quarkus-profile=test` property, e.g. +`./mvnw clean verify -Pnative -Dquarkus-profile=test`. This might come handy if there are test specific resources to be processed, +such as importing test data into the database. + +``` +quarkus.native.resources.includes=version.txt +%test.quarkus.native.resources.includes=version.txt,import-dev.sql +%test.quarkus.hibernate-orm.database.generation=drop-and-create +%test.quarkus.hibernate-orm.sql-load-script=import-dev.sql +``` + +With the aforementioned example in your `application.properties`, your Hibernate ORM managed database will be populated with test +data both during the JVM mode test run and during the native mode test run. The production +executable will contain only the `version.txt` resource, no superfluous test data. + +[WARNING] +==== +The executable built with `-Dquarkus-profile=test` is not suitable for production deployment. +It contains your test resources files and settings. Once the testing is done, the executable would have to be built again, +using the default, `prod` profile. +==== + +=== Java preview features [[graal-test-preview]] [NOTE] @@ -342,14 +320,18 @@ you can only test via HTTP calls. Your test code does not actually run natively, that does not call your HTTP endpoints, it's probably not a good idea to run them as part of native tests. If you share your test class between JVM and native executions like we advise above, you can mark certain tests -with the `@DisabledOnNativeImage` annotation in order to only run them on the JVM. +with the `@DisabledOnIntegrationTest` annotation in order to skip them when testing against a native image. +[NOTE] +==== +Using `@DisabledOnIntegrationTest` will also disable the test in all integration test instances, including +testing the application in JVM mode, in a container image, and native image. +==== === Testing an existing native executable It is also possible to re-run the tests against a native executable that has already been built. To do this run -`./mvnw test-compile failsafe:integration-test`. This will discover the existing native image and run the tests against it using -failsafe. +`./mvnw test-compile failsafe:integration-test -Pnative`. This will discover the existing native image and run the tests against it using failsafe. If the process cannot find the native image for some reason, or you want to test a native image that is no longer in the target directory you can specify the executable with the `-Dnative.image.path=` system property. @@ -566,6 +548,8 @@ Sample Dockerfile for building with Gradle: ---- ## Stage 1 : build with maven builder image with native capabilities FROM quay.io/quarkus/ubi-quarkus-native-image:{graalvm-flavor} AS build +USER root +RUN microdnf install findutils COPY --chown=quarkus:quarkus gradlew /code/gradlew COPY --chown=quarkus:quarkus gradle /code/gradle COPY --chown=quarkus:quarkus build.gradle /code/ @@ -574,7 +558,7 @@ COPY --chown=quarkus:quarkus gradle.properties /code/ USER quarkus WORKDIR /code COPY src /code/src -RUN gradle -b /code/build.gradle buildNative +RUN ./gradlew build -Dquarkus.package.type=native ## Stage 2 : create the docker final image FROM quay.io/quarkus/quarkus-micro-image:1.0 diff --git a/docs/src/main/asciidoc/cdi-reference.adoc b/docs/src/main/asciidoc/cdi-reference.adoc index 94280475efd00..b141ff529c6dc 100644 --- a/docs/src/main/asciidoc/cdi-reference.adoc +++ b/docs/src/main/asciidoc/cdi-reference.adoc @@ -931,7 +931,7 @@ public class Processor { <1> The injected instance is an _immutable list_ of the contextual references of the _disambiguated_ beans. <2> For this injection point the required type is `Service` and no additional qualifiers are declared. -TIP: By default, the list of beans is sorted by priority as defined by `io.quarkus.arc.InjectableBean#getPriority()`. Higher priority goes first. In general, the `@javax.annotation.Priority` and `@io.quarkus.arc.Priority` annotations can be used to assign the priority to a class bean, producer method or producer field. +TIP: The list is sorted by priority as defined by `io.quarkus.arc.InjectableBean#getPriority()`. Higher priority goes first. In general, the `@javax.annotation.Priority` and `@io.quarkus.arc.Priority` annotations can be used to assign the priority to a class bean, producer method or producer field. If an injection point declares no other qualifier than `@All` then `@Any` is used, i.e. the behavior is equivalent to `@Inject @Any Instance`. @@ -960,6 +960,8 @@ public class Processor { NOTE: Neither a type variable nor a wildcard is a legal type parameter for an `@All List<>` injection point, i.e. `@Inject @All List all` is not supported and results in a deployment error. +TIP: It is also possible to obtain the list of all bean instance handles programmatically via the `Arc.container().listAll()` methods. + === Ignoring Class-Level Interceptor Bindings for Methods and Constructors If a managed bean declares interceptor binding annotations on the class level, the corresponding `@AroundInvoke` interceptors will apply to all business methods. diff --git a/docs/src/main/asciidoc/cli-tooling.adoc b/docs/src/main/asciidoc/cli-tooling.adoc index c2b3b924b198a..d9759bdd9a37e 100644 --- a/docs/src/main/asciidoc/cli-tooling.adoc +++ b/docs/src/main/asciidoc/cli-tooling.adoc @@ -20,6 +20,7 @@ The Quarkus CLI is available in several developer-oriented package managers such * https://sdkman.io[SDKMAN!] * https://brew.sh[Homebrew] * https://community.chocolatey.org/packages/quarkus[Chocolatey] +* https://scoop.sh[Scoop] If you already use (or want to use) one of these tools, it is the simplest way to install the Quarkus CLI and keep it updated. @@ -30,6 +31,7 @@ Choose the alternative that is the most practical for you: * SDKMAN! - for Linux and macOS * Homebrew - for Linux and macOS * Chocolatey - for Windows +* Scoop - for Windows [role="primary asciidoc-tabs-sync-jbang"] .JBang @@ -217,6 +219,35 @@ choco upgrade quarkus ---- **** +[role="secondary asciidoc-tabs-sync-scoop"] +.Scoop +**** +https://scoop.sh[Scoop] is a package manager for Windows. +You can use Scoop to install (and update) the Quarkus CLI. +[NOTE] +==== +Make sure you have a JDK installed before installing the Quarkus CLI. +You can install a JDK with `scoop install openjdk17` for Java 17 or `scoop install openjdk11` for Java 11. +==== +To install the Quarkus CLI using Scoop, run the following command: +[source,shell] +---- +scoop install quarkus-cli +---- +It will install the latest version of the Quarkus CLI. +Once installed `quarkus` will be in your `$PATH` and if you run `quarkus --version` it will print the installed version: +[source,shell,subs=attributes+] +---- +quarkus --version +{quarkus-version} +---- +You can upgrade the Quarkus CLI with: +[source,shell] +---- +scoop update quarkus-cli +---- +**** + == Using the CLI Use `--help` to display help information with all the available commands: diff --git a/docs/src/main/asciidoc/config-reference.adoc b/docs/src/main/asciidoc/config-reference.adoc index 521382b5559b9..373576e542b86 100644 --- a/docs/src/main/asciidoc/config-reference.adoc +++ b/docs/src/main/asciidoc/config-reference.adoc @@ -332,6 +332,11 @@ In this style, the configuration names in the profile aware file do not need to Properties in the profile aware file have priority over profile aware properties defined in the main file. ==== +[WARNING] +==== +The profile aware file must be present in the exact same location as the main `application.properties` file. +==== + === Parent Profile A Parent Profile adds one level of hierarchy to the current profile. The configuration `quarkus.config.profile.parent` diff --git a/docs/src/main/asciidoc/container-image.adoc b/docs/src/main/asciidoc/container-image.adoc index ce5ccae55478d..66a887657d665 100644 --- a/docs/src/main/asciidoc/container-image.adoc +++ b/docs/src/main/asciidoc/container-image.adoc @@ -68,6 +68,10 @@ To use this feature, add the following extension to your project. :add-extension-extensions: container-image-docker include::includes/devtools/extension-add.adoc[] +The `quarkus-container-image-docker` extension is capable of https://docs.docker.com/buildx/working-with-buildx/#build-multi-platform-images/[creating multi-platform (or multi-arch)] images using https://docs.docker.com/engine/reference/commandline/buildx_build/[`docker buildx build`]. See the `quarkus.docker.buildx.*` configuration items in the <<#DockerOptions,Docker Options>> section below. + +NOTE: `docker buildx build` ONLY supports https://docs.docker.com/engine/reference/commandline/buildx_build/#load[loading the result of a build] to `docker images` when building for a single platform. Therefore, if you specify more than one argument in the `quarkus.docker.buildx.platform` property, the resulting images will not be loaded into `docker images`. If `quarkus.docker.buildx.platform` is omitted or if only a single platform is specified, it will then be loaded into `docker images`. + [#s2i] === S2I @@ -123,6 +127,21 @@ include::includes/devtools/build.adoc[] NOTE: If you ever want to build a native container image and already have an existing native image you can set `-Dquarkus.native.reuse-existing=true` and the native image build will not be re-run. +== Using @QuarkusIntegrationTest + +To run tests on the resulting image, `quarkus.container-image.build=true` needs to be set using any of the ways that Quarkus supports. + +[source, bash, subs=attributes+, role="primary asciidoc-tabs-sync-maven"] +.Maven +---- +./mvnw verify -Dquarkus.container-image.build=true +---- +[source, bash, subs=attributes+, role="secondary asciidoc-tabs-sync-gradle"] +.Gradle +---- +./gradlew quarkusIntTest -Dquarkus.container-image.build=true +---- + == Pushing To push a container image for your project, `quarkus.container-image.push=true` needs to be set using any of the ways that Quarkus supports. @@ -174,6 +193,7 @@ In addition to the generic container image options, the `container-image-jib` al include::{generated-dir}/config/quarkus-container-image-jib.adoc[opts=optional, leveloffset=+1] +[#DockerOptions] === Docker Options In addition to the generic container image options, the `container-image-docker` also provides the following options: diff --git a/docs/src/main/asciidoc/databases-dev-services.adoc b/docs/src/main/asciidoc/databases-dev-services.adoc new file mode 100644 index 0000000000000..06ab5b457abd0 --- /dev/null +++ b/docs/src/main/asciidoc/databases-dev-services.adoc @@ -0,0 +1,82 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Dev Services for Databases + +include::./attributes.adoc[] + +When testing or running in dev mode Quarkus can provide you with a zero config database out of the box, a feature we refer to as Dev Services. +Depending on your database type you may need Docker installed in order to use this feature. +Dev Services is supported for the following databases: + +* DB2 (container) (requires <<#license_acceptance,license acceptance>>) +* Derby (in-process) +* H2 (in-process) +* MariaDB (container) +* Microsoft SQL Server (container) (requires <<#license_acceptance,license acceptance>>) +* MySQL (container) +* Oracle Express Edition (container) +* PostgreSQL (container) + +If you want to use Dev Services then all you need to do is include the relevant extension for the type of database you want (either reactive or JDBC, or both). +Don't configure a database URL, username and password - Quarkus will provide the database and you can just start coding without worrying about config. + +Production databases need to be configured as normal, so if you want to include a production database config in your +`application.properties` and continue to use Dev Services we recommend that you use the `%prod.` profile to define your database settings. + +== Enabling / Disabling Dev Services for Database + +Dev Services for databases automatically starts a database server in dev mode and when running tests. +So, you don't have to start a server manually. +The application is configured automatically. + +You can disable the automatic database start in `application.properties` via: + +[source,properties] +---- +quarkus.devservices.enabled=false +# OR +quarkus.datasource.devservices.enabled=false +---- + +Dev Services for databases relies on Docker to start the server (except for H2 and Derby which are run in process). +If your environment does not support Docker, you will need to start the server manually, or connect to an already running server. + +[[license_acceptance]] +=== Proprietary Databases - License Acceptance + +If you are using a proprietary database such as DB2 or MSSQL you will need to accept the license agreement. +To do this create a `src/main/resources/container-license-acceptance.txt` files in your project and add a line with the image name and tag of the database. +By default, Quarkus uses the default image for the current version of Testcontainers, if you attempt to start Quarkus the resulting failure will tell you the exact image name in use for you to add to the file. + +An example file is shown below: + +.src/main/resources/container-license-acceptance.txt +---- +ibmcom/db2:11.5.0.0a +mcr.microsoft.com/mssql/server:2017-CU12 +---- + +== Database Vendor Specific Configuration + +All services based on containers are run using Testcontainers but Quarkus is not using the Testcontainers JDBC driver. +Thus, even though extra JDBC URL properties can be set in your `application.properties` file, specific properties supported by the Testcontainers JDBC driver such as `TC_INITSCRIPT`, `TC_INITFUNCTION`, `TC_DAEMON`, `TC_TMPFS` are not supported. + +Quarkus can support *specific* properties sent to the container itself though, e.g. this is the case for `TC_MY_CNF` which allows to override the MariaDB/MySQL configuration file. + +Overriding the MariaDB/MySQL configuration would be done as follows: + +[source,properties] +---- +quarkus.datasource.devservices.container-properties.TC_MY_CNF=testcontainers/mysql-conf +---- + +This support is database specific and needs to be implemented in each dev service specifically. + +== Configuration Reference + +Datasource Dev Services support the following config options: + +include::{generated-dir}/config/quarkus-datasource-config-group-dev-services-build-time-config.adoc[opts=optional,leveloffset=+1] \ No newline at end of file diff --git a/docs/src/main/asciidoc/datasource.adoc b/docs/src/main/asciidoc/datasource.adoc index 30a42ec50276d..129051eb5793d 100644 --- a/docs/src/main/asciidoc/datasource.adoc +++ b/docs/src/main/asciidoc/datasource.adoc @@ -45,54 +45,12 @@ If you want a better understanding of how all this works, this guide has a lot m When testing or running in dev mode Quarkus can even provide you with a zero config database out of the box, a feature we refer to as Dev Services. Depending on your database type you may need Docker installed in order to use this feature. -Dev Services is supported for the following databases: -* DB2 (container) (requires license acceptance) -* Derby (in-process) -* H2 (in-process) -* MariaDB (container) -* Microsoft SQL Server (container) (requires license acceptance) -* MySQL (container) -* Oracle Express Edition (container) -* PostgreSQL (container) +If you want to use Dev Services then all you need to do is include the relevant extension for the type of database you want, +e.g. `jdbc-postgresql`, `reactive-pg-client` or both. Don't configure a database URL, username and password - +Quarkus will provide the database and you can just start coding without worrying about config. -If you want to use Dev Services then all you need to do is include the relevant extension for the type of database you want (either reactive or -JDBC, or both), and don't configure a database URL, username and password, Quarkus will provide the database and you can just start -coding without worrying about config. - -If you are using a proprietary database such as DB2 or MSSQL you will need to accept the license agreement. To do this -create a `src/main/resources/container-license-acceptance.txt` files in your project and add a line with the image -name and tag of the database. By default Quarkus uses the default image for the current version of Testcontainers, if -you attempt to start Quarkus the resulting failure will tell you the exact image name in use for you to add to the -file. - -An example file is shown below: - -.src/main/resources/container-license-acceptance.txt ----- -ibmcom/db2:11.5.0.0a -mcr.microsoft.com/mssql/server:2017-CU12 ----- - -[NOTE] -==== -All services based on containers are run using Testcontainers but Quarkus is not using the Testcontainers JDBC driver. - -Thus, even though extra JDBC URL properties can be set in your `application.properties` file, -specific properties supported by the Testcontainers JDBC driver such as `TC_INITSCRIPT`, `TC_INITFUNCTION`, `TC_DAEMON`, `TC_TMPFS` are not supported. - -Quarkus can support specific properties sent to the container itself though and, -typically, this is the case for `TC_MY_CNF` which allows to override the MariaDB/MySQL configuration file. - -Overriding the MariaDB/MySQL configuration would be done as follows: - -[source,properties] ----- -quarkus.datasource.devservices.container-properties.TC_MY_CNF=testcontainers/mysql-conf ----- - -This support is database specific and needs to be implemented in each dev service specifically. -==== +See xref:databases-dev-services.adoc[Databases Dev Services] for more details and optional configurations. === JDBC datasource @@ -507,22 +465,6 @@ If the Narayana JTA extension is also available, integration is automatic. You can override this by setting the `transactions` configuration property - see the <> below. -== Dev Services (Configuration Free Databases) - -As mentioned above Quarkus supports a feature called Dev Services that allows you to create datasources without any config. If -you have a database extension that supports it and no config is provided, Quarkus will automatically start a database (either -using Testcontainers, or by starting a Java DB in process), and automatically configure a connection to this database. - -Production databases need to be configured as normal, so if you want to include a production database config in your -application.properties and continue to use Dev Services we recommend that you use the `%prod.` profile to define your -database settings. - -=== Configuring Dev Services - -Dev Services supports the following config options: - -include::{generated-dir}/config/quarkus-datasource-config-group-dev-services-build-time-config.adoc[opts=optional, leveloffset=+1] - === Named Datasources When using Dev Services the default datasource will always be created, but to specify a named datasource you need to have diff --git a/docs/src/main/asciidoc/deploying-to-google-cloud.adoc b/docs/src/main/asciidoc/deploying-to-google-cloud.adoc index fd0a952945189..12691aad6aaea 100644 --- a/docs/src/main/asciidoc/deploying-to-google-cloud.adoc +++ b/docs/src/main/asciidoc/deploying-to-google-cloud.adoc @@ -65,6 +65,11 @@ runtime: java11 This will create a default service for your App Engine application. +[NOTE] +==== +You can also use the new Java 17 runtime by defining `runtime: java17` instead. +==== + App Engine Standard does not support the default Quarkus' specific packaging layout, therefore, you must set up your application to be packaged as an uber-jar via your `application.properties` file: [source, properties] @@ -160,7 +165,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], -it is strongly advised to provide them thanks to Quarkus xref:microprofile-health.adoc[Microprofile Health] support. +it is strongly advised to provide them thanks to Quarkus xref:smallrye-health.adoc[Smallrye Health] support. == Deploying to Google Cloud Run diff --git a/docs/src/main/asciidoc/dev-services.adoc b/docs/src/main/asciidoc/dev-services.adoc index cd24259b32308..ef2b4c16b1fcd 100644 --- a/docs/src/main/asciidoc/dev-services.adoc +++ b/docs/src/main/asciidoc/dev-services.adoc @@ -38,17 +38,24 @@ The Apicurio Dev Service will be enabled when the `quarkus-apicurio-registry-avr address has not been explicitly configured. More information can be found at the xref:apicurio-registry-dev-services.adoc[Apicurio Registry Dev Services Guide]. -include::{generated-dir}/config/quarkus-apicurio-registry-devservices-apicurio-registry-avro-apicurio-registry-dev-services-build-time-config.adoc[opts=optional, leveloffset=+1] +include::{generated-dir}/config/quarkus-apicurio-registry-devservices-apicurio-registry-devservice-apicurio-registry-dev-services-build-time-config.adoc[opts=optional, leveloffset=+1] == Databases The database Dev Services will be enabled when a reactive or JDBC datasource extension is present in the application, and the database URL has not been configured. More information can be found at the -xref:datasource.adoc#dev-services[Datasource Guide]. +xref:databases-dev-services.adoc[Databases Dev Services Guide]. Quarkus provides Dev Services for all databases it supports. Most of these are run in a container, with the exception of H2 and Derby which are run in process. Dev Services are supported for both JDBC and reactive drivers. +Those relational databases that are running in a container are started using Testcontainers and support "reusable instances"; +this implies that if you add the property `testcontainers.reuse.enable=true` in your Testcontainers configuration file, +a property file named `.testcontainers.properties` in your user home, then the databases will not be stopped aggressively +after each run, and can be reused. + +N.B. if you opt in for this feature, Quarkus will not reset the state of the database between runs unless you explicitly configure it to. + include::{generated-dir}/config/quarkus-datasource-config-group-dev-services-build-time-config.adoc[opts=optional, leveloffset=+1] == Kafka diff --git a/docs/src/main/asciidoc/flyway.adoc b/docs/src/main/asciidoc/flyway.adoc index 44b034574493b..47b0c16e5793c 100644 --- a/docs/src/main/asciidoc/flyway.adoc +++ b/docs/src/main/asciidoc/flyway.adoc @@ -15,7 +15,7 @@ Quarkus provides first class support for using Flyway as will be explained in th == Setting up support for Flyway -To start using Flyway with your project, you just need to: +As shown in the <> section, to start using Flyway with your project, you just need to: * add your migrations to the `{migrations-path}` folder as you usually do with Flyway * activate the `migrate-at-start` option to migrate the schema automatically or inject the `Flyway` object and run @@ -77,6 +77,7 @@ Also, you can customize the Flyway behaviour by using the following properties: include::{generated-dir}/config/quarkus-flyway.adoc[opts=optional, leveloffset=+1] +== Developing with Flyway The following is an example for the `{config-file}` file: @@ -88,10 +89,10 @@ quarkus.datasource.username=sarah quarkus.datasource.password=connor quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/mydatabase -# Flyway minimal config properties +# Run Flyway migrations automatically quarkus.flyway.migrate-at-start=true -# Flyway optional config properties +# More Flyway configuration options # quarkus.flyway.baseline-on-migrate=true # quarkus.flyway.baseline-version=1.0.0 # quarkus.flyway.baseline-description=Initial version @@ -116,7 +117,12 @@ INSERT INTO quarkus(id, name) VALUES (1, 'QUARKED'); ---- -Now you can start your application and Quarkus will run the Flyway's migrate method according to your config: +Now you can start your application and Quarkus will run the Flyway's +migrate method according to your config. + +NOTE: With `quarkus.flyway.migrate-at-start=true`, as in the example +above, Quarkus will execute the Flyway migration as part of the +xref:lifecycle.adoc[application startup]. [source,java] ---- @@ -132,9 +138,14 @@ public class MigrationService { } } ---- - <1> Inject the Flyway object if you want to use it directly +TIP: In dev-mode Quarkus will automatically restart the application if +any of the existing migration scripts get modified. If you want to take +advantage of this while developing and testing new migration scripts, +you will want to set `%dev.quarkus.flyway.clean-at-start=true`, so that +Flyway actually runs the modified migration. + == Multiple datasources Flyway can be configured for multiple datasources. @@ -182,9 +193,6 @@ NOTE: Without configuration, Flyway is set up for every datasource using the def In case you are interested in using the `Flyway` object directly, you can inject it as follows: -NOTE: If you enabled the `quarkus.flyway.migrate-at-start` property, by the time you use the Flyway instance, -Quarkus will already have run the migrate operation - [source,java] ---- @ApplicationScoped diff --git a/docs/src/main/asciidoc/funqy-gcp-functions.adoc b/docs/src/main/asciidoc/funqy-gcp-functions.adoc index ce391e82465c7..5d1f4bf77c33e 100644 --- a/docs/src/main/asciidoc/funqy-gcp-functions.adoc +++ b/docs/src/main/asciidoc/funqy-gcp-functions.adoc @@ -166,8 +166,16 @@ gcloud functions deploy quarkus-example-funky-pubsub \ --source=target/deployment ---- +[IMPORTANT] +==== The entry point always needs to be `io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction` as it will be this class 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. @@ -200,8 +208,16 @@ gcloud functions deploy quarkus-example-funky-storage \ --source=target/deployment ---- +[IMPORTANT] +==== The entry point always needs to be `io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction` as it will be this class 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. @@ -237,8 +253,16 @@ gcloud beta functions deploy quarkus-example-cloud-event --gen2 \ --runtime=java11 --trigger-bucket=example-cloud-event --source=target/deployment ---- +[IMPORTANT] +==== The entry point always needs to be `io.quarkus.funqy.gcp.functions.FunqyCloudEventsFunction` as it will be this class 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. diff --git a/docs/src/main/asciidoc/funqy-knative-events.adoc b/docs/src/main/asciidoc/funqy-knative-events.adoc index 4f174bc14d588..1476aec3b1aa2 100644 --- a/docs/src/main/asciidoc/funqy-knative-events.adoc +++ b/docs/src/main/asciidoc/funqy-knative-events.adoc @@ -21,7 +21,7 @@ with Knative Events. :prerequisites-no-graalvm: include::includes/devtools/prerequisites.adoc[] * Read about xref:funqy.adoc[Funqy Basics]. This is a short read! -* Have gone through the link:https://redhat-developer-demos.github.io/knative-tutorial/knative-tutorial/index.html[Knative Tutorial], specifically link:https://redhat-developer-demos.github.io/knative-tutorial/knative-tutorial-eventing/eventing-trigger-broker.html[Brokers and Triggers] +* Have gone through the link:https://redhat-developer-demos.github.io/knative-tutorial/knative-tutorial/index.html[Knative Tutorial], specifically link:https://redhat-developer-demos.github.io/knative-tutorial/knative-tutorial/eventing/eventing-trigger-broker.html[Brokers and Triggers] == Setting up Knative @@ -29,7 +29,7 @@ Setting up Knative locally in a Minikube environment is beyond the scope of this to follow https://redhat-developer-demos.github.io/knative-tutorial/knative-tutorial/index.html[this] Knative Tutorial put together by Red Hat. It walks through how to set up Knative on Minikube or OpenShift in a local environment. -NOTE: Specifically you should run the link:https://redhat-developer-demos.github.io/knative-tutorial/knative-tutorial-eventing/eventing-trigger-broker.html[Brokers and Triggers] +NOTE: Specifically you should run the link:https://redhat-developer-demos.github.io/knative-tutorial/knative-tutorial/eventing/eventing-trigger-broker.html[Brokers and Triggers] tutorial as this guide requires that you can invoke on a Broker to trigger the quickstart code. == Read about Cloud Events diff --git a/docs/src/main/asciidoc/gcp-functions-http.adoc b/docs/src/main/asciidoc/gcp-functions-http.adoc index 1a1837621ce0c..b0077233ae4e6 100644 --- a/docs/src/main/asciidoc/gcp-functions-http.adoc +++ b/docs/src/main/asciidoc/gcp-functions-http.adoc @@ -171,6 +171,11 @@ 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 9a4de3293d694..7b0555fbfceaa 100644 --- a/docs/src/main/asciidoc/gcp-functions.adoc +++ b/docs/src/main/asciidoc/gcp-functions.adoc @@ -270,6 +270,11 @@ 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 @@ -297,6 +302,11 @@ 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] @@ -323,6 +333,11 @@ 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] @@ -349,6 +364,11 @@ gcloud beta 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. == Testing locally diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 20ba0cdeed290..6422b21eab53e 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -1209,7 +1209,7 @@ guide except injecting into tests (and the native executable runs in a separate This is covered in the xref:building-native-image.adoc[Native Executable Guide]. [#quarkus-integration-test] -== Using @QuarkusIntegrationTest +== Using `@QuarkusIntegrationTest` `@QuarkusIntegrationTest` should be used to launch and test the artifact produced by the Quarkus build, and supports testing a jar (of whichever type), a native image or container image. Put simply, this means that if the result of a Quarkus build (`mvn package` or `gradle build`) is a jar, that jar will be launched as `java -jar ...` and tests run against it. @@ -1221,21 +1221,80 @@ As is the case with `@NativeImageTest`, this is a black box test that supports t [NOTE] ==== -As a test annotated with `@QuarkusIntegrationTest` tests the result of the build, it should be run as part of the integration test suite - i.e. via the `maven-failsafe-plugin` if using Maven or the `quarkusIntTest` task if using Gradle. +As a test annotated with `@QuarkusIntegrationTest` tests the result of the build, it should be run as part of the integration test suite - i.e. by setting `-DskipITs=false` if using Maven or the `quarkusIntTest` task if using Gradle. These tests will **not** work if run in the same phase as `@QuarkusTest` as Quarkus has not yet created the final artifact. ==== +The `pom.xml` file contains: + +[source, xml] +---- + + org.apache.maven.plugins + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + +---- + +This instructs the failsafe-maven-plugin to run integration-test. + +Then, open the `src/test/java/org/acme/quickstart/GreetingResourceIT.java`. It contains: + +[source,java] +---- +package org.acme.quickstart; + + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest // <1> +public class GreetingResourceIT extends GreetingResourceTest { // <2> + + // Run the same tests + +} +---- +<1> Use another test runner that starts the application from the native file before the tests. +The executable is retrieved by the _Failsafe Maven Plugin_. +<2> We extend our previous tests as a convenience, but you can also implement your tests. + +More information can be found in the link:building-native-image#testing-the-native-executable[Testing the native executable Guide]. + === Launching containers When `@QuarkusIntegrationTest` results in launching a container (because the application was built with `quarkus.container-image.build` set to `true`), the container is launched on a predictable container network. This facilitates writing integration tests that need to launch services to support the application. This means that `@QuarkusIntegrationTest` works out of the box with containers launched via xref:dev-services.adoc[Dev Services], but it also means that it enables using <> resources that launch additional containers. This can be achieved by having your `QuarkusTestLifecycleManager` implement `io.quarkus.test.common.DevServicesContext.ContextAware`. A simple example could be the following: +The container running the resource to test against, for example PostgreSQL via Testcontainers, is assigned an IP address from the container's network. +Use the container's "public" IP from its network and the "unmapped" port number to connect to the service. +The Testcontainers library usually return connection strings without respecting the container network, so additional code is needed to provide Quarkus the "correct" connection string using the container's IP on the container network and the _unmapped_ port number. + +The following example illustrates the use with PostgreSQL, but the approach is applicable to all containers. + [source,java] ---- import io.quarkus.test.common.DevServicesContext; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.containers.PostgreSQLContainer; + import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -1243,6 +1302,7 @@ import java.util.Optional; public class CustomResource implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware { private Optional containerNetworkId; + private JdbcDatabaseContainer container; @Override public void setIntegrationTestContext(DevServicesContext context) { @@ -1252,9 +1312,36 @@ public class CustomResource implements QuarkusTestResourceLifecycleManager, DevS @Override public Map start() { // start a container making sure to call withNetworkMode() with the value of containerNetworkId if present + container = new PostgreSQLContainer<>("postgres:latest").withLogConsumer(outputFrame -> {}); + + // apply the network to the container + containerNetworkId.ifPresent(container::withNetworkMode); + + String jdbcUrl = container.getJdbcUrl(); + if (containerNetworkId.isPresent()) { + // Replace hostname + port in the provided JDBC URL with the hostname of the Docker container + // running PostgreSQL and the listening port. + jdbcUrl = fixJdbcUrl(jdbcUrl); + } // return a map containing the configuration the application needs to use the service - return new HashMap<>(); + return ImmutableMap.of( + "quarkus.datasource.username", container.getUsername(), + "quarkus.datasource.password", container.getPassword(), + "quarkus.datasource.jdbc.url", jdbcUrl); + } + + private String fixJdbcUrl(String jdbcUrl) { + // Part of the JDBC URL to replace + String hostPort = container.getHost() + ':' + container.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT); + + // Host/IP on the container network plus the unmapped port + String networkHostPort = + container.getCurrentContainerInfo().getConfig().getHostName() + + ':' + + PostgreSQLContainer.POSTGRESQL_PORT; + + return jdbcUrl.replace(hostPort, networkHostPort); } @Override diff --git a/docs/src/main/asciidoc/hibernate-orm.adoc b/docs/src/main/asciidoc/hibernate-orm.adoc index 4cfd7ca77f0b3..bb2f1a971d096 100644 --- a/docs/src/main/asciidoc/hibernate-orm.adoc +++ b/docs/src/main/asciidoc/hibernate-orm.adoc @@ -555,7 +555,7 @@ link in the Flyway pane. Hit the `Create Initial Migration` button and the follo - A `db/migration/V1.0.0__\{appname\}.sql` file will be created, containing the SQL Hibernate is running to generate the schema - `quarkus.flyway.baseline-on-migrate` will be set, telling Flyway to automatically create its baseline tables - `quarkus.flyway.migrate-at-start` will be set, telling Flyway to automatically apply migrations on application startup -- `%dev.quarkus.flyway.clean-at-start` and ``%test.quarkus.flyway.clean-at-start` will be set, to clean the DB after reload in dev/test mode +- `%dev.quarkus.flyway.clean-at-start` and `%test.quarkus.flyway.clean-at-start` will be set, to clean the DB after reload in dev/test mode WARNING: This button is simply a convenience to quickly get you started with Flyway, it is up to you to determine how you want to manage your database schemas in production. In particular the `migrate-at-start` setting may not be right for all environments. diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index 1088ef2f68671..dbe75ec85cec7 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -78,7 +78,7 @@ 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. - +[[context-path]] == Configuring the Context path By default Quarkus will serve content from under the root context. If you want to change this you can use the @@ -94,6 +94,17 @@ will be served relative to `{quarkus.http.root-path}/{quarkus.servlet.context-pa If REST Assured is used for testing and `quarkus.http.root-path` is set then Quarkus will automatically configure the base URL for use in Quarkus tests, so test URL's should not include the root path. + +In general, path configurations for web content are interpreted relative to `quarkus.http.root-path` (which is / by default). + +- To specify paths within this context root, use a relative path that does not begin with a forward slash. + +- If you want to specify the URI explicitly, so it is always the same regardless of the value of `quarkus.http.root-path`, use an absolute path that begins with a forward slash. + +As an example, if an extension configures a `service` path, that endpoint will be served from `${quarkus.http.root-path}/service`. If you change the configuration of that path to `/service`, that endpoint will be served from `/service`. + +The link:https://quarkus.io/blog/path-resolution-in-quarkus/[Path Resolution in Quarkus] blog post further explains how path resolution works for both user and extension defined paths. + [[ssl]] == Supporting secure connections with SSL @@ -194,6 +205,47 @@ This will apply the `Pragma` header only when the `/headers/pragma` resource is include::{generated-dir}/config/quarkus-vertx-http-config-group-header-config.adoc[leveloffset=+1, opts=optional] +=== Additional HTTP Headers per path + +If you need different header values depending on the path, you can use the following configuration: + +[source, properties] +---- +quarkus.http.filter.index.header."Cache-Control"=none +quarkus.http.filter.index.matches=/index.html +---- +This will set the `Cache-Control` header to `none` when `/index.html` is called. + +IMPORTANT: The `index` after `quarkus.http.filter` in the key is used for grouping and (as an example) can be named as you like. + +You can use a regular expression in the path and also specify the HTTP methods where the HTTP header will be set: + +[source, properties] +---- +quarkus.http.filter.static.header."Cache-Control"=max-age=31536000 +quarkus.http.filter.static.methods=GET,HEAD +quarkus.http.filter.static.matches=/static/.* +---- + +In case of overlapping paths in the configuration, you can specify an order (higher values take precedence). +For example, having the following configuration: + +[source,properties] +---- +quarkus.http.filter.just-order.order=10 +quarkus.http.filter.just-order.header."Cache-Control"=max-age=5000 +quarkus.http.filter.just-order.matches=/paths/order + +quarkus.http.filter.any-order.order=11 +quarkus.http.filter.any-order.header."Cache-Control"=max-age=1 +quarkus.http.filter.any-order.matches=/paths/order.* +---- + +Will include the `Cache-Control: max-age=1` header when `/paths/order` is requested. + +include::{generated-dir}/config/quarkus-vertx-http-config-group-filter-config.adoc[leveloffset=+1, opts=optional] + + == HTTP/2 Support HTTP/2 is enabled by default, and will be used by browsers if SSL is in use on JDK11 or higher. JDK8 does not support @@ -327,11 +379,12 @@ To consider only non-standard headers, please include the following lines instea [source,properties] ---- quarkus.http.proxy.proxy-address-forwarding=true +quarkus.http.proxy.allow-x-forwarded=true quarkus.http.proxy.enable-forwarded-host=true quarkus.http.proxy.enable-forwarded-prefix=true ---- -Both configurations related to standard and non-standard headers can be combined, although the standard headers configuration will have precedence. +Both configurations related to standard and non-standard headers can be combined, although the standard headers configuration will have precedence. However combining them has security implications as clients can forge requests with a forwarded header that is not overwritten by the proxy. Therefore proxies should strip unexpected `X-Forwarded` or `X-Forwarded-*` headers from the client. Supported forwarding address headers are: diff --git a/docs/src/main/asciidoc/ide-tooling.adoc b/docs/src/main/asciidoc/ide-tooling.adoc index 1cfd441f15c8b..40fcfb2b8d0c6 100644 --- a/docs/src/main/asciidoc/ide-tooling.adoc +++ b/docs/src/main/asciidoc/ide-tooling.adoc @@ -85,7 +85,7 @@ https://download.jboss.org/jbosstools/intellij/snapshots/intellij-quarkus/[Devel |Wizards w/code.quarkus.io |icon:check[] |icon:check[] -|https://issues.jboss.org/browse/JBIDE-26950[icon:times[]] +|icon:check[] |icon:check[] |icon:check[] @@ -141,7 +141,7 @@ https://download.jboss.org/jbosstools/intellij/snapshots/intellij-quarkus/[Devel |Easy Launch debug/dev:mode |icon:check[] |icon:check[] -|icon:times[] +|icon:check[] |icon:check[] |icon:check[] diff --git a/docs/src/main/asciidoc/images/oidc-apple-1.png b/docs/src/main/asciidoc/images/oidc-apple-1.png new file mode 100644 index 0000000000000..984e71462b51b Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-1.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-10.png b/docs/src/main/asciidoc/images/oidc-apple-10.png new file mode 100644 index 0000000000000..96c897799905f Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-10.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-11.png b/docs/src/main/asciidoc/images/oidc-apple-11.png new file mode 100644 index 0000000000000..2182c11e558d6 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-11.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-12.png b/docs/src/main/asciidoc/images/oidc-apple-12.png new file mode 100644 index 0000000000000..d7cfb8717ff71 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-12.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-13.png b/docs/src/main/asciidoc/images/oidc-apple-13.png new file mode 100644 index 0000000000000..084ba635f90f7 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-13.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-14.png b/docs/src/main/asciidoc/images/oidc-apple-14.png new file mode 100644 index 0000000000000..526ae9cf232c8 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-14.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-15.png b/docs/src/main/asciidoc/images/oidc-apple-15.png new file mode 100644 index 0000000000000..3188d816ee4c0 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-15.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-16.png b/docs/src/main/asciidoc/images/oidc-apple-16.png new file mode 100644 index 0000000000000..470c4d7f0d6cd Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-16.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-17.png b/docs/src/main/asciidoc/images/oidc-apple-17.png new file mode 100644 index 0000000000000..e08e7a11dd09a Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-17.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-18.png b/docs/src/main/asciidoc/images/oidc-apple-18.png new file mode 100644 index 0000000000000..e56e0633d495a Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-18.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-19.png b/docs/src/main/asciidoc/images/oidc-apple-19.png new file mode 100644 index 0000000000000..35f68417682d9 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-19.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-2.png b/docs/src/main/asciidoc/images/oidc-apple-2.png new file mode 100644 index 0000000000000..045ec05eeb1f1 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-2.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-20.png b/docs/src/main/asciidoc/images/oidc-apple-20.png new file mode 100644 index 0000000000000..995b058ea3c07 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-20.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-21.png b/docs/src/main/asciidoc/images/oidc-apple-21.png new file mode 100644 index 0000000000000..75236c8d8a953 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-21.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-22.png b/docs/src/main/asciidoc/images/oidc-apple-22.png new file mode 100644 index 0000000000000..33380e8d4ff4e Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-22.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-3.png b/docs/src/main/asciidoc/images/oidc-apple-3.png new file mode 100644 index 0000000000000..868440a5a6760 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-3.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-4.png b/docs/src/main/asciidoc/images/oidc-apple-4.png new file mode 100644 index 0000000000000..95c8e1125a7a6 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-4.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-5.png b/docs/src/main/asciidoc/images/oidc-apple-5.png new file mode 100644 index 0000000000000..5fd241e2d4bdb Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-5.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-6.png b/docs/src/main/asciidoc/images/oidc-apple-6.png new file mode 100644 index 0000000000000..86ffd3348c463 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-6.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-7.png b/docs/src/main/asciidoc/images/oidc-apple-7.png new file mode 100644 index 0000000000000..3129b8bada98b Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-7.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-8.png b/docs/src/main/asciidoc/images/oidc-apple-8.png new file mode 100644 index 0000000000000..053ea35b3abcc Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-8.png differ diff --git a/docs/src/main/asciidoc/images/oidc-apple-9.png b/docs/src/main/asciidoc/images/oidc-apple-9.png new file mode 100644 index 0000000000000..972daafd66443 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-apple-9.png differ diff --git a/docs/src/main/asciidoc/images/oidc-facebook-1.png b/docs/src/main/asciidoc/images/oidc-facebook-1.png new file mode 100644 index 0000000000000..4a5b67ab09763 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-facebook-1.png differ diff --git a/docs/src/main/asciidoc/images/oidc-facebook-2.png b/docs/src/main/asciidoc/images/oidc-facebook-2.png new file mode 100644 index 0000000000000..e3836f96d54c2 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-facebook-2.png differ diff --git a/docs/src/main/asciidoc/images/oidc-facebook-3.png b/docs/src/main/asciidoc/images/oidc-facebook-3.png new file mode 100644 index 0000000000000..ee57845e060f5 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-facebook-3.png differ diff --git a/docs/src/main/asciidoc/images/oidc-facebook-4.png b/docs/src/main/asciidoc/images/oidc-facebook-4.png new file mode 100644 index 0000000000000..08199d4e7deea Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-facebook-4.png differ diff --git a/docs/src/main/asciidoc/images/oidc-facebook-5.png b/docs/src/main/asciidoc/images/oidc-facebook-5.png new file mode 100644 index 0000000000000..82bf435d7a3a3 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-facebook-5.png differ diff --git a/docs/src/main/asciidoc/images/oidc-facebook-6.png b/docs/src/main/asciidoc/images/oidc-facebook-6.png new file mode 100644 index 0000000000000..92ef83325e99a Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-facebook-6.png differ diff --git a/docs/src/main/asciidoc/images/oidc-github-1.png b/docs/src/main/asciidoc/images/oidc-github-1.png new file mode 100644 index 0000000000000..41edce4ce6bba Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-github-1.png differ diff --git a/docs/src/main/asciidoc/images/oidc-github-2.png b/docs/src/main/asciidoc/images/oidc-github-2.png new file mode 100644 index 0000000000000..62a304bc580a8 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-github-2.png differ diff --git a/docs/src/main/asciidoc/images/oidc-github-3.png b/docs/src/main/asciidoc/images/oidc-github-3.png new file mode 100644 index 0000000000000..407d184454750 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-github-3.png differ diff --git a/docs/src/main/asciidoc/images/oidc-google-1.png b/docs/src/main/asciidoc/images/oidc-google-1.png new file mode 100644 index 0000000000000..dabe55dfa77bd Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-google-1.png differ diff --git a/docs/src/main/asciidoc/images/oidc-google-2.png b/docs/src/main/asciidoc/images/oidc-google-2.png new file mode 100644 index 0000000000000..9d57c81919282 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-google-2.png differ diff --git a/docs/src/main/asciidoc/images/oidc-google-3.png b/docs/src/main/asciidoc/images/oidc-google-3.png new file mode 100644 index 0000000000000..a2c17240bdc7b Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-google-3.png differ diff --git a/docs/src/main/asciidoc/images/oidc-google-4.png b/docs/src/main/asciidoc/images/oidc-google-4.png new file mode 100644 index 0000000000000..49906b8ecc57c Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-google-4.png differ diff --git a/docs/src/main/asciidoc/images/oidc-google-5.png b/docs/src/main/asciidoc/images/oidc-google-5.png new file mode 100644 index 0000000000000..fdf9e0df6c2e5 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-google-5.png differ diff --git a/docs/src/main/asciidoc/images/oidc-google-6.png b/docs/src/main/asciidoc/images/oidc-google-6.png new file mode 100644 index 0000000000000..0810343ec2253 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-google-6.png differ diff --git a/docs/src/main/asciidoc/images/oidc-google-7.png b/docs/src/main/asciidoc/images/oidc-google-7.png new file mode 100644 index 0000000000000..32a7a6c157bfc Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-google-7.png differ diff --git a/docs/src/main/asciidoc/images/oidc-google-8.png b/docs/src/main/asciidoc/images/oidc-google-8.png new file mode 100644 index 0000000000000..87be4462cdeb7 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-google-8.png differ diff --git a/docs/src/main/asciidoc/images/oidc-google-9.png b/docs/src/main/asciidoc/images/oidc-google-9.png new file mode 100644 index 0000000000000..0da8d5f488b6f Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-google-9.png differ diff --git a/docs/src/main/asciidoc/images/oidc-microsoft-1.png b/docs/src/main/asciidoc/images/oidc-microsoft-1.png new file mode 100644 index 0000000000000..9a6216eb45de6 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-microsoft-1.png differ diff --git a/docs/src/main/asciidoc/images/oidc-microsoft-2.png b/docs/src/main/asciidoc/images/oidc-microsoft-2.png new file mode 100644 index 0000000000000..2db15f18656bc Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-microsoft-2.png differ diff --git a/docs/src/main/asciidoc/images/oidc-microsoft-3.png b/docs/src/main/asciidoc/images/oidc-microsoft-3.png new file mode 100644 index 0000000000000..7c32b5931fadf Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-microsoft-3.png differ diff --git a/docs/src/main/asciidoc/images/oidc-microsoft-4.png b/docs/src/main/asciidoc/images/oidc-microsoft-4.png new file mode 100644 index 0000000000000..fdb1140af7498 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-microsoft-4.png differ diff --git a/docs/src/main/asciidoc/images/oidc-microsoft-5.png b/docs/src/main/asciidoc/images/oidc-microsoft-5.png new file mode 100644 index 0000000000000..e6adf4a5a0007 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-microsoft-5.png differ diff --git a/docs/src/main/asciidoc/images/oidc-microsoft-6.png b/docs/src/main/asciidoc/images/oidc-microsoft-6.png new file mode 100644 index 0000000000000..fb2ee6282d401 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-microsoft-6.png differ diff --git a/docs/src/main/asciidoc/images/oidc-microsoft-7.png b/docs/src/main/asciidoc/images/oidc-microsoft-7.png new file mode 100644 index 0000000000000..9349461a98a4d Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-microsoft-7.png differ diff --git a/docs/src/main/asciidoc/images/oidc-spotify-1.png b/docs/src/main/asciidoc/images/oidc-spotify-1.png new file mode 100644 index 0000000000000..35b8bc928d820 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-spotify-1.png differ diff --git a/docs/src/main/asciidoc/images/oidc-spotify-2.png b/docs/src/main/asciidoc/images/oidc-spotify-2.png new file mode 100644 index 0000000000000..1d9ca4562f48d Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-spotify-2.png differ diff --git a/docs/src/main/asciidoc/images/oidc-twitter-1.png b/docs/src/main/asciidoc/images/oidc-twitter-1.png new file mode 100644 index 0000000000000..ae2eb97522eff Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-twitter-1.png differ diff --git a/docs/src/main/asciidoc/images/oidc-twitter-2.png b/docs/src/main/asciidoc/images/oidc-twitter-2.png new file mode 100644 index 0000000000000..0121c76bc5622 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-twitter-2.png differ diff --git a/docs/src/main/asciidoc/images/oidc-twitter-3.png b/docs/src/main/asciidoc/images/oidc-twitter-3.png new file mode 100644 index 0000000000000..d149d25789f00 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-twitter-3.png differ diff --git a/docs/src/main/asciidoc/images/oidc-twitter-4.png b/docs/src/main/asciidoc/images/oidc-twitter-4.png new file mode 100644 index 0000000000000..9eed1257f0b72 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-twitter-4.png differ diff --git a/docs/src/main/asciidoc/images/oidc-twitter-5.png b/docs/src/main/asciidoc/images/oidc-twitter-5.png new file mode 100644 index 0000000000000..dc66e972e9f0a Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-twitter-5.png differ diff --git a/docs/src/main/asciidoc/images/oidc-twitter-6.png b/docs/src/main/asciidoc/images/oidc-twitter-6.png new file mode 100644 index 0000000000000..6469ef84665ec Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-twitter-6.png differ diff --git a/docs/src/main/asciidoc/images/oidc-twitter-7.png b/docs/src/main/asciidoc/images/oidc-twitter-7.png new file mode 100644 index 0000000000000..2cd3aeb6dff6f Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-twitter-7.png differ diff --git a/docs/src/main/asciidoc/images/oidc-twitter-8.png b/docs/src/main/asciidoc/images/oidc-twitter-8.png new file mode 100644 index 0000000000000..ffcfb1a0dbc99 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-twitter-8.png differ diff --git a/docs/src/main/asciidoc/images/oidc-twitter-9.png b/docs/src/main/asciidoc/images/oidc-twitter-9.png new file mode 100644 index 0000000000000..e1b70c183838a Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-twitter-9.png differ diff --git a/docs/src/main/asciidoc/images/stork-kubernetes-architecture.png b/docs/src/main/asciidoc/images/stork-kubernetes-architecture.png new file mode 100644 index 0000000000000..738f2b2a2df66 Binary files /dev/null and b/docs/src/main/asciidoc/images/stork-kubernetes-architecture.png differ diff --git a/docs/src/main/asciidoc/kafka-dev-services.adoc b/docs/src/main/asciidoc/kafka-dev-services.adoc index 7082f8629bfd9..2bea2abbfe50c 100644 --- a/docs/src/main/asciidoc/kafka-dev-services.adoc +++ b/docs/src/main/asciidoc/kafka-dev-services.adoc @@ -46,6 +46,7 @@ You can set the port by configuring the `quarkus.kafka.devservices.port` propert Note that the Kafka advertised address is automatically configured with the chosen port. +[[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). @@ -83,6 +84,7 @@ without trying to re-partition the existing topic to a different number of parti You can configure timeout for Kafka admin client calls used in topic creation using `quarkus.kafka.devservices.topic-partitions-timeout`, it defaults to 2 seconds. +[[redpanda-enabling-transactions]] == Enabling transactions By default, the Red Panda broker does not act as a transaction coordinator. diff --git a/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc b/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc index d6d85dfe84dfa..642c7282f9887 100644 --- a/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc +++ b/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc @@ -8,7 +8,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::./attributes.adoc[] This guide shows how your Quarkus application can use Apache Kafka, http://avro.apache.org/docs/current/[Avro] serialized -records, and connect to a schema registry (such as the https://docs.confluent.io/platform/current/schema-registry/index.html[Confluent Schema Registry] or https://www.apicur.io/registry/[Apicurio Registry]. +records, and connect to a schema registry (such as the https://docs.confluent.io/platform/current/schema-registry/index.html[Confluent Schema Registry] or https://www.apicur.io/registry/[Apicurio Registry]). If you are not familiar with Kafka and Kafka in Quarkus in particular, consider first going through the xref:kafka.adoc[Using Apache Kafka with Reactive Messaging] guide. @@ -52,19 +52,17 @@ include::includes/devtools/create-app.adoc[] [TIP] ==== If you use Confluent Schema Registry, you don't need the `quarkus-apicurio-registry-avro` extension. -Instead, you need the following dependencies and the Confluent Maven repository added -to your build file: +Instead, you need the `quarkus-confluent-registry-avro` extension and a few more dependencies. +Also, you need to add the Confluent Maven repository to your build file: [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml ---- ... - - io.quarkus - quarkus-avro + quarkus-confluent-registry-avro @@ -109,8 +107,7 @@ repositories { dependencies { ... - // Quarkus extension for generating Java code from Avro schemas - implementation("io.quarkus:quarkus-avro") + implementation("io.quarkus:quarkus-confluent-registry-avro") // Confluent registry libraries use JAX-RS client implementation("io.quarkus:quarkus-rest-client-reactive") @@ -312,7 +309,10 @@ See xref:kafka-dev-services.adoc[Dev Services for Kafka] and xref:apicurio-regis You might have noticed that we didn't configure the schema registry URL anywhere. This is because Dev Services for Apicurio Registry configures all Kafka channels in SmallRye Reactive Messaging to use the automatically started registry instance. -There's no Dev Services support for Confluent Schema Registry. +Apicurio Registry, in addition to its native API, also exposes an endpoint that is API-compatible with Confluent Schema Registry. +Therefore, this automatic configuration works both for Apicurio Registry serde and Confluent Schema Registry serde. + +However, note that there's no Dev Services support for running Confluent Schema Registry itself. If you want to use a running instance of Confluent Schema Registry, configure its URL, together with the URL of a Kafka broker: [source,properties] @@ -651,7 +651,7 @@ public class KafkaAndSchemaRegistryTestResource implements QuarkusTestResourceLi registry.start(); Map properties = new HashMap<>(); properties.put("mp.messaging.connector.smallrye-kafka.apicurio.registry.url", - "http://" + registry.getContainerIpAddress() + ":" + registry.getMappedPort(8080) + "/apis/registry/v2"); + "http://" + registry.getHost() + ":" + registry.getMappedPort(8080) + "/apis/registry/v2"); properties.put("kafka.bootstrap.servers", kafka.getBootstrapServers()); return properties; } diff --git a/docs/src/main/asciidoc/kafka.adoc b/docs/src/main/asciidoc/kafka.adoc index 00270c48de843..188aff34c87ef 100644 --- a/docs/src/main/asciidoc/kafka.adoc +++ b/docs/src/main/asciidoc/kafka.adoc @@ -1052,6 +1052,80 @@ Reciprocally, multiple producers on the same channel can be merged by setting `m On the `@Incoming` methods, you can control how multiple channels are merged using the `@Merge` annotation. ==== +=== Kafka Transactions + +Kafka transactions enable atomic writes to multiple Kafka topics and partitions. +The Kafka connector provides `KafkaTransactions` custom emitter for writing Kafka records inside a transaction. +It can be injected as a regular emitter `@Channel`: + +[source, java] +---- +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Channel; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; + +@ApplicationScoped +public class KafkaTransactionalProducer { + + @Channel("tx-out-example") + KafkaTransactions txProducer; + + public Uni emitInTransaction() { + return txProducer.withTransaction(emitter -> { + emitter.send(KafkaRecord.of(1, "a")); + emitter.send(KafkaRecord.of(2, "b")); + emitter.send(KafkaRecord.of(3, "c")); + return Uni.createFrom().voidItem(); + }); + } + +} +---- + +The function given to the `withTransaction` method receives a `TransactionalEmitter` for producing records, and returns a `Uni` that provides the result of the transaction. + +* If the processing completes successfully, the producer is flushed and the transaction is committed. +* If the processing throws an exception, returns a failing `Uni`, or marks the `TransactionalEmitter` for abort, the transaction is aborted. + +Kafka transactional producers require configuring `acks=all` client property, and a unique id for `transactional.id`, which implies `enable.idempotence=true`. +When Quarkus detects the use of `KafkaTransactions` for an outgoing channel it configures these properties on the channel, +providing a default value of `"${quarkus.application.name}-${channelName}"` for `transactional.id` property. + +Note that for production use the `transactional.id` must be unique across all application instances. + + +[IMPORTANT] +==== +While a normal message emitter would support concurrent calls to `send` methods and consequently queues outgoing messages to be written to Kafka, +a `KafkaTransactions` emitter only supports one transaction at a time. +A transaction is considered in progress from the call to the `withTransaction` until the returned `Uni` results in success or failure. +While a transaction is in progress, subsequent calls to the `withTransaction`, including nested ones inside the given function, will throw `IllegalStateException`. + +Note that in Reactive Messaging, the execution of processing methods, is already serialized, unless `@Blocking(ordered = false)` is used. +If `withTransaction` can be called concurrently, for example from a REST endpoint, it is recommended to limit the concurrency of the execution. +This can be done using the `@Bulkhead` annotation from link:https://quarkus.io/guides/smallrye-fault-tolerance[_Microprofile Fault Tolerance_]. + +An example usage can be found in <>. +==== + +==== Transaction-aware consumers + +If you'd like to consume records only written and committed inside a Kafka transaction you need to configure the `isolation.level` property on the incoming channel as such: + +[source, properties] +---- +mp.messaging.incoming.prices-in.isolation.level=read_committed +---- + +[NOTE] +==== +If you are using Dev Services for Kafka using Redpanda, you need to <>. +==== + == Processing Messages Applications streaming data often need to consume some events from a topic, process them and publish the result to a different topic. @@ -1116,6 +1190,82 @@ record key propagation produces the outgoing record with the same _key_ as the i If the outgoing record already contains a _key_, it *won't be overridden* by the incoming record key. If the incoming record does have a _null_ key, the `mp.messaging.outgoing.$channel.key` property is used. +=== Exactly-Once Processing + +Kafka Transactions allows managing consumer offsets inside a transaction, together with produced messages. +This enables coupling a consumer with a transactional producer in a _consume-transform-produce_ pattern, also known as *exactly-once processing*. + +The `KafkaTransactions` custom emitter provides a way to apply exactly-once processing to an incoming Kafka message inside a transaction. + +The following example includes a batch of Kafka records inside a transaction. + +[source, java] +---- +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.OnOverflow; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.KafkaRecord; +import io.smallrye.reactive.messaging.kafka.KafkaRecordBatch; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; + +@ApplicationScoped +public class KafkaExactlyOnceProcessor { + + @Channel("prices-out") + @OnOverflow(value = OnOverflow.Strategy.BUFFER, bufferSize = 500) // <3> + KafkaTransactions txProducer; + + @Incoming("prices-in") + public Uni emitInTransaction(KafkaRecordBatch batch) { // <1> + return txProducer.withTransactionAndAck(batch, emitter -> { // <2> + for (KafkaRecord record : batch) { + emitter.send(KafkaRecord.of(record.getKey(), record.getPayload() + 1)); // <3> + } + return Uni.createFrom().voidItem(); + }); + } + +} +---- + +<1> It is recommended to use exactly-once processing along with the batch consumption mode. +While it is possible to use it with a single Kafka message, it'll have a significant performance impact. +<2> The consumed `KafkaRecordBatch` message is passed to the `KafkaTransactions#withTransactionAndAck` in order to handle the offset commits and message acks. +<3> The `send` method writes records to Kafka inside the transaction, without waiting for send receipt from the broker. +Messages pending to be written to Kafka will be buffered, and flushed before committing the transaction. +It is therefore recommended configuring the `@OnOverflow` `bufferSize` in order to fit enough messages, for example the `max.poll.records`, maximum amount of records returned in a batch. + +- If the processing completes successfully, _before committing the transaction_, the topic partition offsets of the given batch message will be committed to the transaction. +- If the processing needs to abort, _after aborting the transaction_, the consumer's position is reset to the last committed offset, effectively resuming the consumption from that offset. If no consumer offset has been committed to a topic-partition, the consumer's position is reset to the beginning of the topic-partition, _even if the offset reset policy is `latest`_. + +When using exactly-once processing, consumed message offset commits are handled by the transaction and therefore the application should not commit offsets through other means. +The consumer should have `enable.auto.commit=false` (the default) and set explicitly `commit-strategy=ignore`: + +[source, properties] +---- +mp.messaging.incoming.prices-in.commit-strategy=ignore +mp.messaging.incoming.prices-in.failure-strategy=ignore +---- + +==== Error handling for the exactly-once processing + +The `Uni` returned from the `KafkaTransactions#withTransaction` will yield a failure if the transaction fails and is aborted. +The application can choose to handle the error case, but if a failing `Uni` is returned from the `@Incoming` method, the incoming channel will effectively fail and stop the reactive stream. + +The `KafkaTransactions#withTransactionAndAck` method acks and nacks the message but will *not* return a failing `Uni`. +Nacked messages will be handled by the failure strategy of the incoming channel, (see <>). +Configuring `failure-strategy=ignore` simply resets the Kafka consumer to the last committed offsets and resumes the consumption from there. + +[NOTE] +==== +Redpanda does not yet support link:https://github.com/redpanda-data/redpanda/issues/3279[producer scalability for exactly-once processing]. +In order to use Kafka exactly-once processing with Quarkus you can configure Dev Services for Kafka to <>. +==== + [[kafka-bare-clients]] == Accessing Kafka clients directly @@ -2386,6 +2536,118 @@ private String toJson(Fruit f) { The workaround is a bit more complex as besides sending the fruits coming from Kafka, we need to send pings periodically. To achieve this we merge the stream coming from Kafka and a periodic stream emitting `{}` every 10 seconds. +[[chaining-kafka-transactions-with-hibernate-reactive-transactions]] +=== Chaining Kafka Transactions with Hibernate Reactive transactions + +By chaining a Kafka transaction with a Hibernate Reactive transaction you can send records to a Kafka transaction, +perform database updates and commit the Kafka transaction only if the database transaction is successful. + +The following example demonstrates: + +* Receive a payload by serving HTTP requests using RESTEasy Reactive, +* Limit concurrency of that HTTP endpoint using Smallrye Fault Tolerance, +* Start a Kafka transaction and send the payload to Kafka record, +* Store the payload in the database using Hibernate Reactive with Panache, +* Commit the Kafka transaction only if the entity is persisted successfully. + +[source, java] +---- +package org.acme; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; + +import org.eclipse.microprofile.faulttolerance.Bulkhead; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.hibernate.reactive.mutiny.Mutiny; + +import io.quarkus.hibernate.reactive.panache.Panache; +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; + +@Path("/") +public class FruitProducer { + + @Channel("kafka") KafkaTransactions kafkaTx; // <1> + + @POST + @Path("/fruits") + @Consumes(MediaType.APPLICATION_JSON) + @Bulkhead(1) // <2> + public Uni post(Fruit fruit) { // <3> + return kafkaTx.withTransaction(emitter -> { // <4> + emitter.send(fruit); // <5> + return Panache.withTransaction(() -> { // <6> + return fruit.persist(); // <7> + }); + }).replaceWithVoid(); + } +} + +---- +<1> Inject a `KafkaTransactions` which exposes a Mutiny API. It allows the integration with the Mutiny API exposed by Hibernate Reactive with Panache. +<2> Limit the concurrency of the HTTP endpoint to "1", preventing starting multiple transactions at a given time. +<3> The HTTP method receiving the payload returns a `Uni`. The HTTP response is written when the operation completes (the entity is persisted and Kafka transaction is committed). +<4> Begin a Kafka transaction. +<5> Send the payload to Kafka inside the Kafka transaction. +<6> Persist the entity into the database in a Hibernate Reactive transaction. +<7> Once the persist operation completes, and there is no errors, the Kafka transaction is committed. +The result is omitted and returned as the HTTP response. + +In the previous example the database transaction (inner) will commit followed by the Kafka transaction (outer). +If you wish to commit the Kafka transaction first and the database transaction second, you need to nest them in the reverse order. + +The next example demostrates that using the Hibernate Reactive API (without Panache): + +[source, java] +---- +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; + +import org.eclipse.microprofile.faulttolerance.Bulkhead; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.hibernate.reactive.mutiny.Mutiny; + +import io.smallrye.mutiny.Uni; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; +import io.vertx.mutiny.core.Context; +import io.vertx.mutiny.core.Vertx; + +@Path("/") +public class FruitProducer { + + @Channel("kafka") KafkaTransactions kafkaTx; + + @Inject Mutiny.SessionFactory sf; // <1> + + @POST + @Path("/fruits") + @Consumes(MediaType.APPLICATION_JSON) + @Bulkhead(1) + public Uni post(Fruit fruit) { + Context context = Vertx.currentContext(); // <2> + return sf.withTransaction(session -> // <3> + kafkaTx.withTransaction(emitter -> // <4> + session.persist(fruit).invoke(() -> emitter.send(fruit)) // <5> + ).emitOn(context::runOnContext) // <6> + ); + } +} +---- + +<1> Inject the Hibernate Reactive `SessionFactory`. +<2> Capture the caller Vert.x context. +<3> Begin a Hibernate Reactive transaction. +<4> Begin a Kafka transaction. +<5> Persist the payload and send the entity to Kafka. +<6> The Kafka transaction terminates on the Kafka producer sender thread. +We need to switch to the Vert.x context previously captured in order to terminate the Hibernate Reactive transaction on the same context we started it. + == Logging To reduce the amount of log written by the Kafka client, Quarkus sets the level of the following log categories to `WARNING`: @@ -2500,6 +2762,115 @@ To allow your Quarkus application to use that secret, add the following line to %prod.quarkus.openshift.env.secrets=kafka-credentials ---- +==== Red Hat OpenShift Service Registry + +https://www.redhat.com/en/technologies/cloud-computing/openshift/openshift-service-registry[Red Hat OpenShift Service Registry] +provides fully managed service registry for handling Kafka schemas. + +You can follow the instructions from +https://access.redhat.com/documentation/en-us/red_hat_openshift_service_registry/1/guide/ab1894d1-cae0-4d11-b185-81d62b4aabc7#_60472331-fa00-48ec-a621-bbd039500c7d[Getting started with Red Hat OpenShift Service Registry], +or use the `rhoas` CLI to create a new service registry instance: + +[source, shell] +---- +rhoas service-registry create --name my-schema-registry +---- + +Make sure to note the _Registry URL_ of the instance created. +For authentication, you can use the same _ServiceAccount_ you created previously. +You need to make sure that it has the necessary permissions to access the service registry. + +For example, using the `rhoas` CLI, you can grant the `MANAGER` role to the service account: + +[source, shell] +---- +rhoas service-registry role add --role manager --service-account [SERVICE_ACCOUNT_CLIENT_ID] +---- + +Then, you can configure the Quarkus application to connect to the schema registry as follows: + +[source, properties] +---- +mp.messaging.connector.smallrye-kafka.apicurio.registry.url=${RHOAS_SERVICE_REGISTRY_URL} <1> +mp.messaging.connector.smallrye-kafka.apicurio.auth.service.token.endpoint=${RHOAS_OAUTH_TOKEN_ENDPOINT} <2> +mp.messaging.connector.smallrye-kafka.apicurio.auth.client.id=${RHOAS_CLIENT_ID} <3> +mp.messaging.connector.smallrye-kafka.apicurio.auth.client.secret=${RHOAS_CLIENT_ID} <4> +---- +<1> The service registry URL, given on the admin console, such as `https://bu98.serviceregistry.rhcloud.com/t/0e95af2c-6e11-475e-82ee-f13bd782df24/apis/registry/v2` +<2> The OAuth token endpoint URL, such as `https://identity.api.openshift.com/auth/realms/rhoas/protocol/openid-connect/token` +<3> The client id (from the service account) +<4> The client secret (from the service account) + +==== Binding Red Hat OpenShift managed services to Quarkus application using the Service Binding Operator + +If your Quarkus application is deployed on a Kubernetes or OpenShift cluster with link:https://github.com/redhat-developer/service-binding-operator[Service Binding Operator] and link:https://github.com/redhat-developer/app-services-operator/tree/main/docs[OpenShift Application Services] operators installed, +configurations necessary to access Red Hat OpenShift Streams for Apache Kafka and Service Registry can be injected to the application using xref:deploying-to-kubernetes.adoc#service_binding[Kubernetes Service Binding]. + +In order to setup the Service Binding, you need first to connect OpenShift managed services to your cluster. +For an OpenShift cluster you can follow the instructions from link:https://github.com/redhat-developer/app-services-guides/tree/main/docs/registry/service-binding-registry#connecting-a-kafka-and-service-registry-instance-to-your-openshift-cluster[Connecting a Kafka and Service Registry instance to your OpenShift cluster]. + +Once you've connected your cluster with the RHOAS Kafka and Service Registry instances, make sure you've granted necessary permissions to the newly created service account. + +Then, using the xref:deploying-to-kubernetes.adoc#service_binding[Kubernetes Service Binding] extension, +you can configure the Quarkus application to generate `ServiceBinding` resources for those services: + +[source, properties] +---- +quarkus.kubernetes-service-binding.detect-binding-resources=true + +quarkus.kubernetes-service-binding.services.kafka.api-version=rhoas.redhat.com/v1alpha1 +quarkus.kubernetes-service-binding.services.kafka.kind=KafkaConnection +quarkus.kubernetes-service-binding.services.kafka.name=my-kafka + +quarkus.kubernetes-service-binding.services.serviceregistry.api-version=rhoas.redhat.com/v1alpha1 +quarkus.kubernetes-service-binding.services.serviceregistry.kind=ServiceRegistryConnection +quarkus.kubernetes-service-binding.services.serviceregistry.name=my-schema-registry +---- + +For this example Quarkus build will generate the following `ServiceBinding` resources: + +[source, yaml] +---- +apiVersion: binding.operators.coreos.com/v1alpha1 +kind: ServiceBinding +metadata: + name: my-app-kafka +spec: + application: + group: apps.openshift.io + name: my-app + version: v1 + kind: DeploymentConfig + services: + - group: rhoas.redhat.com + version: v1alpha1 + kind: KafkaConnection + name: my-kafka + detectBindingResources: true + bindAsFiles: true +--- +apiVersion: binding.operators.coreos.com/v1alpha1 +kind: ServiceBinding +metadata: + name: my-app-serviceregistry +spec: + application: + group: apps.openshift.io + name: my-app + version: v1 + kind: DeploymentConfig + services: + - group: rhoas.redhat.com + version: v1alpha1 + kind: ServiceRegistryConnection + name: my-schema-registry + detectBindingResources: true + bindAsFiles: true +---- + +You can follow xref:deploying-to-kubernetes.adoc#openshift[Deploying to OpenShift] to deploy your application, including generated `ServiceBinding` resources. +The configuration properties necessary to access the Kafka and Schema Registry instances will be injected to the application automatically at deployment. + == Going further This guide has shown how you can interact with Kafka using Quarkus. diff --git a/docs/src/main/asciidoc/kotlin.adoc b/docs/src/main/asciidoc/kotlin.adoc index 1efedd053e9bf..0cb5fc1707cce 100644 --- a/docs/src/main/asciidoc/kotlin.adoc +++ b/docs/src/main/asciidoc/kotlin.adoc @@ -18,6 +18,12 @@ include::./status-include.adoc[] include::includes/devtools/prerequisites.adoc[] +[WARNING] +==== +If building with Mandrel, make sure to use version Mandrel 22.1 or above, for example `ubi-quarkus-mandrel:22.1-java17`. +With older versions, you might encounter errors when trying to deserialize JSON documents that have null or missing fields, similar to the errors mentioned in the <> section. +==== + NB: For Gradle project setup please see below, and for further reference consult the guide in the xref:gradle-tooling.adoc[Gradle setup page]. == Creating the Maven project @@ -321,6 +327,55 @@ When you now execute an HTTP GET request against `http://localhost:8080/hello`, One thing to note is that the live reload feature is not available when making changes to both Java and Kotlin source that have dependencies on each other. We hope to alleviate this limitation in the future. + +=== Configuring live reload compiler + +If you need to customize the compiler flags used by `kotlinc` in development mode, you can configure them in the quarkus plugin: + +[source, xml, subs=attributes+, role="primary asciidoc-tabs-sync-maven"] +.Maven +---- + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + + + ${maven.compiler.source} + ${maven.compiler.target} + + + kotlin + + -Werror + + + + + + ... + +---- +[source, groovy, subs=attributes+, role="secondary asciidoc-tabs-sync-groovy"] +.Gradle (Groovy DSL) +---- +quarkusDev { + compilerOptions { + compiler("kotlin").args(['-Werror']) + } +} +---- + +[source, kotlin, subs=attributes+, role="secondary asciidoc-tabs-sync-kotlin"] +.Gradle (Kotlin DSL) +---- +tasks.quarkusDev { + compilerOptions { + compiler("kotlin").args(["-Werror"]) + } +} +---- + == Packaging the application As usual, the application can be packaged using: @@ -333,6 +388,7 @@ You can also build the native executable using: include::includes/devtools/build-native.adoc[] +[[kotlin-jackson]] == Kotlin and Jackson If the `com.fasterxml.jackson.module:jackson-module-kotlin` dependency and the `quarkus-jackson` extension (or one of the `quarkus-resteasy-jackson` or `quarkus-resteasy-reactive-jackson` extensions) have been added to the project, diff --git a/docs/src/main/asciidoc/maven-tooling.adoc b/docs/src/main/asciidoc/maven-tooling.adoc index 35117688831f8..0742515fef549 100644 --- a/docs/src/main/asciidoc/maven-tooling.adoc +++ b/docs/src/main/asciidoc/maven-tooling.adoc @@ -448,9 +448,13 @@ If you have not used <>, add the following [source,xml,subs=attributes+] ---- + + true <1> + + - <1> + <2> ${quarkus.platform.group-id} quarkus-bom ${quarkus.platform.version} @@ -462,11 +466,11 @@ If you have not used <>, add the following - <2> + <3> ${quarkus.platform.group-id} quarkus-maven-plugin ${quarkus.platform.version} - true <3> + true <4> @@ -477,7 +481,7 @@ If you have not used <>, add the following - <4> + <5> org.apache.maven.plugins maven-surefire-plugin ${surefire-plugin.version} @@ -488,51 +492,53 @@ If you have not used <>, add the following + <6> + org.apache.maven.plugins + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + - <5> + <7> native - <6> + <8> native + false <9> - - - <7> - org.apache.maven.plugins - maven-failsafe-plugin - ${surefire-plugin.version} - - - - integration-test - verify - - - - ${project.build.directory}/${project.build.finalName}-runner - org.jboss.logmanager.LogManager - ${maven.home} - - - - - - - ---- -<1> Optionally use a BOM file to omit the version of the different Quarkus dependencies. -<2> Use the Quarkus Maven plugin that will hook into the build process. -<3> Enabling Maven plugin extensions will register a Quarkus `MavenLifecycleParticipant` which will make sure the Quarkus classloaders used during the build are properly closed. During the `generate-code` and `generate-code-tests` goals the Quarkus application bootstrap is initialized and re-used in the `build` goal (which actually builds and packages a production application). The Quarkus classloaders will be properly closed in the `build` goal of the `quarkus-maven-plugin`. However, if the build fails in between the `generate-code` or `generate-code-tests` and `build` then the Quarkus augmentation classloader won't be properly closed, which may lead to locking of JAR files that happened to be on the classpath on Windows OS. -<4> Add system properties to `maven-surefire-plugin`. + +<1> Disable running of integration tests (test names `*IT` and annotated with `@QuarkusIntegrationTest`) on all builds. To run these tests all the time, either remove this property, set its value to `false`, or set `-DskipITs=false` on the command line when you run the build. + +As mentioned below, this is overridden in the `native` profile. +<2> Optionally use a BOM file to omit the version of the different Quarkus dependencies. +<3> Use the Quarkus Maven plugin that will hook into the build process. +<4> Enabling Maven plugin extensions will register a Quarkus `MavenLifecycleParticipant` which will make sure the Quarkus classloaders used during the build are properly closed. During the `generate-code` and `generate-code-tests` goals the Quarkus application bootstrap is initialized and re-used in the `build` goal (which actually builds and packages a production application). The Quarkus classloaders will be properly closed in the `build` goal of the `quarkus-maven-plugin`. However, if the build fails in between the `generate-code` or `generate-code-tests` and `build` then the Quarkus augmentation classloader won't be properly closed, which may lead to locking of JAR files that happened to be on the classpath on Windows OS. +<5> Add system properties to `maven-surefire-plugin`. + +`maven.home` is only required if you have custom configuration in `${maven.home}/conf/settings.xml`. +<6> If you want to test the artifact produced by your build with Integration Tests, add the following plugin configuration. Test names `*IT` and annotated with `@QuarkusIntegrationTest` will be run against the artifact produced by the build (JAR file, container image, etc). See the xref:getting-started-testing.adoc#quarkus-integration-test[Integration Testing guide] for more info. + `maven.home` is only required if you have custom configuration in `${maven.home}/conf/settings.xml`. -<5> Use a specific `native` profile for native executable building. -<6> Enable the `native` package type. The build will therefore produce a native executable. -<7> If you want to test your native executable with Integration Tests, add the following plugin configuration. Test names `*IT` and annotated `@NativeImageTest` or `@QuarkusIntegrationTest` will be run against the native executable. See the xref:building-native-image.adoc[Native executable guide] for more info. +<7> Use a specific `native` profile for native executable building. +<8> Enable the `native` package type. The build will therefore produce a native executable. +<9> Always run integration tests when building a native image (test names `*IT` and annotated with `@QuarkusIntegrationTest` or `@NativeImageTest`). + +If you do not wish to run integration tests when building a native image, simply remove this property altogether or set its value to `true`. [[fast-jar]] === Using fast-jar diff --git a/docs/src/main/asciidoc/mutiny-primer.adoc b/docs/src/main/asciidoc/mutiny-primer.adoc index 286bc87db2fc0..95428db34885c 100644 --- a/docs/src/main/asciidoc/mutiny-primer.adoc +++ b/docs/src/main/asciidoc/mutiny-primer.adoc @@ -296,7 +296,7 @@ Fortunately, Mutiny provides a set of shortcut to make your code more concise: | uni.then(() → uni2) | uni.onItem().transformToUni(ignored → uni2) | uni.invoke(x → System.out.println(x)) | uni.onItem().invoke(x → System.out.println(x)) | uni.call(x → uni2) | uni.onItem().call(x → uni2) -| uni.eventually) → System.out.println("eventually" | uni.onItemOrFailure().invokeignoredItem, ignoredException) → System.out.println("eventually" +| uni.eventually(() → System.out.println("eventually")) | uni.onItemOrFailure().invoke((ignoredItem, ignoredException) → System.out.println("eventually")) | uni.eventually(() → uni2) | uni.onItemOrFailure().call((ignoredItem, ignoredException) → uni2) |=== diff --git a/docs/src/main/asciidoc/opentelemetry.adoc b/docs/src/main/asciidoc/opentelemetry.adoc index d8e4d18c948f0..e1425fecb6d57 100644 --- a/docs/src/main/asciidoc/opentelemetry.adoc +++ b/docs/src/main/asciidoc/opentelemetry.adoc @@ -135,8 +135,7 @@ receivers: exporters: jaeger: endpoint: jaeger-all-in-one:14250 - tls: - insecure: true + insecure: true processors: batch: diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 73f27388ad897..63a29e14a3224 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -891,6 +891,7 @@ The following operators are supported in `is`/`case` block conditions: ==== Let Section This section allows you to define named local variables: + [source,html] ---- {#let myParent=order.item.parent isActive=false age=10} <1> @@ -902,7 +903,18 @@ This section allows you to define named local variables: <1> The local variable is initialized with an expression that can also represent a <>. <2> Keep in mind that the variable is not available outside the `let` section that defines it. -The section tag is also registered under the `set` alias: +If a key of a section parameter (aka the name of the local variable) ends with a `?` then the local variable is only set if the key without the `?` suffix resolves to `null` or _"not found"_: + +[source,html] +---- +{#let enabled?=true} <1> <2> + {#if enabled}ON{/if} +{/let} +---- +<1> `true` is effectively a _default value_ that is only used if the parent scope does not define `enabled` already. +<2> `enabled?=true` is a short version of `enabled=enabled.or(true)`. + +This section tag is also registered under the `set` alias: [source,html] ---- @@ -960,7 +972,7 @@ This section might also come in handy when we'd like to avoid multiple expensive [[include_helper]] ==== Include Section -This section can be used to include another template and possibly override some parts of the template (template inheritance). +This section can be used to include another template and possibly override some parts of the template (see the _template inheritance_ below). .Simple Example [source,html] @@ -978,7 +990,7 @@ This section can be used to include another template and possibly override some <1> Include a template with id `foo`. The included template can reference data from the current context. <2> It's also possible to define optional parameters that can be used in the included template. -Template inheritance makes it possible to reuse template layouts. +_Template inheritance_ makes it possible to reuse template layouts. .Template "base" [source,html] @@ -994,7 +1006,7 @@ Template inheritance makes it possible to reuse template layouts. ---- <1> `insert` sections are used to specify parts that could be overridden by a template that includes the given template. -<2> An `insert` section may define the default content that is rendered if not overridden. If no name parameter is supplied then the main block of the relevant `{#include}` section is used. +<2> An `insert` section may define the default content that is rendered if not overridden. If there is no name supplied then the main block of the relevant `{#include}` section is used. .Template "detail" [source,html] @@ -1012,28 +1024,10 @@ Template inheritance makes it possible to reuse template layouts. NOTE: Section blocks can also define an optional end tag - `{/title}`. -==== Eval Section - -This section can be used to parse and evaluate a template dynamically. -The behavior is very similar to the <> but: - -1. The template content is passed directly, i.e. not obtained via an `io.quarkus.qute.TemplateLocator`, -2. It's not possible to override parts of the evaluated template. - -[source,html] ----- -{#eval myData.template name='Mia' /} <1><2><3> ----- -<1> The result of `myData.template` will be used as the template. The template is executed with the <>, i.e. can reference data from the template it's included into. -<2> It's also possible to define optional parameters that can be used in the evaluated template. -<3> The content of the section is always ignored. - -NOTE: The evaluated template is parsed and evaluated every time the section is executed. In other words, it's not possible to cache the parsed value to conserve resources and optimize the performance. - [[user_tags]] ==== User-defined Tags -User-defined tags can be used to include a tag template and optionally pass some parameters. +User-defined tags can be used to include a _tag template_, optionally pass some arguments and possibly override some parts of the template. Let's suppose we have a tag template called `itemDetail.html`: [source] @@ -1080,6 +1074,44 @@ For example, the tag above could use the following expression `{items.size}`. However, sometimes it might be useful to disable this behavior and execute the tag as an _isolated_ template, i.e. without access to the context of the template that calls the tag. In this case, just add `_isolated` or `_isolated=true` argument to the call site, e.g. `{#itemDetail item showImage=true _isolated /}`. +User tags can also make use of the template inheritance in the same way as regular `{#include}` sections do. + +.Tag `myTag` +[source] +---- +This is {#insert title}my title{/title}! <1> +---- +<1> `insert` sections are used to specify parts that could be overridden by a template that includes the given template. + +.Tag Call Site +[source,html] +---- +

+ {#myTag} + {title}my custom title{/title} <1> + {/myTag} +

+---- +<1> The result would be something like `

This is my custom title!

`. + +==== Eval Section + +This section can be used to parse and evaluate a template dynamically. +The behavior is very similar to the <> but: + +1. The template content is passed directly, i.e. not obtained via an `io.quarkus.qute.TemplateLocator`, +2. It's not possible to override parts of the evaluated template. + +[source,html] +---- +{#eval myData.template name='Mia' /} <1><2><3> +---- +<1> The result of `myData.template` will be used as the template. The template is executed with the <>, i.e. can reference data from the template it's included into. +<2> It's also possible to define optional parameters that can be used in the evaluated template. +<3> The content of the section is always ignored. + +NOTE: The evaluated template is parsed and evaluated every time the section is executed. In other words, it's not possible to cache the parsed value to conserve resources and optimize the performance. + === Rendering Output `TemplateInstance` provides several ways to trigger the rendering and consume the result. @@ -1353,6 +1385,19 @@ Note that sections can override names that would otherwise match a parameter dec <1> Validated against `org.acme.Foo`. <2> Not validated - `foo` is overridden in the loop section. +A paramater declaration may specify the _default value_ after the key. +The key and the default value are separated by an equals sign: `{@int age=10}`. +The default value is used in the template if the parameter key resolves to `null` or is not found. + +For example, if there's a parameter declaration `{@String foo="Ping"}` and `foo` is not found then you can use `{foo}` and the output will be `Ping`. +On the other hand, if the value is set (e.g. via `TemplateInstance.data("foo", "Pong")`) then the output of `{foo}` will be `Pong`. + +The type of a default value must be assignable to the type of the parameter declaration, i.e. the following parameter declaration is incorrect and results in a build failure: `{@org.acme.Foo foo=1}`. + +TIP: The default value is actually an <>. So the default value does not have to be a literal (such as `42` or `true`). For example, you can leverage the `@TemplateEnum` and specify an enum constant as a default value of a parameter declaration: `{@org.acme.MyEnum myEnum=MyEnum:FOO}`. However, the infix notation is not supported in default values. + +IMPORTANT: The type of a default value is not validated in <>. + .More Parameter Declarations Examples [source] ---- @@ -1360,11 +1405,13 @@ Note that sections can override names that would otherwise match a parameter dec {@java.util.List strings} <2> {@java.util.Map numbers} <3> {@java.util.Optional param} <4> +{@String name="Quarkus"} <5> ---- <1> A primitive type. <2> `String` is replaced with `java.lang.String`: `{@java.util.List strings}` <3> The wildcard is ignored and the upper bound is used instead: `{@java.util.Map}` <4> The wildcard is ignored and the `java.lang.Object` is used instead: `{@java.util.Optional}` +<5> The type is `java.lang.String`, the key is `name` and the default value is `Quarkus`. [[typesafe_templates]] === Type-safe Templates @@ -2044,7 +2091,7 @@ public interface AppMessages { } ---- <1> Denotes a message bundle interface. The bundle name is defaulted to `msg` and is used as a namespace in templates expressions, e.g. `{msg:hello_name}`. -<2> Each method must be annotated with `@Message`. The value is a qute template. +<2> Each method must be annotated with `@Message`. The value is a qute template. If no value is provided, then a corresponding value from a localized file is taken. If no such file exists an exception is thrown and the build fails. <3> The method parameters can be used in the template. ==== Bundle Name and Message Keys @@ -2169,6 +2216,42 @@ public class MyBean { ---- <1> The annotation value is a locale tag string (IETF). +==== Message Templates + +Every method of a message bundle interface must define a message template. The value is normally defined by `io.quarkus.qute.i18n.Message#value()`, +but for convenience, there is also an option to define the value in a localized file. + +.Example of the Message Bundle Interface without the value +[source,java] +---- +import io.quarkus.qute.i18n.Message; +import io.quarkus.qute.i18n.MessageBundle; + +@MessageBundle +public interface AppMessages { + + @Message <1> + String hello_name(String name); + + @Message("Goodbye {name}!") <2> + String goodbye(String name); +} +---- +<1> The annotation value is not defined. In such a case, the value from supplementary localized file is taken. +<2> The annotation value is defined and preferred to the value defined in the localized file. + +.Supplementary localized file +[source,properties] +---- +hello_name=Hello \ + {name} and \ + good morning! +goodbye=Best regards, {name} <1> +---- +<1> The value is ignored as `io.quarkus.qute.i18n.Message#value()` is always prioritized. + +Message templates are validated during the build. If a missing message template is detected, an exception is thrown and build fails. + === Configuration Reference diff --git a/docs/src/main/asciidoc/rest-client-reactive.adoc b/docs/src/main/asciidoc/rest-client-reactive.adoc index bb92f46fbc920..6734e6e34d19e 100644 --- a/docs/src/main/asciidoc/rest-client-reactive.adoc +++ b/docs/src/main/asciidoc/rest-client-reactive.adoc @@ -263,15 +263,12 @@ And the service as follows: ---- package org.acme.rest.client; -import io.smallrye.mutiny.Uni; import org.eclipse.microprofile.rest.client.RestClientBuilder; -import org.eclipse.microprofile.rest.client.inject.RestClient; import javax.ws.rs.GET; import javax.ws.rs.Path; import java.net.URI; import java.util.Set; -import java.util.concurrent.CompletionStage; @Path("/extension") public class ExtensionsResource { @@ -427,15 +424,13 @@ The `Uni` version is very similar: ---- package org.acme.rest.client; +import io.smallrye.mutiny.Uni; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; -import org.jboss.resteasy.annotations.jaxrs.PathParam; import javax.ws.rs.GET; import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import java.util.Set; -import java.util.concurrent.CompletionStage; @Path("/extensions") @RegisterRestClient(configKey = "extensions-api") @@ -454,14 +449,12 @@ The `ExtensionsResource` becomes: ---- package org.acme.rest.client; -import io.smallrye.common.annotation.Blocking; import io.smallrye.mutiny.Uni; import org.eclipse.microprofile.rest.client.inject.RestClient; import javax.ws.rs.GET; import javax.ws.rs.Path; import java.util.Set; -import java.util.concurrent.CompletionStage; @Path("/extension") public class ExtensionsResource { @@ -519,7 +512,6 @@ The code below demonstrates how to use each of these techniques: ---- package org.acme.rest.client; -import io.smallrye.mutiny.Uni; import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam; import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; @@ -529,7 +521,6 @@ import javax.ws.rs.HeaderParam; import javax.ws.rs.Path; import javax.ws.rs.QueryParam; import java.util.Set; -import java.util.concurrent.CompletionStage; @Path("/extensions") @RegisterRestClient @@ -597,6 +588,8 @@ Also, there is a reactive flavor of `ClientHeadersFactory` that allows doing blo ---- package org.acme.rest.client; +import io.smallrye.mutiny.Uni; + import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory; import javax.enterprise.context.ApplicationScoped; diff --git a/docs/src/main/asciidoc/rest-client.adoc b/docs/src/main/asciidoc/rest-client.adoc index 2a61527b55439..6be30f043a969 100644 --- a/docs/src/main/asciidoc/rest-client.adoc +++ b/docs/src/main/asciidoc/rest-client.adoc @@ -685,7 +685,7 @@ public class WireMockExtensions implements QuarkusTestResourceLifecycleManager { wireMockServer = new WireMockServer(); wireMockServer.start(); // <3> - stubFor(get(urlEqualTo("/extensions?id=io.quarkus:quarkus-rest-client")) // <4> + wireMockServer.stubFor(get(urlEqualTo("/extensions?id=io.quarkus:quarkus-rest-client")) // <4> .willReturn(aResponse() .withHeader("Content-Type", "application/json") .withBody( @@ -695,7 +695,7 @@ public class WireMockExtensions implements QuarkusTestResourceLifecycleManager { "}]" ))); - stubFor(get(urlMatching(".*")).atPriority(10).willReturn(aResponse().proxiedFrom("https://stage.code.quarkus.io/api"))); // <5> + wireMockServer.stubFor(get(urlMatching(".*")).atPriority(10).willReturn(aResponse().proxiedFrom("https://stage.code.quarkus.io/api"))); // <5> return Collections.singletonMap("quarkus.rest-client.\"org.acme.rest.client.ExtensionsService\".url", wireMockServer.baseUrl()); // <6> } diff --git a/docs/src/main/asciidoc/resteasy-reactive-migration.adoc b/docs/src/main/asciidoc/resteasy-reactive-migration.adoc index 859112394956d..ef77fa548b3b4 100644 --- a/docs/src/main/asciidoc/resteasy-reactive-migration.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive-migration.adoc @@ -88,12 +88,21 @@ The following table matches the legacy RESTEasy annotations with the new RESTEas |=== +NOTE: The previous table does not include the `org.jboss.resteasy.annotations.Form` annotation because there is no RESTEasy Reactive specific replacement for it. +Users are instead encouraged to use the JAX-RS standard `javax.ws.rs.BeanParam` annotation which is supported on both the server and the client. + === JAX-RS providers Although RESTEasy Reactive provides the same spec compliant behavior as RESTEasy Classic does, it does not include the same exact provider implementations at runtime. The most common case where the difference in providers might result in different behavior, is the included `javax.ws.rs.ext.ExceptionMapper` implementations. To see what classes are included in the application, launch the application in dev mode and navigate to http://localhost:8080/q/dev/io.quarkus.quarkus-resteasy-reactive/exception-mappers. +==== Service Loading + +RESTEasy Classic supports determining providers at build time using Java's Service Loader. In order to ensure that all providers are determined at build time, +RESTEasy Reactive does not support this feature. Instead, users that have providers in application dependencies are encouraged to index those dependencies +using one of the methods described in the xref:cdi-reference.adoc#bean_discovery[Bean Discovery] section of the CDI guide. + === Multipart support HTTP Multipart support in RESTEasy Reactive does **not** reuse the same types or annotations as RESTEasy Classic and thus users are encouraged to read <> part of the reference documentation. @@ -148,5 +157,37 @@ Similarly, `quarkus-oidc-token-propagation` allows user of the legacy REST to pr When using `quarkus-rest-client-reactive` however, users must use `quarkus-oidc-token-propagation-reactive` to access the same functionality. +=== Custom extensions + +This is an advanced section that only needs to be read by users who have developed custom extensions that depend on JAX-RS and / or REST Client functionality. + +==== Dependencies + +A first concern is whether custom extensions should depend on RESTEasy Reactive explicitly, or alternatively support both RESTEasy flavors and leave it to the user to decide. +If the extension is some general purpose extension, it probably makes sense to choose the latter option, while the former option is easiest to adopt when the custom +extension is used by a specific set of users / applications. + +When opting for supporting both extensions, the deployment module of the custom extension will usually depend on the SPI modules - `quarkus-jaxrs-spi-deployment`, `quarkus-resteasy-common-spi`, `quarkus-resteasy-reactive-spi-deployment`, +while the runtime modules will have `optional` dependencies on the runtime modules of both RESTEasy flavors. + +A couple good examples of how Quarkus uses this strategy to support both RESTEasy flavors in the core repository can be seen [here](https://github.com/quarkusio/quarkus/pull/21089) and [here](https://github.com/quarkusio/quarkus/pull/20874). + +In general, it should not be needed to have two different versions of the custom extension to support both flavors. Such a choice is only strictly necessary if it is desired for the extension consumers (i.e. Quarkus applications) to not have to select a RESTEasy version themselves. + +==== Resource and Provider discovery + +Custom extensions that contain JAX-RS Resources, Providers or REST Client interfaces in their runtime modules and depend on Jandex indexing for +their discovery (for example because they have an empty `META-INF/beans.xml` file) don't have to perform any additional setup to make +these discoverable by RESTEasy Reactive. + +==== Provider registration via Build Items + +Extensions that register providers via build items use the `io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem` build item in RESTEasy Classic. +With RESTEasy Reactive however, extensions need to use specific build items, such as `io.quarkus.resteasy.reactive.spi.MessageBodyWriterBuildItem` and `io.quarkus.resteasy.reactive.spi.MessageBodyWriterBuildItem`. + +==== REST Client + +Any code that is run as part of a Quarkus application that used the REST Client, can safely use the Reactive REST Client, as all necessary setup for it has been done at the application's static-init phase. + diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index 22775b35c4606..24fcdfca77192 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -1138,6 +1138,240 @@ Importing this module will allow HTTP message bodies to be read from XML and serialised to XML, for <>. +=== Web Links support + +[[links]] + +To enable Web Links support, add the `quarkus-resteasy-reactive-links` extension to your project. + +.Table Context object +|=== +|GAV|Usage + +|`io.quarkus:quarkus-resteasy-reactive-links` +|https://www.w3.org/wiki/LinkHeader[Web Links support] + +|=== + +Importing this module will allow injecting web links into the response HTTP headers by just annotating your endpoint resources with the `@InjectRestLinks` annotation. To declare the web links that will be returned, you need to use the `@RestLink` annotation in the linked methods. An example of this could look like: + +[source,java] +---- +@Path("/records") +public class RecordsResource { + + @GET + @RestLink(rel = "list") + @InjectRestLinks + public List getAll() { + // ... + } + + @GET + @Path("/{id}") + @RestLink(rel = "self") + @InjectRestLinks(RestLinkType.INSTANCE) + public TestRecord get(@PathParam("id") int id) { + // ... + } + + @PUT + @Path("/{id}") + @RestLink + @InjectRestLinks(RestLinkType.INSTANCE) + public TestRecord update(@PathParam("id") int id) { + // ... + } + + @DELETE + @Path("/{id}") + @RestLink + public TestRecord delete(@PathParam("id") int id) { + // ... + } +} +---- + +When calling the endpoint `/records` which is defined by the method `getAll` within the above resource using curl, you would get the web links header: + +[source,bash] +---- +& curl -i localhost:8080/records +Link: ; rel="list" +---- + +As this resource does not return a single instance of type `Record`, the links for the methods `get`, `update`, and `delete` are not injected. Now, when calling the endpoint `/records/1`, you would get the following web links: + +[source,bash] +---- +& curl -i localhost:8080/records/1 +Link: ; rel="list" +Link: ; rel="self" +Link: ; rel="update" +Link: ; rel="delete" +---- + +Finally, when calling the delete resource, you should not see any web links as the method `delete` is not annotated with the `@InjectRestLinks` annotation. + +==== Programmatically access to the web links registry + +You can programmatically have access to the web links registry just by injecting the `RestLinksProvider` bean: + +[source,java] +---- +@Path("/records") +public class RecordsResource { + + @Inject + RestLinksProvider linksProvider; + + // ... +} +---- + +Using this injected bean of type `RestLinksProvider`, you can get the links by type using the method `RestLinksProvider.getTypeLinks` or get the links by a concrete instance using the method `RestLinksProvider.getInstanceLinks`. + +==== JSON Hypertext Application Language (HAL) support + +The https://tools.ietf.org/id/draft-kelly-json-hal-01.html[HAL] standard is a simple format to represent web links. + +To enable the HAL support, add the `quarkus-hal` extension to your project. Also, as HAL needs JSON support, you need to add either the `quarkus-resteasy-reactive-jsonb` or the `quarkus-resteasy-reactive-jackson` extension. + +.Table Context object +|=== +|GAV|Usage + +|`io.quarkus:quarkus-hal` +|https://tools.ietf.org/id/draft-kelly-json-hal-01.html[HAL] + +|=== + +After adding the extensions, we can now annotate the REST resources to produce the media type `application/hal+json` (or use RestMediaType.APPLICATION_HAL_JSON). For example: + +[source,java] +---- +@Path("/records") +public class RecordsResource { + + @GET + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(rel = "list") + @InjectRestLinks + public List getAll() { + // ... + } + + @GET + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @Path("/{id}") + @RestLink(rel = "self") + @InjectRestLinks(RestLinkType.INSTANCE) + public TestRecord get(@PathParam("id") int id) { + // ... + } +} +---- + +Now, the endpoints `/records` and `/records/{id}` will accept the media type both `json` and `hal+json` to print the records in Hal format. + +For example, if we invoke the `/records` endpoint using curl to return a list of records, the HAL format will look like as follows: + +[source,bash] +---- +& curl -H "Accept:application/hal+json" -i localhost:8080/records +{ + "_embedded": { + "items": [ + { + "id": 1, + "slug": "first", + "value": "First value", + "_links": { + "self": { + "href": "http://localhost:8081/records/1" + }, + "list": { + "href": "http://localhost:8081/records" + } + } + }, + { + "id": 2, + "slug": "second", + "value": "Second value", + "_links": { + "self": { + "href": "http://localhost:8081/records/2" + }, + "list": { + "href": "http://localhost:8081/records" + } + } + } + ] + }, + "_links": { + "list": { + "href": "http://localhost:8081/records" + } + } +} +---- + +When we call a resource `/records/1` that returns only one instance, then the output is: + +[source,bash] +---- +& curl -H "Accept:application/hal+json" -i localhost:8080/records/1 +{ + "id": 1, + "slug": "first", + "value": "First value", + "_links": { + "self": { + "href": "http://localhost:8081/records/1" + }, + "list": { + "href": "http://localhost:8081/records" + } + } +} +---- + +Finally, you can also provide additional HAL links programmatically in your resource just by returning either `HalCollectionWrapper` (to return a list of entities) or `HalEntityWrapper` (to return a single object) as described in the following example: + +[source,java] +---- +@Path("/records") +public class RecordsResource { + + @Inject + RestLinksProvider linksProvider; + + @GET + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(rel = "list") + public HalCollectionWrapper getAll() { + List list = // ... + HalCollectionWrapper halCollection = new HalCollectionWrapper(list, "collectionName", linksProvider.getTypeLinks(Record.class)); + halCollection.addLinks(Link.fromPath("/records/1").rel("first-record").build()); + return halCollection; + } + + @GET + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @Path("/{id}") + @RestLink(rel = "self") + @InjectRestLinks(RestLinkType.INSTANCE) + public HalEntityWrapper get(@PathParam("id") int id) { + Record entity = // ... + HalEntityWrapper halEntity = new HalEntityWrapper(entity, linksProvider.getInstanceLinks(entity)); + halEntity.addLinks(Link.fromPath("/records/1/parent").rel("parent-record").build()); + return halEntity; + } +} +---- + == CORS filter link:https://en.wikipedia.org/wiki/Cross-origin_resource_sharing[Cross-origin resource sharing] (CORS) is a mechanism that diff --git a/docs/src/main/asciidoc/resteasy.adoc b/docs/src/main/asciidoc/resteasy.adoc index 8fb011682fa01..1ffc2601f4e24 100644 --- a/docs/src/main/asciidoc/resteasy.adoc +++ b/docs/src/main/asciidoc/resteasy.adoc @@ -290,6 +290,111 @@ public class CustomJsonbConfig { } ---- +[[links]] +=== JSON Hypertext Application Language (HAL) support + +The https://tools.ietf.org/id/draft-kelly-json-hal-01.html[HAL] standard is a simple format to represent web links. + +To enable the HAL support, add the `quarkus-hal` extension to your project. Also, as HAL needs JSON support, you need to add either the `quarkus-resteasy-jsonb` or the `quarkus-resteasy-jackson` extension. + +.Table Context object +|=== +|GAV|Usage + +|`io.quarkus:quarkus-hal` +|https://tools.ietf.org/id/draft-kelly-json-hal-01.html[HAL] + +|=== + +After adding the extensions, we can now annotate the REST resources to produce the media type `application/hal+json` (or use RestMediaType.APPLICATION_HAL_JSON). For example: + +[source,java] +---- +@Path("/records") +public class RecordsResource { + + @GET + @Produces({ MediaType.APPLICATION_JSON, "application/hal+json" }) + @LinkResource(entityClassName = "org.acme.Record", rel = "list") + public List getAll() { + // ... + } + + @GET + @Path("/first") + @Produces({ MediaType.APPLICATION_JSON, "application/hal+json" }) + @LinkResource(rel = "first") + public TestRecord getFirst() { + // ... + } +} +---- + +Now, the endpoints `/records` and `/records/first` will accept the media type both `json` and `hal+json` to print the records in Hal format. + +For example, if we invoke the `/records` endpoint using curl to return a list of records, the HAL format will look like as follows: + +[source,bash] +---- +& curl -H "Accept:application/hal+json" -i localhost:8080/records +{ + "_embedded": { + "items": [ + { + "id": 1, + "slug": "first", + "value": "First value", + "_links": { + "list": { + "href": "http://localhost:8081/records" + }, + "first": { + "href": "http://localhost:8081/records/first" + } + } + }, + { + "id": 2, + "slug": "second", + "value": "Second value", + "_links": { + "list": { + "href": "http://localhost:8081/records" + }, + "first": { + "href": "http://localhost:8081/records/first" + } + } + } + ] + }, + "_links": { + "list": { + "href": "http://localhost:8081/records" + } + } +} +---- + +When we call a resource `/records/first` that returns only one instance, then the output is: + +[source,bash] +---- +& curl -H "Accept:application/hal+json" -i localhost:8080/records/first +{ + "id": 1, + "slug": "first", + "value": "First value", + "_links": { + "list": { + "href": "http://localhost:8081/records" + }, + "first": { + "href": "http://localhost:8081/records/first" + } + } +} +---- == Creating a frontend diff --git a/docs/src/main/asciidoc/scripting.adoc b/docs/src/main/asciidoc/scripting.adoc index 61d8d73b4062a..b552a19e47cc6 100644 --- a/docs/src/main/asciidoc/scripting.adoc +++ b/docs/src/main/asciidoc/scripting.adoc @@ -28,7 +28,8 @@ Normally we would link to a Git repository to clone but in this case there is no [source,java,subs=attributes+] ---- //usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS io.quarkus:quarkus-resteasy-reactive:{quarkus-version} +//DEPS io.quarkus.platform:quarkus-bom:{quarkus-version}@pom +//DEPS io.quarkus:quarkus-resteasy-reactive //JAVAC_OPTIONS -parameters //JAVA_OPTIONS -Djava.util.logging.manager=org.jboss.logmanager.LogManager @@ -105,20 +106,21 @@ You will find at the top a line looking like this: This line is what on Linux and macOS allows you to run it as a script. On Windows this line is ignored. -The next line +The next lines [source,java] ---- // //DEPS ---- -Is illustrating how you add dependencies to this script. This is a feature of JBang. +illustrate how you add dependencies to this script. This is a feature of JBang. -Go ahead and update this line to include the `quarkus-resteasy-reactive` dependency like so: +Go ahead and update this line to include the `quarkus-bom` and the `quarkus-resteasy-reactive` dependency like so: [source,java,subs=attributes+] ---- -//DEPS io.quarkus:quarkus-resteasy-reactive:{quarkus-version} +//DEPS io.quarkus.platform:quarkus-bom:{quarkus-version}@pom +//DEPS io.quarkus:quarkus-resteasy-reactive ---- Now, run `jbang quarkusapp.java` and you will see JBang resolving this dependency and building the jar with help from Quarkus' JBang integration. diff --git a/docs/src/main/asciidoc/security-authorization.adoc b/docs/src/main/asciidoc/security-authorization.adoc index 5f52e6f411eab..a33d797a1c68e 100644 --- a/docs/src/main/asciidoc/security-authorization.adoc +++ b/docs/src/main/asciidoc/security-authorization.adoc @@ -177,6 +177,35 @@ quarkus.http.auth.permission.permit1.methods=GET,HEAD and enabled at runtime with a system property or environment variable, for example: `-Dquarkus.http.auth.permission.permit1.enabled=true`. +== Permission paths and http root path + +The `quarkus.http.root-path` configuration property is used to change the xref:http-reference.adoc#context-path[http endpoint context path]. + +By default, `quarkus.http.root-path` is prepended automatically to configured permission paths then do not use a forward slash, for example: + +[source,properties] +---- +quarkus.http.auth.permission.permit1.paths=public/*,css/*,js/*,robots.txt +---- + +This configuration is equivalent to the following: + +[source,properties] +---- +quarkus.http.auth.permission.permit1.paths=${quarkus.http.root-path}/public/*,${quarkus.http.root-path}/css/*,${quarkus.http.root-path}/js/*,${quarkus.http.root-path}/robots.txt +---- + +A leading slash will change how the configured permission path is interpreted. The configured URL will be used as-is, and paths will not be adjusted if the value of `quarkus.http.root-path` is changed. For example: + +[source,properties] +---- +quarkus.http.auth.permission.permit1.paths=/public/*,css/*,js/*,robots.txt +---- + +This configuration will only impact resources served from the fixed/static URL `/public`, which may not match your application resources if `quarkus.http.root-path` has been set to something other than `/`. + +See link:https://quarkus.io/blog/path-resolution-in-quarkus/[Path Resolution in Quarkus] for more information. + [#standard-security-annotations] == Authorization using Annotations diff --git a/docs/src/main/asciidoc/security-customization.adoc b/docs/src/main/asciidoc/security-customization.adoc index 827de651640cc..f3f9a6e00d8ff 100644 --- a/docs/src/main/asciidoc/security-customization.adoc +++ b/docs/src/main/asciidoc/security-customization.adoc @@ -72,11 +72,11 @@ public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism == 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. +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. In such cases the mechanisms are asked to verify the credentials in turn until a `SecurityIdentity` is created. The mechanisms are sorted in the descending order using their priority. `Basic` authentication mechanism has the highest priority of `2000`, followed by the `Authorization Code` one with the priority of `1001`, with all other mechanisms provided by Quarkus having the priority of `1000`. -If no credentials are provided then the mechanism specific challenge is created, for example, `401` status is returned by either `Basic` or `JWT` mechanisms, URL redirecting the user to the `OpenId Connect` provider is returned by `quarkus-oidc`, etc. +If no credentials are provided then the mechanism specific challenge is created, for example, `401` status is returned by either `Basic` or `JWT` mechanisms, URL redirecting the user to the OpenID Connect provider is returned by `quarkus-oidc`, etc. So if `Basic` and `Authorization Code` mechanisms are combined then `401` will be returned if no credentials are provided and if `JWT` and `Authorization Code` mechanisms are combined then a redirect URL will be returned. @@ -484,6 +484,40 @@ keytool -genkey -alias server -keyalg RSA -keystore server-keystore.jks -keysize `BCFIPSJSSE` provider option is currently not supported in native image. ==== +[[sun-pkcs11]] +=== SunPKCS11 + +`SunPKCS11` provider provides a bridge to specific `PKCS#11` implementations such as cryptographic smartcards and other Hardware Security Modules, Network Security Services in FIPS mode, etc. + +Typically, in order to work with `SunPKCS11`, one needs to install a `PKCS#11` implementation, generate a configuration which usually refers to a shared library, token slot, etc and write the following Java code: + +[source,java] +---- +import java.security.Provider; +import java.security.Security; + +String configuration = "pkcs11.cfg" + +Provider sunPkcs11 = Security.getProvider("SunPKCS11"); +Provider pkcsImplementation = sunPkcs11.configure(configuration); +// or prepare configuration in the code or read it from the file such as "pkcs11.cfg" and do +// sunPkcs11.configure("--" + configuration); +Security.addProvider(pkcsImplementation); +---- + +In Quarkus you can achieve the same at the configuration level only without having to modify the code, for example: + +[source,properties] +---- +quarkus.security.security-providers=SunPKCS11 +quarkus.security.security-provider-config.SunPKCS11=pkcs11.cfg +---- + +[NOTE] +==== +Note that while accessing the `SunPKCS11` bridge provider is supported in native image, configuring `SunPKCS11` is currently not supported in native image at the Quarkus level. +==== + == Reactive Security If you are going to use security in a reactive environment, you will likely need SmallRye Context Propagation: diff --git a/docs/src/main/asciidoc/security-jwt.adoc b/docs/src/main/asciidoc/security-jwt.adoc index f9aaa5efca0f9..c6e716ac4ff54 100644 --- a/docs/src/main/asciidoc/security-jwt.adoc +++ b/docs/src/main/asciidoc/security-jwt.adoc @@ -318,7 +318,7 @@ nQIDAQAB Often one obtains a JWT from an identity manager like https://www.keycloak.org/[Keycloak], but for this quickstart we will generate our own using the JWT generation API provided by `smallrye-jwt` (see xref:smallrye-jwt-build.adoc[Generate JWT tokens with SmallRye JWT] for more information). -Take the code from the following listing and place into `security-jwt-quickstart/src/main/java/org/acme/security/jwt/GenerateToken.java`: +Take the code from the following listing and place into `security-jwt-quickstart/src/test/java/org/acme/security/jwt/GenerateToken.java`: .GenerateToken main Driver Class [source, java] @@ -805,7 +805,7 @@ mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus [[integration-testing-keycloak]] ==== Keycloak -If you work with Keycloak and configure `mp.jwt.verify.publickey.location` to point to HTTPS or HTTP based JsonWebKey (JWK) set then you can use the same approach as described in the xref:security-openid-connect.adoc#integration-testing-keycloak[OpenID Connect Bearer Token Integration testing] `Keycloak` section but only change the `application.properties` to use MP JWT configuration properties instead: +If you work with Keycloak and configure `mp.jwt.verify.publickey.location` to point to HTTPS or HTTP based JsonWebKey (JWK) set then you can use the same approach as described in the xref:security-openid-connect.adoc#integration-testing-keycloak[OpenID Connect Bearer Token Integration testing] Keycloak section but only change the `application.properties` to use MP JWT configuration properties instead: [source, properties] ---- @@ -1016,6 +1016,11 @@ SmallRye JWT provides more properties which can be used to customize the token p |smallrye.jwt.decrypt.key.location|none|Config property allows for an external or internal location of Private Decryption Key to be specified. This property is deprecated - use 'mp.jwt.decrypt.key.location'. |smallrye.jwt.decrypt.algorithm|`RSA_OAEP`|Decryption algorithm. |smallrye.jwt.token.decryption.kid|none|Decryption Key identifier. If it is set then the decryption JWK key as well every JWT token must have a matching `kid` header. +|smallrye.jwt.client.tls.certificate.path|none|Path to TLS trusted certificate which may need to be configured if the keys have to be fetched over `HTTPS`. +|smallrye.jwt.client.tls.trust-all|false|Trust all the hostnames. If the keys have to be fetched over `HTTPS` and this property is set to `true` then all the hostnames are trusted by default. +|smallrye.jwt.client.tls.trusted.hosts|none|Set of trusted hostnames. If the keys have to be fetched over `HTTPS` and `smallrye.jwt.client.tls.trust-all` is set to `false` then this property can be used to configure the trusted hostnames. +|smallrye.jwt.http.proxy.host|none|HTTP proxy host. +|smallrye.jwt.http.proxy.port|80|HTTP proxy port. |=== == References diff --git a/docs/src/main/asciidoc/security-keycloak-authorization.adoc b/docs/src/main/asciidoc/security-keycloak-authorization.adoc index 436ff96cb5af2..3dc5b523a71ed 100644 --- a/docs/src/main/asciidoc/security-keycloak-authorization.adoc +++ b/docs/src/main/asciidoc/security-keycloak-authorization.adoc @@ -368,6 +368,8 @@ public class ProtectedResource { } ---- +Note: If you want to use the `AuthzClient` directly make sure to to set `quarkus.keycloak.policy-enforcer.enable=true` otherwise there is no Bean available for injection. + == Mapping Protected Resources By default, the extension is going to fetch resources on-demand from Keycloak where their `URI` are used to map the resources in your application that should be protected. diff --git a/docs/src/main/asciidoc/security-openid-connect-client.adoc b/docs/src/main/asciidoc/security-openid-connect-client.adoc index 715691bda0fd2..027c5800c5e23 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client.adoc @@ -771,7 +771,7 @@ and finally write the test code. Given the Wiremock-based resource above, the fi ==== Keycloak -If you work with Keycloak then you can use the same approach as described in the xref:security-openid-connect#integration-testing-keycloak.adoc[OpenID Connect Bearer Token Integration testing] `Keycloak` section. +If you work with Keycloak then you can use the same approach as described in the xref:security-openid-connect#integration-testing-keycloak.adoc[OpenID Connect Bearer Token Integration testing] Keycloak section. === How to check the errors in the logs 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 2054d2b56c9d9..4c69a520e72f0 100644 --- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc @@ -52,7 +52,7 @@ include::includes/devtools/dev.adoc[] Note that you can disable sharing the containers with `quarkus.keycloak.devservices.shared=false`. -Now open the main link:http://localhost:8080/q/dev[Dev UI page] and you will see the `OpenID Connect Card` linking to a `Keycloak` page: +Now open the main link:http://localhost:8080/q/dev[Dev UI page] and you will see the `OpenID Connect Card` linking to a Keycloak page: image::dev-ui-oidc-keycloak-card.png[alt=Dev UI OpenID Connect Card,role="center"] diff --git a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc index aa3d44efdd8f9..bb2e23afd8a6a 100644 --- a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc @@ -386,6 +386,7 @@ Now all methods and classes carrying `@HrTenant` will be authenticated using the `quarkus.oidc.hr.auth-server-url`, while all other classes and methods will still be authenticated using the default OIDC provider. +[[tenant-config-resolver]] == Programmatically Resolving Tenants Configuration If you need a more dynamic configuration for the different tenants you want to support and don't want to end up with multiple diff --git a/docs/src/main/asciidoc/security-openid-connect-providers.adoc b/docs/src/main/asciidoc/security-openid-connect-providers.adoc new file mode 100644 index 0000000000000..2e288bfa4b313 --- /dev/null +++ b/docs/src/main/asciidoc/security-openid-connect-providers.adoc @@ -0,0 +1,366 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Configuring Well-Known OpenID Connect Providers + +include::./attributes.adoc[] + +== Introduction + +If you use xref:security-openid-connect-web-authentication.adoc[OpenID Connect Authorization Code Flow] to protect Quarkus endpoints then you need to configure Quarkus to tell it how to connect to OpenID Connect providers, how to authenticate to such providers, which scopes to use, etc. + +Sometimes you need to use the configuration to workaround the fact that some providers do not implement OpenID Connect completely or when they are in fact xref:security-openid-connect-web-authentication.adoc#oauth2[OAuth2 providers only]. + +The configuration of such providers can become complex, very technical and difficult to understand. + +`quarkus.oidc.provider` configuration property has been introduced to refer to well-known OpenID Connect and OAuth2 providers. This property can be used to refer to a provider such as `github` with only a minimum number of customizations required, typically, an account specific `client id`, `client secret` and some properties have to be set to complete the configuration. + +This property can be used in `application.properties`, in xref:security-openid-connect-multitenancy.adoc[multi-tenant] set-ups if more than one provider has to be configured (for example, see https://quarkiverse.github.io/quarkiverse-docs/quarkus-renarde/dev/security.html#_using_oidc_for_login[Quarkus Renarde security documentation]), in custom xref:security-openid-connect-web-multitenancy.adoc#tenant-config-resolver[TenantConfigResolvers] if the tenant configurations are created dynamically. + +== Well Known Providers + +=== GitHub + +In order to set up OIDC for GitHub you need to create a new OAuth application in your https://github.com/settings/applications/new[GitHub developer settings]: + +image::oidc-github-1.png[role="thumb"] + +Make sure to fill in the appropriate details, but more importantly the Authorization Callback URL, set to `http://localhost:8080/_renarde/security/github-success` +(if you intend to test this using the Quarkus DEV mode). + +Now click on `Register application` and you'll be shown your application page: + +image::oidc-github-2.png[role="thumb"] + +You need to click on `Generate a new client secret`, confirm your credentials, and write down +your Client ID and Client secret (especially that one, because you will not be able to see it again +later from that page, but you can always recreate one, do don't worry too much): + +image::oidc-github-3.png[role="thumb"] + +Now add the following configuration to your `application.properties`: + +[source,properties] +---- +quarkus.oidc.provider=github +quarkus.oidc.client-id= +quarkus.oidc.credentials.secret= +---- + +=== 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]: + +Pick a project name and click on `CREATE`. + +image::oidc-google-1.png[role="thumb"] + +Now make sure you select your project in the top selector, and click on the left-hand bar menu on `APIs and Services > OAuth consent screen`: + +image::oidc-google-2.png[role="thumb"] + +Select `External` to authorise any Google user to log in to your application and press `CREATE`: + +image::oidc-google-3.png[role="thumb"] + +Now you can fill in your application name, your support email, your developer contact information and press `SAVE AND CONTINUE`: + +image::oidc-google-4.png[role="thumb"] + +Do not add any scopes on the next page, and press `SAVE AND CONTINUE`: + +image::oidc-google-5.png[role="thumb"] + +Do not add any test user on the next page, and press `SAVE AND CONTINUE`: + +image::oidc-google-6.png[role="thumb"] + +Click on the top menu `CREATE CREDENTIALS` > `OAuth client ID`: + +image::oidc-google-7.png[role="thumb"] + +Select `Web application` as `Application type`, and add `http://localhost:8080/_renarde/security/oidc-success` in +the `Authorised redirect URIs` list, then press `CREATE`: + +image::oidc-google-8.png[role="thumb"] + +Copy your Client ID and Client Secret: + +image::oidc-google-9.png[role="thumb"] + +You can now configure your `application.properties`: + +[source,properties] +---- +quarkus.oidc.provider=google +quarkus.oidc.client-id= +quarkus.oidc.credentials.secret= +---- + +=== Microsoft + +In order to set up OIDC for Microsoft you need to go to your https://portal.azure.com[Microsoft Azure Portal], +and search for `Azure Active Directory`, then click on it: + +image::oidc-microsoft-1.png[role="thumb"] + +Once there, on the left side under `Manage`, click on `App registrations` then click on `New registration`: + +image::oidc-microsoft-2.png[role="thumb"] + +Fill in your application name, select `Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)` to allow anyone to log in, and add a `Web` Redirect URI as `http://localhost:8080/_renarde/security/oidc-success`, +then click on `Register`: + +image::oidc-microsoft-3.png[role="thumb"] + +On that resulting page, copy the `Client Id` (under `Application (client) ID`, then click on `Add a certificate or secret`: + +image::oidc-microsoft-4.png[role="thumb"] + +Now, under `Client secrets (0)`, click on `New client secret`: + +image::oidc-microsoft-5.png[role="thumb"] + +Click on `Add` in that dialog without changing anything: + +image::oidc-microsoft-6.png[role="thumb"] + +On the resulting page, copy your `Secret ID`: + +image::oidc-microsoft-7.png[role="thumb"] + +You can now configure your `application.properties`: + +[source,properties] +---- +quarkus.oidc.provider=microsoft +quarkus.oidc.client-id= +quarkus.oidc.credentials.secret= +---- + +=== Apple + +In order to set up OIDC for Apple you need to create a developer account, and sign up for the 99€/year program, but you cannot test your application on `localhost` like most other OIDC providers: +you will need to run it over `https` and make it publicly accessible, so for development purposes +you may want to use a service such as https://ngrok.com. + +Go to https://developer.apple.com/account/resources/identifiers/list[Create a new identifier] and press `+` + +image::oidc-apple-1.png[role="thumb"] + +Dont touch anything, keep `App IDs` selected, and press `Continue`: + +image::oidc-apple-2.png[role="thumb"] + +Dont touch anything, keep `App` selected, and press `Continue`: + +image::oidc-apple-3.png[role="thumb"] + +Enter a description and a Bundle ID (use your application package name): + +image::oidc-apple-4.png[role="thumb"] + +Then scroll down to find the `Sign in with Apple` Capability, select it, and press `Continue`: + +image::oidc-apple-5.png[role="thumb"] + +Write down your App ID Prefix, then press `Register`: + +image::oidc-apple-6.png[role="thumb"] + +Back on the `Identifiers` page, press `+`: + +image::oidc-apple-7.png[role="thumb"] + +Select `Service IDs` and press `Continue`: + +image::oidc-apple-8.png[role="thumb"] + +Enter a description and Bundle ID (use your application package name), then press `Continue`: + +image::oidc-apple-9.png[role="thumb"] + +Now press `Register`: + +image::oidc-apple-10.png[role="thumb"] + +Back on the service list, click on your newly created service: + +image::oidc-apple-11.png[role="thumb"] + +Enable `Sign in with Apple` and press `Configure`: + +image::oidc-apple-12.png[role="thumb"] + +Add your domain and return URL (set to `/_renarde/security/oidc-success`) and press `Next`: + +image::oidc-apple-13.png[role="thumb"] + +Now press `Done`: + +image::oidc-apple-14.png[role="thumb"] + +Now press `Continue`: + +image::oidc-apple-15.png[role="thumb"] + +And now press `Save`: + +image::oidc-apple-16.png[role="thumb"] + +Go to the https://developer.apple.com/account/resources/authkeys/list[Keys] page on the left menu, and press `+`: + +image::oidc-apple-17.png[role="thumb"] + +Fill in a key name, enable `Sign in with Apple`, and press `Configure`: + +image::oidc-apple-18.png[role="thumb"] + +Select your Primary App ID and press `Save`: + +image::oidc-apple-19.png[role="thumb"] + +Back on the key page, press `Continue`: + +image::oidc-apple-20.png[role="thumb"] + +Now press `Register`: + +image::oidc-apple-21.png[role="thumb"] + +Write down your `Key ID`, download your key and save it to your Quarkus application in `src/main/resources/AuthKey_.p8`: + +image::oidc-apple-22.png[role="thumb"] + +You can now configure your `application.properties`: + +[source,properties] +---- +quarkus.oidc.provider=apple +quarkus.oidc.client-id= +quarkus.oidc.credentials.jwt.key-file=AuthKey_.p8 +quarkus.oidc.credentials.jwt.token-key-id= +quarkus.oidc.credentials.jwt.issuer= +quarkus.oidc.credentials.jwt.subject= Settings` on the left menu: + +image::oidc-facebook-4.png[role="thumb"] + +Enter your `Redirect URIs` (set to `/_renarde/security/oidc-success`) and press `Save changes`: + +image::oidc-facebook-5.png[role="thumb"] + +Now go to `Settings > Basic` on the left hand menu, and write down your `App ID` and `App secret`: + +image::oidc-facebook-6.png[role="thumb"] + +You can now configure your `application.properties`: + +[source,properties] +---- +quarkus.oidc.provider=facebook +quarkus.oidc.client-id= +quarkus.oidc.credentials.secret= +---- + +=== Twitter + +You can use Twitter for OIDC login, but at the moment, it restricts access to the user's email, which means you +will have to obtain it and verify it yourself. + +In order to set up OIDC for Twitter start by https://developer.twitter.com/en/portal/projects/new[Creating a project], enter a project name, and press `Next`: + +image::oidc-twitter-1.png[role="thumb"] + +Enter a use case, and press `Next`: + +image::oidc-twitter-2.png[role="thumb"] + +Enter a project description, and press `Next`: + +image::oidc-twitter-3.png[role="thumb"] + +Now enter an application name, and press `Next`: + +image::oidc-twitter-4.png[role="thumb"] + +Write down your keys, because they will not be displayed again, and press `App Settings`: + +image::oidc-twitter-5.png[role="thumb"] + +Navigate down to the `User authentication settings` section and press `Set up`: + +image::oidc-twitter-6.png[role="thumb"] + +Check the `OAuth 2.0` check box: + +image::oidc-twitter-7.png[role="thumb"] + +Select `Web App` as application type, then fill in your application details (use `/_renarde/security/twitter-success` +for the `Callback URI`). + +NOTE: Twitter doesn't require https usage in practice, but won't accept your `Website URL` without it, so +you can still use ngrok for it. + +Now press `Save`: + +image::oidc-twitter-8.png[role="thumb"] + +You can now copy your `Client ID` and `Client Secret` and press `Done`: + +image::oidc-twitter-9.png[role="thumb"] + +You can now configure your `application.properties`: + +[source,properties] +---- +quarkus.oidc.provider=twitter +quarkus.oidc.client-id= +quarkus.oidc.credentials.secret= +---- + +=== Spotify + +Create a https://developer.spotify.com/documentation/general/guides/authorization/app-settings/[Spotify application]: + +image::oidc-spotify-1.png[role="thumb"] + +Don't forget to add `http://localhost:8080` as a redirect URI for testing during development purposes. You should get a client id and secret generated once a Spotify application setup has been complete, for example: + +image::oidc-spotify-2.png[role="thumb"] + +You can now configure your `application.properties`: + +[source,properties] +---- +quarkus.oidc.provider=spotify +quarkus.oidc.client-id= +quarkus.oidc.credentials.secret= +---- + +== References + +* xref:security-openid-connect-web-authentication.adoc[Using OpenID Connect to Protect Web Applications] +* xref:security.adoc[Quarkus Security] 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 e0c2024c42a3f..19f968c41e2c1 100644 --- a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc @@ -232,7 +232,7 @@ After getting a cup of coffee, you'll be able to run this binary directly: To test the application, you should open your browser and access the following URL: -* http://localhost:8080[http://localhost:8080] +* http://localhost:8080/tokens[http://localhost:8080/tokens] If everything is working as expected, you should be redirected to the Keycloak server to authenticate. @@ -471,7 +471,7 @@ quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id} [[back-channel-logout]] ==== Back-Channel Logout -link:https://openid.net/specs/openid-connect-backchannel-1_0.html[Back-Channel Logout] is used by OpenId Connect providers to logout the current user from all the applications this user is currently logged in, bypassing the user agent. +link:https://openid.net/specs/openid-connect-backchannel-1_0.html[Back-Channel Logout] is used by OpenID Connect providers to logout the current user from all the applications this user is currently logged in, bypassing the user agent. You can configure Quarkus to support `Back-Channel Logout` as follows: @@ -485,7 +485,7 @@ quarkus.oidc.application-type=web-app quarkus.oidc.logout.backchannel.path=/back-channel-logout ---- -Absolute `Back-Channel Logout` URL is calculated by adding `quarkus.oidc.back-channel-logout.path` to the current endpoint URL, for example, `http://localhost:8080/back-channel-logout`. You will need to configure this URL in the Admin Console of your OpenId Connect Provider. +Absolute `Back-Channel Logout` URL is calculated by adding `quarkus.oidc.back-channel-logout.path` to the current endpoint URL, for example, `http://localhost:8080/back-channel-logout`. You will need to configure this URL in the Admin Console of your OpenID Connect Provider. [[local-logout]] ==== Local Logout @@ -627,7 +627,7 @@ public class CustomTokenStateManager implements TokenStateManager { link:https://datatracker.ietf.org/doc/html/rfc7636[Proof Of Key for Code Exchange] (PKCE) minimizes the risk of the authorization code interception. -While `PKCE` is of primary importance to the public OpenId Connect clients (such as the SPA scripts running in a browser), it can also provide an extra level of protection to Quarkus OIDC `web-app` applications which are confidential OpenId Connect clients capable of securely storing the client secret and using it to exchange the code for the tokens. +While `PKCE` is of primary importance to the public OpenID Connect clients (such as the SPA scripts running in a browser), it can also provide an extra level of protection to Quarkus OIDC `web-app` applications which are confidential OpenID Connect clients capable of securely storing the client secret and using it to exchange the code for the tokens. If can enable `PKCE` for your OIDC `web-app` endpoint with a `quarkus.oidc.authentication.pkce-required` property and a 32 characters long secret, for example: @@ -639,7 +639,7 @@ 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. -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. +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. === Listening to important authentication events @@ -693,35 +693,36 @@ Future callQuarkusService() async { If you plan to consume this application from a Single Page Application running on a different domain, you will need to configure CORS (Cross-Origin Resource Sharing). Please read the xref:http-reference.adoc#cors-filter[HTTP CORS documentation] for more details. +[[oauth2]] === Integration with GitHub and other OAuth2 providers -Some well known providers such as `GitHub` or `LinkedIn` are not `OpenID Connect` but `OAuth2` providers which support the `authorization code flow`, for example, link:https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps[GitHub OAuth2] and link:https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow[LinkedIn OAuth2]. +Some well known providers such as GitHub or LinkedIn are not OpenID Connect but OAuth2 providers which support the `authorization code flow`, for example, link:https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps[GitHub OAuth2] and link:https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow[LinkedIn OAuth2]. -The main difference between `OpenID Connect` and `OAuth2` providers is that `OpenID Connect` providers, by building on top of `OAuth2`, return an `ID Token` representing a user authentication, in addition to the standard authorization code flow `access` and `refresh` tokens returned by `OAuth2` providers. +The main difference between OpenID Connect and OAuth2 providers is that OpenID Connect providers, by building on top of OAuth2, return an `ID Token` representing a user authentication, in addition to the standard authorization code flow `access` and `refresh` tokens returned by `OAuth2` providers. -`OAuth2` providers such as `GitHub` do not return `IdToken`, the fact of the user authentication is implicit and is indirectly represented by the `access` token which represents an authenticated user authorizing the current Quarkus `web-app` application to access some data on behalf of the authenticated user. +OAuth2 providers such as GitHub do not return `IdToken`, the fact of the user authentication is implicit and is indirectly represented by the `access` token which represents an authenticated user authorizing the current Quarkus `web-app` application to access some data on behalf of the authenticated user. -For example, when working with `GitHub`, the Quarkus endpoint can acquire an `access` token which will allow it to request a `GitHub` profile of the current user. -In fact this is exactly how a standard `OpenID Connect` `UserInfo` acqusition also works - by authenticating into your `OpenID Connect` provider you also give a permission to Quarkus application to acquire your <> on your behalf - and it also shows what is meant by `OpenID Connect` being built on top of `OAuth2`. +For example, when working with GitHub, the Quarkus endpoint can acquire an `access` token which will allow it to request a GitHub profile of the current user. +In fact this is exactly how a standard OpenID Connect `UserInfo` acquisition also works - by authenticating into your OpenID Connect provider you also give a permission to Quarkus application to acquire your <> on your behalf - and it also shows what is meant by OpenID Connect being built on top of OAuth2. -In order to support the integration with such `OAuth2` servers, `quarkus-oidc` needs to be configured to allow the authorization code flow responses without `IdToken`: `quarkus.oidc.authentication.id-token-required=false`. +In order to support the integration with such OAuth2 servers, `quarkus-oidc` needs to be configured to allow the authorization code flow responses without `IdToken`: `quarkus.oidc.authentication.id-token-required=false`. It is required because `quarkus-oidc` expects that not only `access` and `refresh` tokens but also `IdToken` will be returned once the authorization code flow completes. -Note, even though you will configure the extension to support the authorization code flows without `IdToken`, an internal `IdToken` will be generated to support the way `quarkus-oidc` operates where an `IdToken` is used to support the authentication session and to avoid redirecting the user to the provider such as `GitHub` on every request. In this case the session lifespan is set to 5 minutes which can be extended further as described in the <> section. +Note, even though you will configure the extension to support the authorization code flows without `IdToken`, an internal `IdToken` will be generated to support the way `quarkus-oidc` operates where an `IdToken` is used to support the authentication session and to avoid redirecting the user to the provider such as GitHub on every request. In this case the session lifespan is set to 5 minutes which can be extended further as described in the <> section. The next step is to ensure that the returned access token can be useful to the current Quarkus endpoint. -If the `OAuth2` provider supports the introspection endpoint then you may be able to use this access token as a source of roles with `quarkus.oidc.roles.source=accesstoken`. If no introspection endpoint is available then at the very least it should be possible to request <> from this provider with `quarkus.oidc.authentication.user-info-required` - this is the case with `GitHib`. +If the OAuth2 provider supports the introspection endpoint then you may be able to use this access token as a source of roles with `quarkus.oidc.roles.source=accesstoken`. If no introspection endpoint is available then at the very least it should be possible to request <> from this provider with `quarkus.oidc.authentication.user-info-required` - this is the case with GitHub. -Configuring the endpoint to request <> is the only way `quarkus-oidc` can be integrated with the providers such as `GitHib`. +Configuring the endpoint to request <> is the only way `quarkus-oidc` can be integrated with the providers such as GitHub. Note that requiring <> involves making a remote call on every request - therefore you may want to consider caching `UserInfo` data, see < for more details. -Alternatively, you may want to request that `UserInfo` is embedded into the internal generated `IdToken`with the `quarkus.oidc.cache-user-info-in-idtoken=true` property - the advantage of this approach is that by default no cached `UserInfo` state will be kept with the endpoint - instead it will be stored in a session cookie. You may also want to consider encrypting `IdToken` in this case if `UserInfo` contains sensitive data, please see <> for more information. +Alternatively, you may want to request that `UserInfo` is embedded into the internal generated `IdToken` with the `quarkus.oidc.cache-user-info-in-idtoken=true` property - the advantage of this approach is that by default no cached `UserInfo` state will be kept with the endpoint - instead it will be stored in a session cookie. You may also want to consider encrypting `IdToken` in this case if `UserInfo` contains sensitive data, please see <> for more information. Also, OAuth2 servers may not support a well-known configuration endpoint in which case the discovery has to be disabled and the authorization, token, and introspection and/or userinfo endpoint paths have to be configured manually. -Here is how you can integrate `quarkus-oidc` with `GitHub` after you have link:https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app[created a GitHub OAuth application]. Configure your Quarkus endpoint like this: +Here is how you can integrate `quarkus-oidc` with GitHub after you have link:https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app[created a GitHub OAuth application]. Configure your Quarkus endpoint like this: [source,properties] ---- @@ -740,6 +741,8 @@ quarkus.oidc.credentials.secret=github_app_clientsecret # quarkus.oidc.cache-user-info-in-idtoken=true ---- +See xref:security-openid-connect-providers.adoc[Well Known OpenID Connect providers] for more details about configuring other well-known providers. + This is all what is needed for an endpoint like this one to return the currently authenticated user's profile with `GET http://localhost:8080/github/userinfo` and access it as the individual `UserInfo` properties: [source,java] @@ -768,7 +771,7 @@ public class TokenResource { } ---- -If you support more than one social provider with the help of xref:security-openid-connect-multitenancy.adoc[OpenID Connect Multi-Tenancy], for example, `Google` which is an OpenID Connect Provider returning `IdToken` and `GitHub` which is an `OAuth2` provider returning no `IdToken` and only allowing to access `UserInfo` then you can have your endpoint working with only the injected `SecurityIdentity` for both `Google` and `GitHub` flows. A simple augmentation of `SecurityIdentity` will be required where a principal created with the internally generated `IdToken` will be replaced with the `UserInfo` based principal when the GiHub flow is active: +If you support more than one social provider with the help of xref:security-openid-connect-multitenancy.adoc[OpenID Connect Multi-Tenancy], for example, Google which is an OpenID Connect Provider returning `IdToken` and GitHub which is an OAuth2 provider returning no `IdToken` and only allowing to access `UserInfo` then you can have your endpoint working with only the injected `SecurityIdentity` for both Google and GitHub flows. A simple augmentation of `SecurityIdentity` will be required where a principal created with the internally generated `IdToken` will be replaced with the `UserInfo` based principal when the GiHub flow is active: [source,java] ---- @@ -811,7 +814,7 @@ public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmento } ---- -Now, the following code will work when the user is signing in into your application with both `Google` or `GitHub`: +Now, the following code will work when the user is signing in into your application with both Google or GitHub: [source,java] ---- @@ -1011,7 +1014,7 @@ Using `client_secret_jwt` or `private_key_jwt` authentication methods ensures th ==== Additional JWT Authentication options -If `client_secret_jwt`, `private_key_jwt` authentication methods are used or an `Apple` `post_jwt` method is used then the JWT signature algorithm, key identifier, audience, subject and issuer can be customized, for example: +If `client_secret_jwt`, `private_key_jwt` authentication methods are used or an Apple `post_jwt` method is used then the JWT signature algorithm, key identifier, audience, subject and issuer can be customized, for example: [source,properties] ---- @@ -1363,7 +1366,7 @@ quarkus.oidc.authentication.extra-params.response_mode=query === Customize authentication error response -If the user authentication has failed at the OpenId Connect Authorization endpoint, for example, due to an invalid scope or other invalid parameters included in the redirect to the provider, then the provider will redirect the user back to Quarkus not with the `code` but `error` and `error_description` parameters. +If the user authentication has failed at the OpenID Connect Authorization endpoint, for example, due to an invalid scope or other invalid parameters included in the redirect to the provider, then the provider will redirect the user back to Quarkus not with the `code` but `error` and `error_description` parameters. In such cases HTTP `401` will be returned by default. However, you can instead request that a custom public error endpoint is called in order to return a user friendly HTML error page. Use `quarkus.oidc.authentication.error-path`, for example: @@ -1385,6 +1388,7 @@ include::{generated-dir}/config/quarkus-oidc.adoc[opts=optional] * https://www.keycloak.org/documentation.html[Keycloak Documentation] * https://openid.net/connect/[OpenID Connect] * https://tools.ietf.org/html/rfc7519[JSON Web Token] +* xref:security-openid-connect-providers.adoc[Well Known OpenID Connect providers]. * xref:security-openid-connect-client.adoc[Quarkus - Using OpenID Connect and OAuth2 Client and Filters to manage access tokens] * xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak] * xref:security.adoc#oidc-jwt-oauth2-comparison[Summary of Quarkus OIDC, JWT and OAuth2 features] diff --git a/docs/src/main/asciidoc/security-openid-connect.adoc b/docs/src/main/asciidoc/security-openid-connect.adoc index 987bf7c897be8..7c3c2861341b3 100644 --- a/docs/src/main/asciidoc/security-openid-connect.adoc +++ b/docs/src/main/asciidoc/security-openid-connect.adoc @@ -167,7 +167,7 @@ Example configuration: ---- %prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus quarkus.oidc.client-id=backend-service -quarkus.oidc.client-secret=secret +quarkus.oidc.credentials.secret=secret # Tell Dev Services for Keycloak to import the realm file # This property is not effective when running the application in JVM or Native modes @@ -179,7 +179,7 @@ NOTE: Adding a `%prod.` profile prefix to `quarkus.oidc.auth-server-url` ensures === Starting and Configuring the Keycloak Server -NOTE: Do not start the Keycloak server when you run the application in a dev mode - `Dev Services for Keycloak` will launch a container. See <> section below for more information. +NOTE: Do not start the Keycloak server when you run the application in a dev mode - `Dev Services for Keycloak` will launch a container. See <> section below for more information. Make sure to put the {quickstarts-tree-url}/security-openid-connect-quickstart/config/quarkus-realm.json[realm configuration file] on the classpath (`target/classes` directory) so that it gets imported automatically when running in dev mode - unless you have already built a {quickstarts-tree-url}/security-openid-connect-quickstart[complete solution] in which case this realm file will be added to the classpath during the build. To start a Keycloak Server you can use Docker and just run the following command: @@ -251,7 +251,7 @@ After getting a cup of coffee, you'll be able to run this binary directly: [source,bash] ---- -./target/security-openid-connect-quickstart-runner +./target/security-openid-connect-quickstart-1.0.0-SNAPSHOT-runner ---- === Testing the Application @@ -302,7 +302,7 @@ In order to access the admin endpoint you should obtain a token for the `admin` [source,bash] ---- export access_token=$(\ - curl --insecure -X POST https://localhost:8543/realms/quarkus/protocol/openid-connect/token \ + curl --insecure -X POST http://localhost:8180/realms/quarkus/protocol/openid-connect/token \ --user backend-service:secret \ -H 'content-type: application/x-www-form-urlencoded' \ -d 'username=admin&password=admin&grant_type=password' | jq --raw-output '.access_token' \ @@ -377,7 +377,7 @@ If UserInfo is the source of the roles then set `quarkus.oidc.authentication.use Additionally a custom `SecurityIdentityAugmentor` can also be used to add the roles as documented xref:security.adoc#security-identity-customization[here]. [[token-verification-introspection]] -=== Token Verification And Introspection +=== Token Verification And Introspection If the token is a JWT token then, by default, it will be verified with a `JsonWebKey` (JWK) key from a local `JsonWebKeySet` retrieved from the OpenID Connect Provider's JWK endpoint. The token's key identifier `kid` header value will be used to find the matching JWK key. If no matching `JWK` is available locally then `JsonWebKeySet` will be refreshed by fetching the current key set from the JWK endpoint. The `JsonWebKeySet` refresh can be repeated again only after the `quarkus.oidc.token.forced-jwk-refresh-interval` (default is 10 minutes) expires. @@ -637,10 +637,8 @@ and finally write the test code, for example: ---- import static org.hamcrest.Matchers.equalTo; -import java.util.Arrays; -import java.util.HashSet; +import java.util.Set; -import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import io.quarkus.test.common.QuarkusTestResource; @@ -655,8 +653,8 @@ public class BearerTokenAuthorizationTest { @Test public void testBearerToken() { - RestAssured.given().auth().oauth2(getAccessToken("alice", new HashSet<>(Arrays.asList("user")))) - .when().get("/api/users/preferredUserName") + RestAssured.given().auth().oauth2(getAccessToken("alice", Set.of("user"))) + .when().get("/api/users/me") .then() .statusCode(200) // the test endpoint returns the name extracted from the injected SecurityIdentity Principal @@ -681,6 +679,11 @@ Testing your `quarkus-oidc` `service` application with `OidcWiremockTestResource If there is an immediate need for a test to define Wiremock stubs not currently supported by `OidcWiremockTestResource` one can do so via a `WireMockServer` instance injected into the test class, for example: +[NOTE] +==== +`OidcWiremockTestResource` does not work with `@QuarkusIntegrationTest` against Docker containers, because the Wiremock server is running in the JVM running the test, which cannot be accessed from the Docker container running the Quarkus application. +==== + [source, java] ---- package io.quarkus.it.keycloak; @@ -693,14 +696,11 @@ import org.junit.jupiter.api.Test; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; -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; @QuarkusTest -@QuarkusTestResource(OidcWiremockTestResource.class) public class CustomOidcWireMockStubTest { @OidcWireMock @@ -708,7 +708,7 @@ public class CustomOidcWireMockStubTest { @Test public void testInvalidBearerToken() { - wireMockServer.stubFor(WireMock.post("/realms/quarkus/protocol/openid-connect/token/introspect") + wireMockServer.stubFor(WireMock.post("/auth/realms/quarkus/protocol/openid-connect/token/introspect") .withRequestBody(matching(".*token=invalid_token.*")) .willReturn(WireMock.aResponse().withStatus(400))); @@ -1029,7 +1029,7 @@ 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()`. -`@OidcSecurity` annotation is optional and can be used to set the additional token claims, as well as `UserInfo` and `OidcConfigurationMetadata` properties. +`@OidcSecurity` annotation is optional and can be used to set the additional token claims, as well as `UserInfo` and `OidcConfigurationMetadata` properties. Additionally, if `quarkus.oidc.token.issuer` property is configured then it will be used as an `OidcConfigurationMetadata` `issuer` property value. If you work with the opaque tokens then you can test them as follows: diff --git a/docs/src/main/asciidoc/security-webauthn.adoc b/docs/src/main/asciidoc/security-webauthn.adoc index f338f2771fb41..a284bc50177dd 100644 --- a/docs/src/main/asciidoc/security-webauthn.adoc +++ b/docs/src/main/asciidoc/security-webauthn.adoc @@ -888,7 +888,7 @@ webAuthn.registerOnly({ name: userName, displayName: firstName + " " + lastName }); ---- -=== Only invoke the registration challenge and authenticator +=== Only invoke the login challenge and authenticator The `webAuthn.loginOnly` method invokes the login challenge endpoint, then calls the authenticator and returns a https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise[Promise object] containing a @@ -897,7 +897,7 @@ in hidden form `input` elements, for example, and send it as part of a regular H [source,javascript] ---- -webAuthn.login({ name: userName }) +webAuthn.loginOnly({ name: userName }) .then(body => { // store the login JSON in form elements document.getElementById('webAuthnId').value = body.id; diff --git a/docs/src/main/asciidoc/smallrye-graphql.adoc b/docs/src/main/asciidoc/smallrye-graphql.adoc index da575fcde8d4b..eb566d1e7e8b9 100644 --- a/docs/src/main/asciidoc/smallrye-graphql.adoc +++ b/docs/src/main/asciidoc/smallrye-graphql.adoc @@ -454,9 +454,9 @@ the heroes: <1> Here receive the films as a batch, allowing you to fetch the corresponding heroes. -=== Reactive +=== Non blocking -Queries can be made reactive by using `Uni`, or `CompletionStage` as a return type, for example: +Queries can be made reactive by using `Uni` as a return type, or adding `@NonBlocking` to the method: [source,java] ---- @@ -467,41 +467,39 @@ Queries can be made reactive by using `Uni`, or `CompletionStage` as a return ty } ---- -NOTE: Due to the underlying library, graphql-java, `Uni` is creating a `CompletionStage` under the hood. -Or you can use `CompletionStage`: +Or you can use `@NonBlocking`: [source,java] ---- @Query @Description("Get a Films from a galaxy far far away") - public CompletionStage getFilm(int filmId) { + @NonBlocking + public Film getFilm(int filmId) { // ... } ---- -Using `Uni` or `CompletionStage` means that when a request contains more than one query, they will be executed concurrently. +Using `Uni` or `@NonBlocking` means that the request will be executed on Event-loop threads rather than Worker threads. -For instance, the query below will fetch `film0` and `film1` concurrently: +You can mix Blocking and Non-blocking in one request, -[source, graphql] +[source, java] ---- -query getFilms { - film0: film(filmId: 0) { - title - director - releaseDate - episodeID - } - film1: film(filmId: 1) { - title - director - releaseDate - episodeID - } -} + @Query + @Description("Get a Films from a galaxy far far away") + @NonBlocking + public Film getFilm(int filmId) { + // ... + } + + public List heroes(@Source Film film) { + return service.getHeroesByFilm(film); + } ---- +Above will fetch the film on the event-loop threads, but switch to the worker thread to fetch the heroes. + == Mutations Mutations are used when data is created, updated or deleted. @@ -569,9 +567,8 @@ Similar to the `createHero` mutation method we also retrieve the `name` and == Subscriptions -Subscriptions allows you to subscribe to a query. It allows you to receive events. - -NOTE: Subscription is currently still considered experimental. +Subscriptions allows you to subscribe to a query. It allows you to receive events and is using web sockets. +See the https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md[GraphQL over WebSocket Protocol] spec for more details. Example: We want to know when new Heroes are being created: @@ -714,33 +711,35 @@ By using the `@Observer` you can add anything to the Schema builder. NOTE: For the Observer to work, you need to enable events. In `application.properties`, add the following: `quarkus.smallrye-graphql.events.enabled=true`. -== Map to Scalar +== Adapting + +=== Adapt to Scalar Another SmallRye specific experimental feature, allows you to map an existing scalar (that is mapped by the implementation to a certain Java type) to another type, or to map complex object, that would typically create a `Type` or `Input` in GraphQL, to an existing scalar. -=== Mapping an existing Scalar to another type: +==== Adapting an existing Scalar to another type: [source,java] ---- public class Movie { - @ToScalar(Scalar.Int.class) + @AdaptToScalar(Scalar.Int.class) Long idLongThatShouldChangeToInt; // .... } ---- -Above will map the `Long` java type to an `Int` Scalar type, rather than the https://download.eclipse.org/microprofile/microprofile-graphql-1.0/microprofile-graphql.html#scalars[default] `BigInteger`. +Above will adapt the `Long` java type to an `Int` Scalar type, rather than the https://download.eclipse.org/microprofile/microprofile-graphql-1.0/microprofile-graphql.html#scalars[default] `BigInteger`. -=== Mapping a complex object to a Scalar type: +==== Adapting a complex object to a Scalar type: [source,java] ---- public class Person { - @ToScalar(Scalar.String.class) + @AdaptToScalar(Scalar.String.class) Phone phone; // .... @@ -771,7 +770,189 @@ public class Phone { } ---- -See more about the `@ToScalar` feature in the https://javadoc.io/static/io.smallrye/smallrye-graphql-api/1.0.6/index.html?io/smallrye/graphql/api/ToScalar.html[JavaDoc]. +See more about the `@AdaptToScalar` feature in the https://javadoc.io/static/io.smallrye/smallrye-graphql-api/1.5.0/io/smallrye/graphql/api/AdaptToScalar.html[JavaDoc]. + +=== Adapt with + +Another option for more complex cases is to provide an Adapter. You can then do the mapping yourself in the adapter. + +See more about the `AdaptWith` feature in the https://javadoc.io/static/io.smallrye/smallrye-graphql-api/1.5.0/io/smallrye/graphql/api/AdaptWith.html[JavaDoc]. + +For example: + +[source,java] +---- + public class Profile { + // Map this to an email address + @AdaptWith(AddressAdapter.class) + public Address address; + + // other getters/setters... + } + + public class AddressAdapter implements Adapter { + + @Override + public Address from(EmailAddress email) { + Address a = new Address(); + a.addressType = AddressType.email; + a.addLine(email.getValue()); + return a; + } + + @Override + public EmailAddress to(Address address) { + if (address != null && address.addressType != null && address.addressType.equals(AddressType.email)) { + return new EmailAddress(address.lines.get(0)); + } + return null; + } + } + +---- + +NOTE: `@JsonbTypeAdapter` is also supported. + +=== Built-in support for Maps + +By default, due to the fact that maps are hard to model in a schema (as the keys and values can be dynamic at runtime) GraphQL does not support maps by default. +Using the above adaption, `Map` support is added for Quarkus and are mapped to an `Entry` with an optional key parameter. +This allows you to return a map, and optionally query it by key. + +Example: + +[source,java] +---- + + @Query + public Map language() { + return languageService.getLanguages(); + } + + public enum ISO6391 { + af, + en, + de, + fr + } + + public class Language { + private ISO6391 iso6391; + private String nativeName; + private String enName; + private String please; + private String thankyou; + + // Getters & Setters + } + +---- + +NOTE: The key and value object can be any of Enum, Scalar or Complex object + +You can now query the whole map with all the fields: + +[source,graphql] +---- +{ + language{ + key + value { + enName + iso6391 + nativeName + please + thankyou + } + } +} +---- + +This will return a result like this for example: + +[source,json] +---- +{ + "data": { + "language": [ + { + "key": "fr", + "value": { + "enName": "french", + "iso6391": "fr", + "nativeName": "français", + "please": "s'il te plaît", + "thankyou": "merci" + } + }, + { + "key": "af", + "value": { + "enName": "afrikaans", + "iso6391": "af", + "nativeName": "afrikaans", + "please": "asseblief", + "thankyou": "dankie" + } + }, + { + "key": "de", + "value": { + "enName": "german", + "iso6391": "de", + "nativeName": "deutsch", + "please": "bitte", + "thankyou": "danke dir" + } + }, + { + "key": "en", + "value": { + "enName": "english", + "iso6391": "en", + "nativeName": "english", + "please": "please", + "thankyou": "thank you" + } + } + ] + } +} +---- + +You can also query by key + +[source,graphql] +---- +{ + language (key:af){ + value { + please + thankyou + } + } +} +---- + +That will return only that value in the map: + +[source,json] +---- +{ + "data": { + "language": [ + { + "value": { + "please": "asseblief", + "thankyou": "dankie" + } + } + ] + } +} +---- + +NOTE: The default map adapter can to overridden with our own implementation. == Error code diff --git a/docs/src/main/asciidoc/stork-kubernetes.adoc b/docs/src/main/asciidoc/stork-kubernetes.adoc new file mode 100644 index 0000000000000..788bbbc13df32 --- /dev/null +++ b/docs/src/main/asciidoc/stork-kubernetes.adoc @@ -0,0 +1,478 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Getting Started with SmallRye Stork +:extension-status: preview + +include::./attributes.adoc[] + +The essence of distributed systems resides in the interaction between services. +In modern architecture, you often have multiple instances of your service to share the load or improve the resilience by redundancy. +But how do you select the best instance of your service? +That's where https://smallrye.io/smallrye-stork[SmallRye Stork] helps. +Stork is going to choose the most appropriate instance. +It offers: + +* Extensible service discovery mechanisms +* Built-in support for Consul and Kubernetes +* Customizable client load-balancing strategies + +include::./status-include.adoc[] + +== Prerequisites + +:prerequisites-docker: +include::includes/devtools/prerequisites.adoc[] +* Access to a Kubernetes cluster (Minikube is a viable option) + +== Architecture + +In this guide, we will work with a few components deployed in a Kubernetes cluster: + +* A simple blue service. +* A simple red service. +* The `color-service` is the Kubernetes service which is the entry point to the Blue and Red instances. +* A client service using a REST client to call the blue or the red service. Service discovery and selection are delegated to Stork. + +image::stork-kubernetes-architecture.png[Architecture of the application,width=100%, align=center] + +For the sake of simplicity, everything will be deployed in the same namespace of the Kubernetes cluster. + +== Solution + +We recommend that you follow the instructions in the next sections and create the applications step by step. +However, you can go right to the completed example. + +Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {quickstarts-archive-url}[archive]. + +The solution is located in the `stork-kubernetes-quickstart` {quickstarts-tree-url}/stork-kubernetes-quickstart[directory]. + +== Discovery and selection + +Before going further, we need to discuss discovery vs. selection. + +- Service discovery is the process of locating service instances. +It produces a list of service instances that is potentially empty (if no service matches the request) or contains multiple service instances. + +- Service selection, also called load-balancing, chooses the best instance from the list returned by the discovery process. +The result is a single service instance or an exception when no suitable instance can be found. + +Stork handles both discovery and selection. +However, it does not handle the communication with the service but only provides a service instance. +The various integrations in Quarkus extract the location of the service from that service instance. + + +== Bootstrapping the project + +Create a Quarkus project importing the quarkus-rest-client-reactive and quarkus-resteasy-reactive extensions using your favorite approach: + +:create-app-artifact-id: stork-kubernetes-quickstart +:create-app-extensions: quarkus-rest-client-reactive,quarkus-resteasy-reactive +include::includes/devtools/create-app.adoc[] + +In the generated project, also add the following dependencies: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.smallrye.stork + stork-service-discovery-kubernetes + + + io.smallrye.stork + stork-load-balancer-random + + + io.quarkus + quarkus-kubernetes + + + io.quarkus + quarkus-kubernetes-client + + + io.quarkus + quarkus-container-image-jib + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.smallrye.stork:stork-service-discovery-kubernetes") +implementation("io.smallrye.stork:stork-load-balancer-random") +implementation("io.quarkus:quarkus-kubernetes") +implementation("io.quarkus:quarkus-kubernetes-client") +implementation("io.quarkus:quarkus-container-image-jib") +---- + +`stork-service-discovery-kubernetes` provides an implementation of service discovery for Kubernetes. `stork-load-balancer-random` provides an implmentation of random load balancer. `quarkus-kubernetes` enables the generation of Kubernetes manifests each time we perform a build. The `quarkuks-kubernetes-client` extension enables the use of the Fabric8 Kubernetes Client in native mode. And `quarkus-container-image-jib` enables the build of a container image using https://github.com/GoogleContainerTools/jib[Jib]. + +== The Blue and Red services + +Let's start with the very beginning: the service we will discover, select and call. + +The Red and Blue are two simple REST services serving an endpoint responding `Hello from Red!` and `Hello from Blue!` respectively. The code of both applications has been developed following the https://quarkus.io/guides/getting-started[Getting Started Guide]. + +As the goal of this guide is to show how to use Stork Kubernetes service discovery, we won't provide the specifics steps for the Red and Blue services. Their container images are already built and available in a public registry: + +* https://quay.io/repository/quarkus/blue-service[Blue service container image] +* https://quay.io/repository/quarkus/red-service[Red service container image] + + +== Deploy the Blue and Red services in Kubernetes + +Now that we have our service container images available in a public registry, we need to deploy them into the Kubernetes cluster. + +The following file contains all the Kubernetes resources needed to deploy the Blue and Red services in the cluster and make them accessible: + +[source, yaml] +---- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + namespace: development + name: endpoints-reader +rules: + - apiGroups: [""] # "" indicates the core API group + resources: ["endpoints", "pods"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: stork-rb + namespace: development +subjects: + - kind: ServiceAccount + # Reference to upper's `metadata.name` + name: default + # Reference to upper's `metadata.namespace` + namespace: development +roleRef: + kind: Role + name: endpoints-reader + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + app.quarkus.io/commit-id: f747f359406bedfb1a39c57392a5b5a9eaefec56 + app.quarkus.io/build-timestamp: 2022-03-31 - 10:36:56 +0000 + labels: + app.kubernetes.io/name: color-service + app.kubernetes.io/version: "1.0" + name: color-service //<1> +spec: + ports: + - name: http + port: 80 + targetPort: 8080 + selector: + app.kubernetes.io/version: "1.0" + type: color-service + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + app.quarkus.io/commit-id: f747f359406bedfb1a39c57392a5b5a9eaefec56 + app.quarkus.io/build-timestamp: 2022-03-31 - 10:36:56 +0000 + labels: + color: blue + type: color-service + app.kubernetes.io/name: blue-service + app.kubernetes.io/version: "1.0" + name: blue-service //<2> +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: blue-service + app.kubernetes.io/version: "1.0" + template: + metadata: + annotations: + app.quarkus.io/commit-id: f747f359406bedfb1a39c57392a5b5a9eaefec56 + app.quarkus.io/build-timestamp: 2022-03-31 - 10:36:56 +0000 + labels: + color: blue + type: color-service + app.kubernetes.io/name: blue-service + app.kubernetes.io/version: "1.0" + spec: + containers: + - env: + - name: KUBERNETES_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + image: quay.io/quarkus/blue-service:1.0 + imagePullPolicy: Always + name: blue-service + ports: + - containerPort: 8080 + name: http + protocol: TCP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + app.quarkus.io/commit-id: 27be03414510f776ca70d70d859b33e134570443 + app.quarkus.io/build-timestamp: 2022-03-31 - 10:38:54 +0000 + labels: + color: red + type: color-service + app.kubernetes.io/version: "1.0" + app.kubernetes.io/name: red-service + name: red-service //<2> +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/version: "1.0" + app.kubernetes.io/name: red-service + template: + metadata: + annotations: + app.quarkus.io/commit-id: 27be03414510f776ca70d70d859b33e134570443 + app.quarkus.io/build-timestamp: 2022-03-31 - 10:38:54 +0000 + labels: + color: red + type: color-service + app.kubernetes.io/version: "1.0" + app.kubernetes.io/name: red-service + spec: + containers: + - env: + - name: KUBERNETES_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + image: quay.io/quarkus/red-service:1.0 + imagePullPolicy: Always + name: red-service + ports: + - containerPort: 8080 + name: http + protocol: TCP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress //<3> +metadata: + annotations: + app.quarkus.io/commit-id: f747f359406bedfb1a39c57392a5b5a9eaefec56 + app.quarkus.io/build-timestamp: 2022-03-31 - 10:46:19 +0000 + labels: + app.kubernetes.io/name: color-service + app.kubernetes.io/version: "1.0" + color: blue + type: color-service + name: color-service +spec: + rules: + - host: color-service.127.0.0.1.nip.io + http: + paths: + - backend: + service: + name: color-service + port: + name: http + path: / + pathType: Prefix + +---- + +There are a few interesting parts in this listing: + +<1> The Kubernetes Service resource, `color-service`, that Stork will discover. +<2> The Red and Blue service instances behind the `color-service` Kubernetes service. +<3> A Kubernetes Ingress resource making the `color-service` accessible from the outside of the cluster at the `color-service.127.0.0.1.nip.io` url. Not that the Ingress is not needed for Stork however, it helps to check that the architecture is in place. + +Create a file named `kubernetes-setup.yml` with the content above at the root of the project and run the following commands to deploy all the resources in the Kubernetes cluster. Don't forget to create a dedicated namespace: + +[source,shell script] +---- +kubectl create namespace development +kubectl apply -f kubernetes-setup.yml -n=development +---- + +If everything went well the Color service is accessible on http://color-service.127.0.0.1.nip.io. You should have `Hello from Red!` and `Hello from Blue!` response randomly. + +NOTE: Stork is not limited to Kubernetes and integrates with other service discovery mechanisms. + + +== The REST Client interface and the front end API + +So far, we didn't use Stork; we just deployed the services we will be discovering, selecting, and calling. + +We will call the services using the Reactive REST Client. +Create the `src/main/java/org/acme/MyService.java` file with the following content: + +[source, java] +---- +package org.acme; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import javax.ws.rs.GET; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +/** + * The REST Client interface. + * + * Notice the `baseUri`. It uses `stork://` as URL scheme indicating that the called service uses Stork to locate and + * select the service instance. The `my-service` part is the service name. This is used to configure Stork discovery + * and selection in the `application.properties` file. + */ +@RegisterRestClient(baseUri = "stork://my-service") +public interface MyService { + + @GET + @Produces(MediaType.TEXT_PLAIN) + String get(); +} +---- + +It's a straightforward REST client interface containing a single method. However, note the `baseUri` attribute: +* the `stork://` suffix instructs the REST client to delegate the discovery and selection of the service instances to Stork, +* the `my-service` part of the URI is the service name we will be using in the application configuration. + +It does not change how the REST client is used. +Create the `src/main/java/org/acme/FrontendApi.java` file with the following content: + +[source, java] +---- +package org.acme; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +/** + * A frontend API using our REST Client (which uses Stork to locate and select the service instance on each call). + */ +@Path("/api") +public class FrontendApi { + + @RestClient MyService service; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String invoke() { + return service.get(); + } + +} +---- + +It injects and uses the REST client as usual. + +== Stork configuration + +Now we need to configure Stork for using Kubernetes to discover the red and blue instances of the service. + +In the `src/main/resources/application.properties`, add: + +[source, properties] +---- +quarkus.stork.my-service.service-discovery.type=kubernetes +quarkus.stork.my-service.service-discovery.k8s-namespace=development +quarkus.stork.my-service.service-discovery.application=color-service +quarkus.stork.my-service.load-balancer.type=random +---- + +`stork.my-service.service-discovery` indicates which type of service discovery we will be using to locate the `my-service` service. +In our case, it's `kubernetes`. +If your access to the Kubernetes cluster is configured via Kube config file, you don't need to configure the access to it. Otherwise, set the proper Kubernetes url using the `quarkus.stork.my-service.service-discovery.k8s-host` property. +`quarkus.stork.my-service.service-discovery.application` contains the name of the Kubernetes service Stork is going to ask for. In our case, this is the `color-service` corresponding to the kubernetes service backed by the Red and Blue instances. +Finally, `quarkus.stork.my-service.load-balancer.type` configures the service selection. In our case, we use a `random` Load Balancer. + +== Deploy the REST Client interface and the front end API in the Kubernetes cluster + +The system is almost complete. We only need to deploy the REST Client interface and the client service to the cluster. +In the `src/main/resources/application.properties`, add: + +[source, properties] +---- +quarkus.container-image.registry= +quarkus.kubernetes-client.trust-certs=true +quarkus.kubernetes.ingress.expose=true +quarkus.kubernetes.ingress.host=my-service.127.0.0.1.nip.io +---- + +The `quarkus.container-image.registry` contains the container registry to use. +The `quarkus.kubernetes.ingress.expose` indicates that the service will be accessible from the outside of the cluster. +The `quarkus.kubernetes.ingress.host` contains the url to access the service. We are using https://nip.io/[nip.io] wildcard for IP address mappings. + +For a more customized configuration you can check the https://quarkus.io/guides/deploying-to-kubernetes[Deploying to Kubernetes guide] + +== Build and push the container image + +Thanks to the extensions we are using, we can perform the build of a container image using Jib and also enabling the generation of Kubernetes manifests while building the application. For example, the following command will generate a Kubernetes manifest in the `target/kubernetes/` directory and also build and push a container image for the project: + +[source,shell script] +---- +./mvnw package -Dquarkus.container-image.build=true -Dquarkus.container-image.push=true +---- + +== Deploy client service to the Kubernetes cluster + +The generated manifest can be applied to the cluster from the project root using kubectl: + +[source,shell script] +---- +kubectl apply -f target/kubernetes/kubernetes.yml -n=development +---- + +We're done! +So, let's see if it works. + +Open a browser and navigate to http://my-service.127.0.0.1.nip.io/api. + +Or if you prefer, in another terminal, run: + +[source, shell script] +---- +> curl http://my-service.127.0.0.1.nip.io/api +... +> curl http://my-service.127.0.0.1.nip.io/api +... +> curl http://my-service.127.0.0.1.nip.io/api +... +---- + +The responses should alternate randomly between `Hello from Red!` and `Hello from Blue!`. + +You can compile this application into a native executable: + +include::includes/devtools/build-native.adoc[] + +Then, you need to build a container image based on the native executable. For this use the corresponding Dockerfile: + +[source, shell script] +---- +> docker build -f src/main/docker/Dockerfile.native -t quarkus/stork-kubernetes-quickstart . +---- + +After publishing the new image to the container registry. You can redeploy the Kubernetes manifest to the cluster. + +== Going further + +This guide has shown how to use SmallRye Stork to discover and select your services. +You can find more about Stork in: + +- the xref:stork-reference.adoc[Stork reference guide], +- the xref:stork.adoc[Stork with Consul reference guide], +- the https://smallrye.io/smallrye-stork[SmallRye Stork website]. diff --git a/docs/src/main/asciidoc/stork.adoc b/docs/src/main/asciidoc/stork.adoc index 428af56684d96..640bbbd0e24aa 100644 --- a/docs/src/main/asciidoc/stork.adoc +++ b/docs/src/main/asciidoc/stork.adoc @@ -6,8 +6,6 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Getting Started with SmallRye Stork :extension-status: preview -// Temporary until stork in in the BOM -:stork-version: 1.0.0.Beta1 include::./attributes.adoc[] The essence of distributed systems resides in the interaction between services. diff --git a/docs/src/main/asciidoc/vertx.adoc b/docs/src/main/asciidoc/vertx.adoc index 17c36a9d00dad..e2f991c61ba4d 100644 --- a/docs/src/main/asciidoc/vertx.adoc +++ b/docs/src/main/asciidoc/vertx.adoc @@ -227,7 +227,7 @@ public Multi readLargeFile() { new OpenOptions().setRead(true) ) .onItem().transformToMulti(file -> file.toMulti()) // <3> - .onItem().transform(content -> content.toString(StandardCharsets.UTF_8)) // <4> + .onItem().transform(content -> content.toString(StandardCharsets.UTF_8) // <4> + "\n------------\n"); // <5> } ---- diff --git a/docs/src/main/asciidoc/websockets.adoc b/docs/src/main/asciidoc/websockets.adoc index dcdc31adc770f..72d9bf9ffe92e 100644 --- a/docs/src/main/asciidoc/websockets.adoc +++ b/docs/src/main/asciidoc/websockets.adoc @@ -237,6 +237,6 @@ public class ChatTest { == More WebSocket Information -The Quarkus WebSocket implementation is an implementation of link:https://eclipse-ee4j.github.io/websocket-api/[Jakarta Websockets]. +The Quarkus WebSocket implementation is an implementation of link:https://jakarta.ee/specifications/websocket/[Jakarta Websockets]. diff --git a/docs/src/main/asciidoc/writing-extensions.adoc b/docs/src/main/asciidoc/writing-extensions.adoc index 2f21823f21841..6cedd9ceb5d75 100644 --- a/docs/src/main/asciidoc/writing-extensions.adoc +++ b/docs/src/main/asciidoc/writing-extensions.adoc @@ -546,7 +546,7 @@ Please refer to https://github.com/quarkusio/quarkus/blob/{quarkus-version}/devt You will need to apply the `io.quarkus.extension` plugin in the `runtime` module of your extension project. The plugin includes the `extensionDescriptor` task that will generate `META-INF/quarkus-extension.properties` and `META-INF/quarkus-extension.yml` files. The plugin also enables the `io.quarkus:quarkus-extension-processor` annotation processor in both `deployment` and `runtime` modules. -The name of the deployment module can be configured in the plugin by setting the `deploymentArtifact` property. The property is set to `deployment` by default: +The name of the deployment module can be configured in the plugin by setting the `deploymentModule` property. The property is set to `deployment` by default: [source,groovy,subs=attributes+] ---- @@ -556,7 +556,7 @@ plugins { } quarkusExtension { - deploymentArtifact = 'deployment' + deploymentModule = 'deployment' } dependencies { @@ -755,6 +755,9 @@ Empty build items are final (usually empty) classes which extend `io.quarkus.bui They represent build items that don't actually carry any data, and allow such items to be produced and consumed without having to instantiate empty classes. They cannot themselves be instantiated. +IMPORTANT: As they cannot be instantiated, they cannot be injected by any means, nor be returned by a build step (or via a `BuildProducer`). +To produce an empty build item you must annotate the build step with `@Produce(MyEmptyBuildItem.class)` and to consume it by `@Consume(MyEmptyBuildItem.class)`. + .Example of an empty build item [source%nowrap,java] ---- @@ -3125,7 +3128,7 @@ Before publishing your extension to the xref:tooling.adoc[Quarkus tooling], make * The `quarkus-extension.yaml` file (in the extension's `runtime/` module) has the minimum metadata set: ** `name` -** `description` (unless you have it already set in the `runtime/pom.xml`'s `` element, which is the recommended approach) +** `description` (unless you have it already set in the ``runtime/pom.xml``'s `` element, which is the recommended approach) * Your extension is published in Maven Central diff --git a/docs/src/main/java/io/quarkus/docs/generation/TooltipInlineMacroProcessor.java b/docs/src/main/java/io/quarkus/docs/generation/TooltipInlineMacroProcessor.java new file mode 100644 index 0000000000000..6ddfd4ee67095 --- /dev/null +++ b/docs/src/main/java/io/quarkus/docs/generation/TooltipInlineMacroProcessor.java @@ -0,0 +1,24 @@ +package io.quarkus.docs.generation; + +import java.util.HashMap; +import java.util.Map; + +import org.asciidoctor.ast.ContentNode; +import org.asciidoctor.extension.InlineMacroProcessor; +import org.asciidoctor.extension.Name; + +/** + * + * Tooltip inline macro implementation for PDF (HTML) files where tooltip is not supported. + * Enum constant name is wrapped in `` and constant description is ignored. + */ +@Name("tooltip") +public class TooltipInlineMacroProcessor extends InlineMacroProcessor { + + @Override + public Object process(ContentNode contentNode, String target, Map map) { + var attributes = new HashMap(); + attributes.put("subs", ":normal"); + return createPhraseNode(contentNode, "quoted", String.format("`%s`", target), attributes); + } +} diff --git a/extensions/amazon-lambda-http/deployment/pom.xml b/extensions/amazon-lambda-http/deployment/pom.xml index f3b04e2293f73..6785c8ce70636 100644 --- a/extensions/amazon-lambda-http/deployment/pom.xml +++ b/extensions/amazon-lambda-http/deployment/pom.xml @@ -45,6 +45,15 @@
+ + + src/main/resources + + + src/main/resources/http + false + + maven-compiler-plugin diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java index dea92dbf76d41..43d53032adcb3 100644 --- a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import org.jboss.logging.Logger; @@ -34,7 +35,6 @@ import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.ReferenceCountUtil; -import io.quarkus.amazon.lambda.http.model.Headers; import io.quarkus.netty.runtime.virtual.VirtualClientConnection; import io.quarkus.netty.runtime.virtual.VirtualResponseHandler; import io.quarkus.vertx.http.runtime.QuarkusHttpHeaders; @@ -46,10 +46,11 @@ public class LambdaHttpHandler implements RequestHandler> ERROR_HEADERS = Map.of("Content-Type", List.of("application/json")); + + // comma headers for headers that have comma in value and we don't want to split it up into + // multiple headers + private static final Set COMMA_HEADERS = Set.of("access-control-request-headers"); public APIGatewayV2HTTPResponse handleRequest(APIGatewayV2HTTPEvent request, Context context) { InetSocketAddress clientAddress = null; @@ -66,7 +67,7 @@ public APIGatewayV2HTTPResponse handleRequest(APIGatewayV2HTTPEvent request, Con APIGatewayV2HTTPResponse res = new APIGatewayV2HTTPResponse(); res.setStatusCode(500); res.setBody("{ \"message\": \"Internal Server Error\" }"); - res.setMultiValueHeaders(errorHeaders); + res.setMultiValueHeaders(ERROR_HEADERS); return res; } @@ -182,8 +183,14 @@ httpMethod, ofNullable(request.getRawQueryString()) if (request.getHeaders() != null) { //apparently this can be null if no headers are sent for (Map.Entry header : request.getHeaders().entrySet()) { if (header.getValue() != null) { - for (String val : header.getValue().split(",")) - nettyRequest.headers().add(header.getKey(), val); + // Some header values have commas in them and we don't want to + // split them up into multiple header values. + if (COMMA_HEADERS.contains(header.getKey().toLowerCase(Locale.ROOT))) { + nettyRequest.headers().add(header.getKey(), header.getValue()); + } else { + for (String val : header.getValue().split(",")) + nettyRequest.headers().add(header.getKey(), val); + } } } } diff --git a/extensions/amazon-lambda-rest/deployment/pom.xml b/extensions/amazon-lambda-rest/deployment/pom.xml index ee947d1bc5adb..893a1316b2ca5 100644 --- a/extensions/amazon-lambda-rest/deployment/pom.xml +++ b/extensions/amazon-lambda-rest/deployment/pom.xml @@ -41,6 +41,15 @@
+ + + src/main/resources + + + src/main/resources/http + false + + maven-compiler-plugin diff --git a/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AbstractLambdaPollLoop.java b/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AbstractLambdaPollLoop.java index 03d2883bb28d2..d856507bf9526 100644 --- a/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AbstractLambdaPollLoop.java +++ b/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AbstractLambdaPollLoop.java @@ -35,6 +35,10 @@ public AbstractLambdaPollLoop(ObjectMapper objectMapper, ObjectReader cognitoIdR this.launchMode = launchMode; } + protected boolean shouldLog(Exception e) { + return true; + } + protected abstract boolean isStream(); protected HttpURLConnection requestConnection = null; @@ -131,7 +135,9 @@ public void run() { if (abortGracefully(e)) { return; } - log.error("Failed to run lambda (" + launchMode + ")", e); + if (shouldLog(e)) { + log.error("Failed to run lambda (" + launchMode + ")", e); + } postError(AmazonLambdaApi.invocationError(baseUrl, requestId), new FunctionError(e.getClass().getName(), e.getMessage())); diff --git a/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java b/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java index 569649d3a62e6..dd05269cf7ec2 100644 --- a/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java +++ b/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import javax.inject.Named; @@ -24,6 +25,7 @@ import io.quarkus.amazon.lambda.runtime.AmazonLambdaRecorder; import io.quarkus.amazon.lambda.runtime.FunctionError; +import io.quarkus.amazon.lambda.runtime.LambdaBuildTimeConfig; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.builder.BuildException; @@ -258,4 +260,15 @@ void startPoolLoopDevOrTest(AmazonLambdaRecorder recorder, } } + @BuildStep + @Record(value = ExecutionTime.RUNTIME_INIT) + void recordExpectedExceptions(LambdaBuildTimeConfig config, + BuildProducer registerForReflection, + AmazonLambdaRecorder recorder) { + Set> classes = config.expectedExceptions.map(Set::copyOf).orElseGet(Set::of); + classes.stream().map(clazz -> new ReflectiveClassBuildItem(false, false, false, clazz)) + .forEach(registerForReflection::produce); + recorder.setExpectedExceptionClasses(classes); + } + } diff --git a/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/LambdaHandlerET.java b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/LambdaHandlerET.java index e37e209b3c221..135b02bac0547 100644 --- a/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/LambdaHandlerET.java +++ b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/LambdaHandlerET.java @@ -12,7 +12,7 @@ class LambdaHandlerET { @Test public void testSimpleLambdaSuccess() throws Exception { - // you test your lambas by invoking on http://localhost:8081 + // you test your lambdas by invoking on http://localhost:8081 // this works in dev mode too Person in = new Person(); diff --git a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/test/java/LambdaHandlerTest.java b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/test/java/LambdaHandlerTest.java index d522e25ec4dec..bbc9e8619ae24 100644 --- a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/test/java/LambdaHandlerTest.java +++ b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/test/java/LambdaHandlerTest.java @@ -12,7 +12,7 @@ public class LambdaHandlerTest { @Test public void testSimpleLambdaSuccess() throws Exception { - // you test your lambas by invoking on http://localhost:8081 + // you test your lambdas by invoking on http://localhost:8081 // this works in dev mode too InputObject in = new InputObject(); diff --git a/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AmazonLambdaRecorder.java b/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AmazonLambdaRecorder.java index 0de989ddc979b..8c779b60dbc9f 100644 --- a/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AmazonLambdaRecorder.java +++ b/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AmazonLambdaRecorder.java @@ -6,6 +6,7 @@ import java.lang.reflect.Method; import java.util.List; import java.util.Map; +import java.util.Set; import org.jboss.logging.Logger; @@ -35,6 +36,7 @@ public class AmazonLambdaRecorder { private static BeanContainer beanContainer; private static LambdaInputReader objectReader; private static LambdaOutputWriter objectWriter; + private static Set> expectedExceptionClasses; private final LambdaConfig config; @@ -47,6 +49,10 @@ public void setStreamHandlerClass(Class handler, beanContainer = container; } + public void setExpectedExceptionClasses(Set> classes) { + expectedExceptionClasses = classes; + } + public void setHandlerClass(Class> handler, BeanContainer container) { handlerClass = handler; beanContainer = container; @@ -186,6 +192,11 @@ protected void processRequest(InputStream input, OutputStream output, AmazonLamb handler.handleRequest(input, output, context); } + + @Override + protected boolean shouldLog(Exception e) { + return expectedExceptionClasses.stream().noneMatch(clazz -> clazz.isAssignableFrom(e.getClass())); + } }; loop.startPollLoop(context); diff --git a/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaBuildTimeConfig.java b/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaBuildTimeConfig.java new file mode 100644 index 0000000000000..4a035af4fb7ee --- /dev/null +++ b/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaBuildTimeConfig.java @@ -0,0 +1,22 @@ +package io.quarkus.amazon.lambda.runtime; + +import java.util.List; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "lambda", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public class LambdaBuildTimeConfig { + + /** + * The exception classes expected to be thrown by the handler. + * + * Any exception thrown by the handler that is an instance of a class in this list will not be logged, + * but will otherwise be handled normally by the lambda runtime. This is useful for avoiding unnecessary + * stack traces while preserving the ability to log unexpected exceptions. + */ + @ConfigItem + public Optional>> expectedExceptions; +} 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 e442f72837c75..53808672a5a2b 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 @@ -106,7 +106,7 @@ void registerCustomConfigBeanTypes(BeanDiscoveryFinishedBuildItem beanDiscovery, AnnotationInstance configProperty = injectionPoint.getRequiredQualifier(MP_CONFIG_PROPERTY_NAME); if (configProperty != null) { // Register a custom bean for injection points that are not handled by ConfigProducer - Type injectedType = injectionPoint.getType(); + Type injectedType = injectionPoint.getRequiredType(); if (!isHandledByProducers(injectedType)) { customBeanTypes.add(injectedType); } diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/config/ConfigImplicitConverterTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/config/ConfigImplicitConverterTest.java index 0ddcf2d264dea..1762af06bdf97 100644 --- a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/config/ConfigImplicitConverterTest.java +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/config/ConfigImplicitConverterTest.java @@ -4,6 +4,7 @@ import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; +import javax.inject.Provider; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.shrinkwrap.api.asset.StringAsset; @@ -17,8 +18,8 @@ public class ConfigImplicitConverterTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar - .addClasses(Configured.class) - .addAsResource(new StringAsset("foo=1"), "application.properties")); + .addClasses(Configured.class, Foo.class, Bar.class) + .addAsResource(new StringAsset("foo=1\nbar=1"), "application.properties")); @Inject Configured configured; @@ -26,6 +27,7 @@ public class ConfigImplicitConverterTest { @Test public void testFoo() { assertEquals("1", configured.getFooValue()); + assertEquals("1", configured.getBarProviderValue()); } @ApplicationScoped @@ -35,9 +37,17 @@ static class Configured { @ConfigProperty(name = "foo") Foo foo; + @ConfigProperty(name = "bar") + Provider barProvider; + String getFooValue() { return foo != null ? foo.value : null; } + + String getBarProviderValue() { + return barProvider.get().value; + } + } public static class Foo { @@ -50,4 +60,14 @@ public Foo(String value) { } + public static class Bar { + + String value; + + public Bar(String value) { + this.value = value; + } + + } + } diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/lookup/ListInjectionTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/lookup/ListInjectionTest.java index 2a840af310afc..27f2a443c3f74 100644 --- a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/lookup/ListInjectionTest.java +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/lookup/ListInjectionTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.arc.All; +import io.quarkus.arc.Arc; import io.quarkus.arc.InstanceHandle; import io.quarkus.arc.Priority; import io.quarkus.test.QuarkusUnitTest; @@ -70,6 +71,22 @@ public void testInjection() { assertTrue(CounterAlpha.DESTROYED.get()); } + @Test + public void testListAll() { + List> services = Arc.container().listAll(Service.class); + assertEquals(2, services.size()); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> services.remove(0)); + // ServiceBravo has higher priority + InstanceHandle bravoHandle = services.get(0); + Service bravo = bravoHandle.get(); + assertEquals("bravo", bravo.ping()); + assertEquals(Dependent.class, bravoHandle.getBean().getScope()); + assertTrue(bravo.getInjectionPoint().isPresent()); + // Empty injection point + assertEquals(Object.class, bravo.getInjectionPoint().get().getType()); + } + @Singleton static class Foo { diff --git a/extensions/cache/deployment/src/test/java/io/quarkus/cache/test/runtime/CacheResultCompletionStageReturnTypeTest.java b/extensions/cache/deployment/src/test/java/io/quarkus/cache/test/runtime/CacheResultCompletionStageReturnTypeTest.java deleted file mode 100644 index 3cc2d167cffef..0000000000000 --- a/extensions/cache/deployment/src/test/java/io/quarkus/cache/test/runtime/CacheResultCompletionStageReturnTypeTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package io.quarkus.cache.test.runtime; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import io.quarkus.cache.CacheResult; -import io.quarkus.test.QuarkusUnitTest; - -public class CacheResultCompletionStageReturnTypeTest { - - private static final Object KEY_1 = new Object(); - private static final Object KEY_2 = new Object(); - - @RegisterExtension - static final QuarkusUnitTest TEST = new QuarkusUnitTest().withApplicationRoot(jar -> jar.addClass(CachedService.class)); - - @Inject - CachedService cachedService; - - @Test - public void testAllCacheAnnotations() throws InterruptedException, ExecutionException { - // STEP 1 - // Action: @CacheResult-annotated method call. - // Expected effect: method invoked and result cached. - // Verified by: STEP 2. - CompletionStage completionStage1 = cachedService.cachedMethod(KEY_1); - - // STEP 2 - // Action: same call as STEP 1. - // Expected effect: method not invoked and result coming from the cache. - // Verified by: same object reference between STEPS 1 and 2 results. - CompletionStage completionStage2 = cachedService.cachedMethod(KEY_1); - assertTrue(completionStage1 == completionStage2); - - // STEP 3 - // Action: same call as STEP 2 with a new key. - // Expected effect: method invoked and result cached. - // Verified by: different objects references between STEPS 2 and 3 results. - CompletionStage completionStage3 = cachedService.cachedMethod(KEY_2); - assertTrue(completionStage2 != completionStage3); - - // We need all of the futures to complete at this point. - CompletableFuture.allOf(completionStage1.toCompletableFuture(), completionStage2.toCompletableFuture(), - completionStage3.toCompletableFuture()).get(); - - Object value1 = completionStage1.toCompletableFuture().get(); - Object value2 = completionStage2.toCompletableFuture().get(); - Object value3 = completionStage3.toCompletableFuture().get(); - - // Values objects references resulting from STEPS 1 and 2 should be equal since the same cache key was used. - assertTrue(value1 == value2); - - // Values objects references resulting from STEPS 2 and 3 should be different since a different cache key was used. - assertTrue(value2 != value3); - } - - @ApplicationScoped - static class CachedService { - - // This is required to make sure the CompletableFuture from the tests are executed concurrently. - private ExecutorService executorService = Executors.newFixedThreadPool(3); - - @CacheResult(cacheName = "test-cache") - public CompletionStage cachedMethod(Object key) { - return CompletableFuture.supplyAsync(() -> { - try { - // This is another requirement for concurrent CompletableFuture executions. - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - return new Object(); - }, executorService); - } - } -} diff --git a/extensions/cache/deployment/src/test/java/io/quarkus/cache/test/runtime/CompletionStageReturnTypeTest.java b/extensions/cache/deployment/src/test/java/io/quarkus/cache/test/runtime/CompletionStageReturnTypeTest.java new file mode 100644 index 0000000000000..823fe07a0df2e --- /dev/null +++ b/extensions/cache/deployment/src/test/java/io/quarkus/cache/test/runtime/CompletionStageReturnTypeTest.java @@ -0,0 +1,168 @@ +package io.quarkus.cache.test.runtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.cache.CacheInvalidate; +import io.quarkus.cache.CacheInvalidateAll; +import io.quarkus.cache.CacheResult; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Tests the caching annotations on methods returning {@link CompletableFuture}. + */ +public class CompletionStageReturnTypeTest { + + private static final String CACHE_NAME_1 = "test-cache-1"; + private static final String CACHE_NAME_2 = "test-cache-2"; + private static final String KEY_1 = "key-1"; + private static final String KEY_2 = "key-2"; + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().withApplicationRoot((jar) -> jar.addClass(CachedService.class)); + + @Inject + CachedService cachedService; + + @Test + void testCacheResult() throws ExecutionException, InterruptedException { + // STEP 1 + // Action: a method annotated with @CacheResult and returning a CompletableFuture is called. + // Expected effect: the method is invoked and its result is cached asynchronously, as CompletableFuture is eager. + // Verified by: invocations counter. + CompletableFuture cf1 = cachedService.cacheResult1(KEY_1); + assertEquals(1, cachedService.getCacheResultInvocations()); + + // STEP 2 + // Action: same call as STEP 1. + // Expected effect: the method is not invoked and a new CompletableFuture instance is returned (because of the cache interceptor implementation). + // Verified by: invocations counter and different objects references between STEPS 1 AND 2 results. + CompletableFuture cf2 = cachedService.cacheResult1(KEY_1); + assertEquals(1, cachedService.getCacheResultInvocations()); + assertNotSame(cf1, cf2); + + // STEP 3 + // Action: the result of the CompletableFuture from STEP 1 is retrieved. + // Expected effect: the method from STEP 1 is not invoked and the value cached in STEP 1 is returned. + // Verified by: invocations counter and STEP 4. + String result1 = cf1.get(); + assertEquals(1, cachedService.getCacheResultInvocations()); + + // STEP 4 + // Action: the result of the CompletableFuture from STEP 2 is retrieved. + // Expected effect: the method from STEP 2 is not invoked and the value cached in STEP 1 is returned. + // Verified by: invocations counter and same object reference between STEPS 3 and 4 emitted items. + String result2 = cf2.get(); + assertEquals(1, cachedService.getCacheResultInvocations()); + assertSame(result1, result2); + + // STEP 5 + // Action: same call as STEP 2 with a different key and an immediate CompletableFuture result retrieval. + // Expected effect: the method is invoked and a new value is cached. + // Verified by: invocations counter and different objects references between STEPS 2 and 5 results. + String result3 = cachedService.cacheResult1("another-key").get(); + assertEquals(2, cachedService.getCacheResultInvocations()); + assertNotSame(result2, result3); + } + + @Test + void testCacheInvalidate() throws ExecutionException, InterruptedException { + // First, let's put some data into the caches. + String value1 = cachedService.cacheResult1(KEY_1).get(); + Object value2 = cachedService.cacheResult2(KEY_1).get(); + Object value3 = cachedService.cacheResult2(KEY_2).get(); + + // The cached data identified by KEY_1 is invalidated now. + cachedService.cacheInvalidate(KEY_1).get(); + // The method annotated with @CacheInvalidate should have been invoked, as CompletionFuture is eager. + assertEquals(1, cachedService.getCacheInvalidateInvocations()); + + // The data for the second key should still be cached at this point. + Object value4 = cachedService.cacheResult2(KEY_2).get(); + assertSame(value3, value4); + + // The data identified by KEY_1 should have been removed from the cache. + String value5 = cachedService.cacheResult1(KEY_1).get(); + Object value6 = cachedService.cacheResult2(KEY_1).get(); + + // The objects references should be different for the invalidated key. + assertNotSame(value1, value5); + assertNotSame(value2, value6); + } + + @Test + void testCacheInvalidateAll() throws ExecutionException, InterruptedException { + // First, let's put some data into the caches. + String value1 = cachedService.cacheResult1(KEY_1).get(); + Object value2 = cachedService.cacheResult2(KEY_2).get(); + + // All the cached data is invalidated now. + cachedService.cacheInvalidateAll().get(); + + // The method annotated with @CacheInvalidateAll should have been invoked, as CompletionStage is eager. + assertEquals(1, cachedService.getCacheInvalidateAllInvocations()); + + // Let's call the methods annotated with @CacheResult again. + String value3 = cachedService.cacheResult1(KEY_1).get(); + Object value4 = cachedService.cacheResult2(KEY_2).get(); + + // All objects references should be different. + assertNotSame(value1, value3); + assertNotSame(value2, value4); + } + + @ApplicationScoped + static class CachedService { + + private volatile int cacheResultInvocations; + private volatile int cacheInvalidateInvocations; + private volatile int cacheInvalidateAllInvocations; + + @CacheResult(cacheName = CACHE_NAME_1) + public CompletableFuture cacheResult1(String key) { + cacheResultInvocations++; + return CompletableFuture.completedFuture(new String()); + } + + @CacheResult(cacheName = CACHE_NAME_2) + public CompletableFuture cacheResult2(String key) { + return CompletableFuture.completedFuture(new Object()); + } + + @CacheInvalidate(cacheName = CACHE_NAME_1) + @CacheInvalidate(cacheName = CACHE_NAME_2) + public CompletableFuture cacheInvalidate(String key) { + cacheInvalidateInvocations++; + return CompletableFuture.completedFuture(null); + } + + @CacheInvalidateAll(cacheName = CACHE_NAME_1) + @CacheInvalidateAll(cacheName = CACHE_NAME_2) + public CompletableFuture cacheInvalidateAll() { + cacheInvalidateAllInvocations++; + return CompletableFuture.completedFuture(null); + } + + public int getCacheResultInvocations() { + return cacheResultInvocations; + } + + public int getCacheInvalidateInvocations() { + return cacheInvalidateInvocations; + } + + public int getCacheInvalidateAllInvocations() { + return cacheInvalidateAllInvocations; + } + } +} diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInterceptor.java b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInterceptor.java index 35b0ef596a627..fe86e0cfce9d0 100644 --- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInterceptor.java +++ b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInterceptor.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletionStage; import java.util.function.Supplier; import javax.inject.Inject; @@ -16,6 +17,7 @@ import io.quarkus.arc.runtime.InterceptorBindings; import io.quarkus.cache.Cache; +import io.quarkus.cache.CacheException; import io.quarkus.cache.CacheKey; import io.quarkus.cache.CacheManager; import io.quarkus.cache.CompositeCacheKey; @@ -27,6 +29,7 @@ public abstract class CacheInterceptor { private static final Logger LOGGER = Logger.getLogger(CacheInterceptor.class); private static final String PERFORMANCE_WARN_MSG = "Cache key resolution based on reflection calls. Please create a GitHub issue in the Quarkus repository, the maintainers might be able to improve your application performance."; + protected static final String UNHANDLED_ASYNC_RETURN_TYPE_MSG = "Unhandled async return type"; @Inject CacheManager cacheManager; @@ -135,7 +138,44 @@ protected Object getCacheKey(Cache cache, List cacheKeyParameterPositions } } - protected static boolean isUniReturnType(InvocationContext invocationContext) { - return Uni.class.isAssignableFrom(invocationContext.getMethod().getReturnType()); + protected static ReturnType determineReturnType(Class returnType) { + if (Uni.class.isAssignableFrom(returnType)) { + return ReturnType.Uni; + } + if (CompletionStage.class.isAssignableFrom(returnType)) { + return ReturnType.CompletionStage; + } + return ReturnType.NonAsync; + } + + protected Uni asyncInvocationResultToUni(Object invocationResult, ReturnType returnType) { + if (returnType == ReturnType.Uni) { + return (Uni) invocationResult; + } else if (returnType == ReturnType.CompletionStage) { + return Uni.createFrom().completionStage(new Supplier<>() { + @Override + public CompletionStage get() { + return (CompletionStage) invocationResult; + } + }); + } else { + throw new CacheException(new IllegalStateException(UNHANDLED_ASYNC_RETURN_TYPE_MSG)); + } + } + + protected Object createAsyncResult(Uni cacheValue, ReturnType returnType) { + if (returnType == ReturnType.Uni) { + return cacheValue; + } + if (returnType == ReturnType.CompletionStage) { + return cacheValue.subscribeAsCompletionStage(); + } + throw new CacheException(new IllegalStateException(UNHANDLED_ASYNC_RETURN_TYPE_MSG)); + } + + protected enum ReturnType { + NonAsync, + Uni, + CompletionStage } } diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInvalidateAllInterceptor.java b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInvalidateAllInterceptor.java index 8f6168335d229..4d504f62e391d 100644 --- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInvalidateAllInterceptor.java +++ b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInvalidateAllInterceptor.java @@ -27,21 +27,26 @@ public class CacheInvalidateAllInterceptor extends CacheInterceptor { public Object intercept(InvocationContext invocationContext) throws Exception { CacheInterceptionContext interceptionContext = getInterceptionContext(invocationContext, CacheInvalidateAll.class, false); + if (interceptionContext.getInterceptorBindings().isEmpty()) { // This should never happen. LOGGER.warn(INTERCEPTOR_BINDINGS_ERROR_MSG); return invocationContext.proceed(); - } else if (isUniReturnType(invocationContext)) { - return invalidateAllNonBlocking(invocationContext, interceptionContext); - } else { + } + ReturnType returnType = determineReturnType(invocationContext.getMethod().getReturnType()); + if (returnType == ReturnType.NonAsync) { return invalidateAllBlocking(invocationContext, interceptionContext); + + } else { + return invalidateAllNonBlocking(invocationContext, interceptionContext, returnType); } } private Object invalidateAllNonBlocking(InvocationContext invocationContext, - CacheInterceptionContext interceptionContext) { + CacheInterceptionContext interceptionContext, + ReturnType returnType) { LOGGER.trace("Invalidating all cache entries in a non-blocking way"); - return Multi.createFrom().iterable(interceptionContext.getInterceptorBindings()) + var uni = Multi.createFrom().iterable(interceptionContext.getInterceptorBindings()) .onItem().transformToUniAndMerge(new Function>() { @Override public Uni apply(CacheInvalidateAll binding) { @@ -53,12 +58,13 @@ public Uni apply(CacheInvalidateAll binding) { @Override public Uni apply(Object ignored) { try { - return (Uni) invocationContext.proceed(); + return asyncInvocationResultToUni(invocationContext.proceed(), returnType); } catch (Exception e) { throw new CacheException(e); } } }); + return createAsyncResult(uni, returnType); } private Object invalidateAllBlocking(InvocationContext invocationContext, diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInvalidateInterceptor.java b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInvalidateInterceptor.java index 548bc1d4a6023..62cf500d32af6 100644 --- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInvalidateInterceptor.java +++ b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInvalidateInterceptor.java @@ -32,17 +32,20 @@ public Object intercept(InvocationContext invocationContext) throws Exception { // This should never happen. LOGGER.warn(INTERCEPTOR_BINDINGS_ERROR_MSG); return invocationContext.proceed(); - } else if (isUniReturnType(invocationContext)) { - return invalidateNonBlocking(invocationContext, interceptionContext); - } else { + } + ReturnType returnType = determineReturnType(invocationContext.getMethod().getReturnType()); + if (returnType == ReturnType.NonAsync) { return invalidateBlocking(invocationContext, interceptionContext); + } else { + return invalidateNonBlocking(invocationContext, interceptionContext, returnType); } } private Object invalidateNonBlocking(InvocationContext invocationContext, - CacheInterceptionContext interceptionContext) { + CacheInterceptionContext interceptionContext, + ReturnType returnType) { LOGGER.trace("Invalidating cache entries in a non-blocking way"); - return Multi.createFrom().iterable(interceptionContext.getInterceptorBindings()) + var uni = Multi.createFrom().iterable(interceptionContext.getInterceptorBindings()) .onItem().transformToUniAndMerge(new Function>() { @Override public Uni apply(CacheInvalidate binding) { @@ -55,12 +58,13 @@ public Uni apply(CacheInvalidate binding) { @Override public Uni apply(Object ignored) { try { - return (Uni) invocationContext.proceed(); + return asyncInvocationResultToUni(invocationContext.proceed(), returnType); } catch (Exception e) { throw new CacheException(e); } } }); + return createAsyncResult(uni, returnType); } private Object invalidateBlocking(InvocationContext invocationContext, diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheResultInterceptor.java b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheResultInterceptor.java index 9eeb368866a2f..722595d0f1989 100644 --- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheResultInterceptor.java +++ b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheResultInterceptor.java @@ -50,7 +50,8 @@ public Object intercept(InvocationContext invocationContext) throws Throwable { LOGGER.debugf("Loading entry with key [%s] from cache [%s]", key, binding.cacheName()); try { - if (isUniReturnType(invocationContext)) { + ReturnType returnType = determineReturnType(invocationContext.getMethod().getReturnType()); + if (returnType != ReturnType.NonAsync) { Uni cacheValue = cache.get(key, new Function() { @Override public Object apply(Object k) { @@ -63,7 +64,7 @@ public Object apply(Object k) { public Uni apply(Object value) { if (value == UnresolvedUniValue.INSTANCE) { try { - return ((Uni) invocationContext.proceed()) + return asyncInvocationResultToUni(invocationContext.proceed(), returnType) .call(new Function>() { @Override public Uni apply(Object emittedValue) { @@ -81,14 +82,14 @@ public Uni apply(Object emittedValue) { } }); if (binding.lockTimeout() <= 0) { - return cacheValue; + return createAsyncResult(cacheValue, returnType); } - return cacheValue.ifNoItem().after(Duration.ofMillis(binding.lockTimeout())) + cacheValue = cacheValue.ifNoItem().after(Duration.ofMillis(binding.lockTimeout())) .recoverWithUni(new Supplier>() { @Override public Uni get() { try { - return (Uni) invocationContext.proceed(); + return asyncInvocationResultToUni(invocationContext.proceed(), returnType); } catch (CacheException e) { throw e; } catch (Exception e) { @@ -96,12 +97,14 @@ public Uni get() { } } }); - + return createAsyncResult(cacheValue, returnType); } else { Uni cacheValue = cache.get(key, new Function() { @Override public Object apply(Object k) { try { + LOGGER.debugf("Adding entry with key [%s] into cache [%s]", + key, binding.cacheName()); return invocationContext.proceed(); } catch (CacheException e) { throw e; @@ -137,4 +140,5 @@ public Object apply(Object k) { } } } + } diff --git a/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackProcessor.java b/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackProcessor.java index 0675e966ea5d3..f71b42db28538 100644 --- a/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackProcessor.java +++ b/extensions/container-image/container-image-buildpack/deployment/src/main/java/io/quarkus/container/image/buildpack/deployment/BuildpackProcessor.java @@ -42,6 +42,8 @@ public class BuildpackProcessor { private static final Logger log = Logger.getLogger(BuildpackProcessor.class); + private static final String QUARKUS_CONTAINER_IMAGE_BUILD = "QUARKUS_CONTAINER_IMAGE_BUILD"; + private static final String QUARKUS_CONTAINER_IMAGE_PUSH = "QUARKUS_CONTAINER_IMAGE_PUSH"; public static final String BUILDPACK = "buildpack"; @@ -160,11 +162,15 @@ private String runBuildpackBuild(BuildpackConfig buildpackConfig, String targetImageName = containerImage.getImage().toString(); log.debug("Using Destination image of " + targetImageName); - Map envMap = buildpackConfig.builderEnv; + Map envMap = new HashMap<>(buildpackConfig.builderEnv); if (!envMap.isEmpty()) { log.info("Using builder environment of " + envMap); } + // Let's explicitly disable build and push during the build to avoid inception style builds + envMap.put(QUARKUS_CONTAINER_IMAGE_BUILD, "false"); + envMap.put(QUARKUS_CONTAINER_IMAGE_PUSH, "false"); + log.info("Initiating Buildpack build"); Buildpack buildpack = Buildpack.builder() .addNewFileContent(dirs.get(ProjectDirs.ROOT).toFile()) @@ -176,9 +182,9 @@ private String runBuildpackBuild(BuildpackConfig buildpackConfig, .accept(BuildpackBuilder.class, b -> { if (isNativeBuild) { - buildpackConfig.nativeBuilderImage.ifPresent(i -> b.withBuildImage(i)); + buildpackConfig.nativeBuilderImage.ifPresent(i -> b.withBuilderImage(i)); } else { - buildpackConfig.jvmBuilderImage.ifPresent(i -> b.withBuildImage(i)); + buildpackConfig.jvmBuilderImage.ifPresent(i -> b.withBuilderImage(i)); } if (buildpackConfig.runImage.isPresent()) { @@ -191,6 +197,11 @@ private String runBuildpackBuild(BuildpackConfig buildpackConfig, } }).build(); + + if (buildpack.getExitCode() != 0) { + throw new IllegalStateException("Buildpack build failed"); + } + log.info("Buildpack build complete"); if (containerImageConfig.isPushExplicitlyEnabled() || pushRequest.isPresent()) { if (!containerImageConfig.registry.isPresent()) { @@ -203,12 +214,11 @@ private String runBuildpackBuild(BuildpackConfig buildpackConfig, log.info("Pushing image to " + authConfig.getRegistryAddress()); Stream.concat(Stream.of(containerImage.getImage()), containerImage.getAdditionalImageTags().stream()).forEach(i -> { - ResultCallback.Adapter adapter = new ResultCallback.Adapter<>(); - buildpack.getDockerClient().pushImageCmd(i).exec(adapter); + ResultCallback.Adapter callback = buildpack.getDockerClient().pushImageCmd(i).start(); try { - adapter.awaitCompletion(); + callback.awaitCompletion(); log.info("Push complete"); - } catch (InterruptedException e) { + } catch (Exception e) { throw new IllegalStateException(e); } }); diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java index 86cd209ca2480..99d5420a51d92 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java @@ -4,6 +4,8 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -51,4 +53,50 @@ public class DockerConfig { */ @ConfigItem(defaultValue = "docker") public String executableName; -} + + /** + * Configuration for Docker Buildx options + */ + @ConfigItem + @ConfigDocSection + public DockerBuildxConfig buildx; + + /** + * Configuration for Docker Buildx options. These are only relevant if using Docker Buildx + * (https://docs.docker.com/buildx/working-with-buildx/#build-multi-platform-images) to build multi-platform (or + * cross-platform) + * images. + * If any of these configurations are set, it will add {@code buildx} to the {@code executableName}. + */ + @ConfigGroup + public static class DockerBuildxConfig { + /** + * Which platform(s) to target during the build. See + * https://docs.docker.com/engine/reference/commandline/buildx_build/#platform + */ + @ConfigItem + public Optional> platform; + + /** + * Sets the export action for the build result. See + * https://docs.docker.com/engine/reference/commandline/buildx_build/#output. Note that any filesystem paths need to be + * absolute paths, + * not relative from where the command is executed from. + */ + @ConfigItem + public Optional output; + + /** + * Set type of progress output ({@code auto}, {@code plain}, {@code tty}). Use {@code plain} to show container output + * (default “{@code auto}”). See https://docs.docker.com/engine/reference/commandline/buildx_build/#progress + */ + @ConfigItem + public Optional progress; + + boolean useBuildx() { + return platform.filter(p -> !p.isEmpty()).isPresent() || + output.isPresent() || + progress.isPresent(); + } + } +} \ No newline at end of file diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java index 1f470c572d3bb..29bc53a8d1cd7 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java @@ -20,6 +20,7 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +import java.util.stream.Stream; import org.jboss.logging.Logger; @@ -30,10 +31,10 @@ import io.quarkus.container.spi.ContainerImageBuilderBuildItem; import io.quarkus.container.spi.ContainerImageInfoBuildItem; import io.quarkus.container.spi.ContainerImagePushRequestBuildItem; -import io.quarkus.deployment.IsDockerWorking; import io.quarkus.deployment.IsNormalNotRemoteDev; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.deployment.pkg.builditem.AppCDSResultBuildItem; import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; @@ -55,8 +56,6 @@ public class DockerProcessor { private static final String DOCKER_DIRECTORY_NAME = "docker"; static final String DOCKER_CONTAINER_IMAGE_NAME = "docker"; - private final IsDockerWorking isDockerWorking = new IsDockerWorking(); - @BuildStep public AvailableContainerImageExtensionBuildItem availability() { return new AvailableContainerImageExtensionBuildItem(DOCKER); @@ -64,6 +63,7 @@ public AvailableContainerImageExtensionBuildItem availability() { @BuildStep(onlyIf = { IsNormalNotRemoteDev.class, DockerBuild.class }, onlyIfNot = NativeBuild.class) public void dockerBuildFromJar(DockerConfig dockerConfig, + DockerStatusBuildItem dockerStatusBuildItem, ContainerImageConfig containerImageConfig, OutputTargetBuildItem out, ContainerImageInfoBuildItem containerImageInfo, @@ -83,7 +83,7 @@ public void dockerBuildFromJar(DockerConfig dockerConfig, return; } - if (!isDockerWorking.getAsBoolean()) { + if (!dockerStatusBuildItem.isDockerAvailable()) { throw new RuntimeException("Unable to build docker image. Please check your docker installation"); } @@ -116,6 +116,7 @@ public void dockerBuildFromJar(DockerConfig dockerConfig, @BuildStep(onlyIf = { IsNormalNotRemoteDev.class, NativeBuild.class, DockerBuild.class }) public void dockerBuildFromNativeImage(DockerConfig dockerConfig, + DockerStatusBuildItem dockerStatusBuildItem, ContainerImageConfig containerImageConfig, ContainerImageInfoBuildItem containerImage, Optional buildRequest, @@ -134,7 +135,7 @@ public void dockerBuildFromNativeImage(DockerConfig dockerConfig, return; } - if (!isDockerWorking.getAsBoolean()) { + if (!dockerStatusBuildItem.isDockerAvailable()) { throw new RuntimeException("Unable to build docker image. Please check your docker installation"); } @@ -162,8 +163,34 @@ private String createContainerImage(ContainerImageConfig containerImageConfig, D OutputTargetBuildItem out, ImageIdReader reader, boolean forNative, boolean pushRequested, PackageConfig packageConfig) { + var useBuildx = dockerConfig.buildx.useBuildx(); + var pushImages = pushRequested || containerImageConfig.isPushExplicitlyEnabled(); + + // useBuildx: Whether or not any of the buildx parameters are set + // + // pushImages: Whether or not the user requested the built images to be pushed to a registry + // Pushing images is different based on if you're using buildx or not. + // If not using any of the buildx params (useBuildx == false), then the flow is as it was before: + // + // 1) Build the image (docker build) + // 2) Apply any tags (docker tag) + // 3) Push the image and all tags (docker push) + // + // If using any of the buildx options (useBuildx == true), the tagging & pushing happens + // as part of the 'docker buildx build' command via the added -t and --push params (see the getDockerArgs method). + // + // This is because when using buildx with more than one platform, the resulting images are not loaded into 'docker images'. + // Therefore, a docker tag or docker push will not work after a docker build. + DockerfilePaths dockerfilePaths = getDockerfilePaths(dockerConfig, forNative, packageConfig, out); - String[] dockerArgs = getDockerArgs(containerImageInfo.getImage(), dockerfilePaths, containerImageConfig, dockerConfig); + String[] dockerArgs = getDockerArgs(containerImageInfo.getImage(), dockerfilePaths, containerImageConfig, dockerConfig, + containerImageInfo, pushImages); + + if (useBuildx && pushImages) { + // Needed because buildx will push all the images in a single step + loginToRegistryIfNeeded(containerImageConfig, containerImageInfo, dockerConfig); + } + log.infof("Executing the following command to build docker image: '%s %s'", dockerConfig.executableName, String.join(" ", dockerArgs)); boolean buildSuccessful = ExecUtil.exec(out.getOutputDirectory().toFile(), reader, dockerConfig.executableName, @@ -172,61 +199,109 @@ private String createContainerImage(ContainerImageConfig containerImageConfig, D throw dockerException(dockerArgs); } - log.infof("Built container image %s (%s)\n", containerImageInfo.getImage(), reader.getImageId()); - - if (!containerImageInfo.getAdditionalImageTags().isEmpty()) { - createAdditionalTags(containerImageInfo.getImage(), containerImageInfo.getAdditionalImageTags(), dockerConfig); - } - - if (pushRequested || containerImageConfig.isPushExplicitlyEnabled()) { - String registry = "docker.io"; - if (!containerImageInfo.getRegistry().isPresent()) { - log.info("No container image registry was set, so 'docker.io' will be used"); - } else { - registry = containerImageInfo.getRegistry().get(); - } - // Check if we need to login first - if (containerImageConfig.username.isPresent() && containerImageConfig.password.isPresent()) { - boolean loginSuccessful = ExecUtil.exec(dockerConfig.executableName, "login", registry, "-u", - containerImageConfig.username.get(), - "-p" + containerImageConfig.password.get()); - if (!loginSuccessful) { - throw dockerException(new String[] { "-u", containerImageConfig.username.get(), "-p", "********" }); - } + dockerConfig.buildx.platform + .filter(platform -> platform.size() > 1) + .ifPresentOrElse( + platform -> log.infof("Built container image %s (%s platform(s))\n", containerImageInfo.getImage(), + String.join(",", platform)), + () -> log.infof("Built container image %s (%s)\n", containerImageInfo.getImage(), reader.getImageId())); + + if (!useBuildx) { + // If we didn't use buildx, now we need to process any tags + if (!containerImageInfo.getAdditionalImageTags().isEmpty()) { + createAdditionalTags(containerImageInfo.getImage(), containerImageInfo.getAdditionalImageTags(), dockerConfig); } - List imagesToPush = new ArrayList<>(containerImageInfo.getAdditionalImageTags()); - imagesToPush.add(containerImageInfo.getImage()); - for (String imageToPush : imagesToPush) { - pushImage(imageToPush, dockerConfig); + if (pushImages) { + // If not using buildx, push the images + loginToRegistryIfNeeded(containerImageConfig, containerImageInfo, dockerConfig); + + Stream.concat(containerImageInfo.getAdditionalTags().stream(), Stream.of(containerImageInfo.getImage())) + .forEach(imageToPush -> pushImage(imageToPush, dockerConfig)); } } return containerImageInfo.getImage(); } + private void loginToRegistryIfNeeded(ContainerImageConfig containerImageConfig, + ContainerImageInfoBuildItem containerImageInfo, DockerConfig dockerConfig) { + var registry = containerImageInfo.getRegistry() + .orElseGet(() -> { + log.info("No container image registry was set, so 'docker.io' will be used"); + return "docker.io"; + }); + + // Check if we need to login first + if (containerImageConfig.username.isPresent() && containerImageConfig.password.isPresent()) { + boolean loginSuccessful = ExecUtil.exec(dockerConfig.executableName, "login", registry, "-u", + containerImageConfig.username.get(), + "-p" + containerImageConfig.password.get()); + if (!loginSuccessful) { + throw dockerException(new String[] { "-u", containerImageConfig.username.get(), "-p", "********" }); + } + } + } + private String[] getDockerArgs(String image, DockerfilePaths dockerfilePaths, ContainerImageConfig containerImageConfig, - DockerConfig dockerConfig) { + DockerConfig dockerConfig, ContainerImageInfoBuildItem containerImageInfo, boolean pushImages) { List dockerArgs = new ArrayList<>(6 + dockerConfig.buildArgs.size()); - dockerArgs.addAll(Arrays.asList("build", "-f", dockerfilePaths.getDockerfilePath().toAbsolutePath().toString())); - for (Map.Entry entry : dockerConfig.buildArgs.entrySet()) { - dockerArgs.addAll(Arrays.asList("--build-arg", entry.getKey() + "=" + entry.getValue())); - } - for (Map.Entry entry : containerImageConfig.labels.entrySet()) { - dockerArgs.addAll(Arrays.asList("--label", String.format("%s=%s", entry.getKey(), entry.getValue()))); - } - if (dockerConfig.cacheFrom.isPresent()) { - List cacheFrom = dockerConfig.cacheFrom.get(); - if (!cacheFrom.isEmpty()) { - dockerArgs.add("--cache-from"); - dockerArgs.add(String.join(",", cacheFrom)); + var useBuildx = dockerConfig.buildx.useBuildx(); + + if (useBuildx) { + // Check the executable. If not 'docker', then fail the build + if (!DOCKER.equals(dockerConfig.executableName)) { + throw new IllegalArgumentException( + String.format( + "The 'buildx' properties are specific to 'executable-name=docker' and can not be used with the '%s' executable name. Either remove the `buildx` properties or the `executable-name` property.", + dockerConfig.executableName)); } + + dockerArgs.add("buildx"); } - if (dockerConfig.network.isPresent()) { + + dockerArgs.addAll(Arrays.asList("build", "-f", dockerfilePaths.getDockerfilePath().toAbsolutePath().toString())); + dockerConfig.buildx.platform + .filter(platform -> !platform.isEmpty()) + .ifPresent(platform -> { + dockerArgs.add("--platform"); + dockerArgs.add(String.join(",", platform)); + + if (platform.size() == 1) { + // Buildx only supports loading the image to the docker system if there is only 1 image + dockerArgs.add("--load"); + } + }); + dockerConfig.buildx.progress.ifPresent(progress -> dockerArgs.addAll(List.of("--progress", progress))); + dockerConfig.buildx.output.ifPresent(output -> dockerArgs.addAll(List.of("--output", output))); + dockerConfig.buildArgs + .forEach((key, value) -> dockerArgs.addAll(Arrays.asList("--build-arg", String.format("%s=%s", key, value)))); + containerImageConfig.labels + .forEach((key, value) -> dockerArgs.addAll(Arrays.asList("--label", String.format("%s=%s", key, value)))); + dockerConfig.cacheFrom + .filter(cacheFrom -> !cacheFrom.isEmpty()) + .ifPresent(cacheFrom -> { + dockerArgs.add("--cache-from"); + dockerArgs.add(String.join(",", cacheFrom)); + }); + dockerConfig.network.ifPresent(network -> { dockerArgs.add("--network"); - dockerArgs.add(dockerConfig.network.get()); - } + dockerArgs.add(network); + }); dockerArgs.addAll(Arrays.asList("-t", image)); + + if (useBuildx) { + // When using buildx for multi-arch images, it wants to push in a single step + // 1) Create all the additional tags + containerImageInfo.getAdditionalImageTags() + .forEach(additionalImageTag -> dockerArgs.addAll(List.of("-t", additionalImageTag))); + + if (pushImages) { + // 2) Enable the --push flag + dockerArgs.add("--push"); + } + } + dockerArgs.add(dockerfilePaths.getDockerExecutionPath().toAbsolutePath().toString()); return dockerArgs.toArray(new String[0]); } @@ -399,4 +474,4 @@ public Path getDockerExecutionPath() { } } -} +} \ No newline at end of file diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/PlatformHelper.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/PlatformHelper.java old mode 100755 new mode 100644 diff --git a/extensions/datasource/deployment-spi/src/main/java/io/quarkus/datasource/deployment/spi/DevServicesDatasourceContainerConfig.java b/extensions/datasource/deployment-spi/src/main/java/io/quarkus/datasource/deployment/spi/DevServicesDatasourceContainerConfig.java new file mode 100644 index 0000000000000..7061c743b3bca --- /dev/null +++ b/extensions/datasource/deployment-spi/src/main/java/io/quarkus/datasource/deployment/spi/DevServicesDatasourceContainerConfig.java @@ -0,0 +1,39 @@ +package io.quarkus.datasource.deployment.spi; + +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +public class DevServicesDatasourceContainerConfig { + + private final Optional imageName; + private final Map containerProperties; + private final Map additionalJdbcUrlProperties; + private final OptionalInt fixedExposedPort; + + public DevServicesDatasourceContainerConfig(Optional imageName, + Map containerProperties, + Map additionalJdbcUrlProperties, + OptionalInt port) { + this.imageName = imageName; + this.containerProperties = containerProperties; + this.additionalJdbcUrlProperties = additionalJdbcUrlProperties; + this.fixedExposedPort = port; + } + + public Optional getImageName() { + return imageName; + } + + public Map getContainerProperties() { + return containerProperties; + } + + public Map getAdditionalJdbcUrlProperties() { + return additionalJdbcUrlProperties; + } + + public OptionalInt getFixedExposedPort() { + return fixedExposedPort; + } +} diff --git a/extensions/datasource/deployment-spi/src/main/java/io/quarkus/datasource/deployment/spi/DevServicesDatasourceProvider.java b/extensions/datasource/deployment-spi/src/main/java/io/quarkus/datasource/deployment/spi/DevServicesDatasourceProvider.java index 25bd51c5571ed..05e3dffee7b60 100644 --- a/extensions/datasource/deployment-spi/src/main/java/io/quarkus/datasource/deployment/spi/DevServicesDatasourceProvider.java +++ b/extensions/datasource/deployment-spi/src/main/java/io/quarkus/datasource/deployment/spi/DevServicesDatasourceProvider.java @@ -2,9 +2,7 @@ import java.io.Closeable; import java.time.Duration; -import java.util.Map; import java.util.Optional; -import java.util.OptionalInt; import io.quarkus.runtime.LaunchMode; @@ -12,10 +10,9 @@ public interface DevServicesDatasourceProvider { RunningDevServicesDatasource startDatabase(Optional username, Optional password, Optional datasourceName, - Optional imageName, - Map containerProperties, - Map additionalJdbcUrlProperties, - OptionalInt port, LaunchMode launchMode, Optional startupTimeout); + DevServicesDatasourceContainerConfig devServicesDatasourceContainerConfig, + LaunchMode launchMode, + Optional startupTimeout); default boolean isDockerRequired() { return true; 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 cb85cc34041f3..73d68e608977a 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 @@ -15,18 +15,19 @@ import io.quarkus.datasource.deployment.spi.DefaultDataSourceDbKindBuildItem; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceConfigurationHandlerBuildItem; +import io.quarkus.datasource.deployment.spi.DevServicesDatasourceContainerConfig; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProvider; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceResultBuildItem; import io.quarkus.datasource.runtime.DataSourceBuildTimeConfig; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; -import io.quarkus.deployment.IsDockerWorking; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.console.ConsoleInstalledBuildItem; import io.quarkus.deployment.console.StartupLogCompressor; @@ -45,10 +46,9 @@ public class DevServicesDatasourceProcessor { static volatile boolean first = true; - private final IsDockerWorking isDockerWorking = new IsDockerWorking(true); - @BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) DevServicesDatasourceResultBuildItem launchDatabases(CurateOutcomeBuildItem curateOutcomeBuildItem, + DockerStatusBuildItem dockerStatusBuildItem, List installedDrivers, List devDBProviders, DataSourcesBuildTimeConfig dataSourceBuildTimeConfig, @@ -127,7 +127,8 @@ DevServicesDatasourceResultBuildItem launchDatabases(CurateOutcomeBuildItem cura !dataSourceBuildTimeConfig.namedDataSources.isEmpty(), devDBProviderMap, dataSourceBuildTimeConfig.defaultDataSource, - configHandlersByDbType, propertiesMap, launchMode.getLaunchMode(), consoleInstalledBuildItem, + configHandlersByDbType, propertiesMap, + dockerStatusBuildItem, launchMode.getLaunchMode(), consoleInstalledBuildItem, loggingSetupBuildItem, globalDevServicesConfig); if (defaultDevService != null) { runningDevServices.add(defaultDevService); @@ -137,6 +138,7 @@ DevServicesDatasourceResultBuildItem launchDatabases(CurateOutcomeBuildItem cura RunningDevService namedDevService = startDevDb(entry.getKey(), curateOutcomeBuildItem, installedDrivers, true, devDBProviderMap, entry.getValue(), configHandlersByDbType, propertiesMap, + dockerStatusBuildItem, launchMode.getLaunchMode(), consoleInstalledBuildItem, loggingSetupBuildItem, globalDevServicesConfig); if (namedDevService != null) { runningDevServices.add(namedDevService); @@ -187,6 +189,7 @@ private RunningDevService startDevDb(String dbName, Map devDBProviders, DataSourceBuildTimeConfig dataSourceBuildTimeConfig, Map> configurationHandlerBuildItems, Map propertiesMap, + DockerStatusBuildItem dockerStatusBuildItem, LaunchMode launchMode, Optional consoleInstalledBuildItem, LoggingSetupBuildItem loggingSetupBuildItem, GlobalDevServicesConfig globalDevServicesConfig) { boolean explicitlyDisabled = !(dataSourceBuildTimeConfig.devservices.enabled.orElse(true)); @@ -232,7 +235,7 @@ private RunningDevService startDevDb(String dbName, } String prettyName = dbName == null ? "the default datasource" : " datasource '" + dbName + "'"; - if (devDbProvider.isDockerRequired() && !isDockerWorking.getAsBoolean()) { + if (devDbProvider.isDockerRequired() && !dockerStatusBuildItem.isDockerAvailable()) { String message = "Please configure the datasource URL for " + prettyName + " or ensure the Docker daemon is up and running."; @@ -259,13 +262,17 @@ private RunningDevService startDevDb(String dbName, prefix = prefix + dbName + "."; } + DevServicesDatasourceContainerConfig containerConfig = new DevServicesDatasourceContainerConfig( + dataSourceBuildTimeConfig.devservices.imageName, + dataSourceBuildTimeConfig.devservices.containerProperties, + dataSourceBuildTimeConfig.devservices.properties, + dataSourceBuildTimeConfig.devservices.port); + DevServicesDatasourceProvider.RunningDevServicesDatasource datasource = devDbProvider .startDatabase(ConfigProvider.getConfig().getOptionalValue(prefix + "username", String.class), ConfigProvider.getConfig().getOptionalValue(prefix + "password", String.class), - Optional.ofNullable(dbName), dataSourceBuildTimeConfig.devservices.imageName, - dataSourceBuildTimeConfig.devservices.containerProperties, - dataSourceBuildTimeConfig.devservices.properties, - dataSourceBuildTimeConfig.devservices.port, launchMode, globalDevServicesConfig.timeout); + Optional.ofNullable(dbName), containerConfig, + launchMode, globalDevServicesConfig.timeout); propertiesMap.put(prefix + "db-kind", dataSourceBuildTimeConfig.dbKind.orElse(null)); String devServicesPrefix = prefix + "devservices."; diff --git a/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ContainerShutdownCloseable.java b/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ContainerShutdownCloseable.java new file mode 100644 index 0000000000000..60b1b8636dced --- /dev/null +++ b/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ContainerShutdownCloseable.java @@ -0,0 +1,51 @@ +package io.quarkus.devservices.common; + +import java.io.Closeable; +import java.util.Objects; + +import org.jboss.logging.Logger; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.TestcontainersConfiguration; + +/** + * Helper to define the stop strategy for containers created by DevServices. + * In particular we don't want to actually stop the containers when they + * have been flagged for reuse, and when the Testcontainers configuration + * has been explicitly set to allow container reuse. + * To enable reuse, ass {@literal testcontainers.reuse.enable=true} in your + * {@literal .testcontainers.properties} file, to be stored in your home. + * + * @see Testcontainers Configuration. + */ +public final class ContainerShutdownCloseable implements Closeable { + + private static final Logger LOG = Logger.getLogger(ContainerShutdownCloseable.class); + + private final GenericContainer container; + private final String friendlyServiceName; + + /** + * @param container the container to be eventually closed + * @param friendlyServiceName for logging purposes + */ + public ContainerShutdownCloseable(GenericContainer container, String friendlyServiceName) { + Objects.requireNonNull(container); + Objects.requireNonNull(friendlyServiceName); + this.container = container; + this.friendlyServiceName = friendlyServiceName; + } + + @Override + public void close() { + if (TestcontainersConfiguration.getInstance().environmentSupportsReuse() + && container.isShouldBeReused()) { + LOG.infof( + "Dev Services for %s is no longer needed by this Quarkus instance, but is not shut down as 'testcontainers.reuse.enable' is enabled in your Testcontainers configuration file", + friendlyServiceName); + } else { + container.stop(); + LOG.infof("Dev Services for %s shut down.", this.friendlyServiceName); + } + } + +} diff --git a/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java b/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java index 6fb9218f41c2a..8db17dc91d14c 100644 --- a/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java +++ b/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java @@ -1,10 +1,7 @@ package io.quarkus.devservices.db2.deployment; -import java.io.Closeable; -import java.io.IOException; import java.time.Duration; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.OptionalInt; @@ -13,11 +10,13 @@ import org.testcontainers.utility.DockerImageName; import io.quarkus.datasource.common.runtime.DatabaseKind; +import io.quarkus.datasource.deployment.spi.DevServicesDatasourceContainerConfig; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProvider; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; import io.quarkus.devservices.common.ConfigureUtil; +import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.runtime.LaunchMode; public class DB2DevServicesProcessor { @@ -30,16 +29,17 @@ DevServicesDatasourceProviderBuildItem setupDB2( return new DevServicesDatasourceProviderBuildItem(DatabaseKind.DB2, new DevServicesDatasourceProvider() { @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, - Optional datasourceName, Optional imageName, - Map containerProperties, Map additionalJdbcUrlProperties, - OptionalInt fixedExposedPort, LaunchMode launchMode, Optional startupTimeout) { - QuarkusDb2Container container = new QuarkusDb2Container(imageName, fixedExposedPort, + Optional datasourceName, DevServicesDatasourceContainerConfig containerConfig, + LaunchMode launchMode, Optional startupTimeout) { + QuarkusDb2Container container = new QuarkusDb2Container(containerConfig.getImageName(), + containerConfig.getFixedExposedPort(), !devServicesSharedNetworkBuildItem.isEmpty()); startupTimeout.ifPresent(container::withStartupTimeout); container.withPassword(password.orElse("quarkus")) .withUsername(username.orElse("quarkus")) - .withDatabaseName(datasourceName.orElse("default")); - additionalJdbcUrlProperties.forEach(container::withUrlParam); + .withDatabaseName(datasourceName.orElse("default")) + .withReuse(true); + containerConfig.getAdditionalJdbcUrlProperties().forEach(container::withUrlParam); container.start(); LOG.info("Dev Services for IBM Db2 started."); @@ -48,14 +48,7 @@ public RunningDevServicesDatasource startDatabase(Optional username, Opt container.getEffectiveJdbcUrl(), container.getUsername(), container.getPassword(), - new Closeable() { - @Override - public void close() throws IOException { - container.stop(); - - LOG.info("Dev Services for IBM Db2 shut down."); - } - }); + new ContainerShutdownCloseable(container, "IBM Db2")); } }); } diff --git a/extensions/devservices/deployment/src/main/java/io/quarkus/devservices/deployment/DevServicesProcessor.java b/extensions/devservices/deployment/src/main/java/io/quarkus/devservices/deployment/DevServicesProcessor.java index caf05ad26ef7e..a7a5a035961c7 100644 --- a/extensions/devservices/deployment/src/main/java/io/quarkus/devservices/deployment/DevServicesProcessor.java +++ b/extensions/devservices/deployment/src/main/java/io/quarkus/devservices/deployment/DevServicesProcessor.java @@ -19,12 +19,12 @@ import com.github.dockerjava.api.model.ContainerNetworkSettings; import io.quarkus.deployment.IsDevelopment; -import io.quarkus.deployment.IsDockerWorking; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.ConsoleCommandBuildItem; import io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.console.ConsoleCommand; import io.quarkus.deployment.console.ConsoleStateManager; @@ -36,20 +36,19 @@ public class DevServicesProcessor { private static final String EXEC_FORMAT = "docker exec -it %s /bin/bash"; - private final IsDockerWorking isDockerWorking = new IsDockerWorking(true); - static volatile ConsoleStateManager.ConsoleContext context; static volatile boolean logForwardEnabled = false; static Map containerLogForwarders = new HashMap<>(); @BuildStep(onlyIf = { IsDevelopment.class }) public List config( + DockerStatusBuildItem dockerStatusBuildItem, BuildProducer commandBuildItemBuildProducer, LaunchModeBuildItem launchModeBuildItem, Optional devServicesLauncherConfig, List devServicesResults) { - List serviceDescriptions = buildServiceDescriptions(devServicesResults, - devServicesLauncherConfig); + List serviceDescriptions = buildServiceDescriptions( + dockerStatusBuildItem, devServicesResults, devServicesLauncherConfig); for (DevServiceDescriptionBuildItem devService : serviceDescriptions) { if (devService.hasContainerInfo()) { @@ -72,8 +71,8 @@ public List config( } context.reset( new ConsoleCommand('c', "Show dev services containers", null, () -> { - List descriptions = buildServiceDescriptions(devServicesResults, - devServicesLauncherConfig); + List descriptions = buildServiceDescriptions( + dockerStatusBuildItem, devServicesResults, devServicesLauncherConfig); StringBuilder builder = new StringBuilder(); builder.append("\n\n") .append(RED + "==" + RESET + " " + UNDERLINE + "Dev Services" + NO_UNDERLINE) @@ -91,14 +90,16 @@ public List config( return serviceDescriptions; } - private List buildServiceDescriptions(List devServicesResults, + private List buildServiceDescriptions( + DockerStatusBuildItem dockerStatusBuildItem, + List devServicesResults, Optional devServicesLauncherConfig) { // Fetch container infos Set containerIds = devServicesResults.stream() .map(DevServicesResultBuildItem::getContainerId) .filter(Objects::nonNull) .collect(Collectors.toSet()); - Map containerInfos = fetchContainerInfos(containerIds); + Map containerInfos = fetchContainerInfos(dockerStatusBuildItem, containerIds); // Build descriptions Set configKeysFromDevServices = new HashSet<>(); List descriptions = new ArrayList<>(); @@ -121,8 +122,9 @@ private List buildServiceDescriptions(List fetchContainerInfos(Set containerIds) { - if (containerIds.isEmpty() || !isDockerWorking.getAsBoolean()) { + private Map fetchContainerInfos(DockerStatusBuildItem dockerStatusBuildItem, + Set containerIds) { + if (containerIds.isEmpty() || !dockerStatusBuildItem.isDockerAvailable()) { return Collections.emptyMap(); } return DockerClientFactory.lazyClient().listContainersCmd() diff --git a/extensions/devservices/derby/src/main/java/io/quarkus/devservices/derby/deployment/DerbyDevServicesProcessor.java b/extensions/devservices/derby/src/main/java/io/quarkus/devservices/derby/deployment/DerbyDevServicesProcessor.java index 4a2389fe066cf..0b75127ef0792 100644 --- a/extensions/devservices/derby/src/main/java/io/quarkus/devservices/derby/deployment/DerbyDevServicesProcessor.java +++ b/extensions/devservices/derby/src/main/java/io/quarkus/devservices/derby/deployment/DerbyDevServicesProcessor.java @@ -7,12 +7,12 @@ import java.time.Duration; import java.util.Map; import java.util.Optional; -import java.util.OptionalInt; import org.apache.derby.drda.NetworkServerControl; import org.jboss.logging.Logger; import io.quarkus.datasource.common.runtime.DatabaseKind; +import io.quarkus.datasource.deployment.spi.DevServicesDatasourceContainerConfig; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProvider; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; @@ -30,11 +30,11 @@ DevServicesDatasourceProviderBuildItem setupDerby() { return new DevServicesDatasourceProviderBuildItem(DatabaseKind.DERBY, new DevServicesDatasourceProvider() { @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, - Optional datasourceName, Optional imageName, - Map containerProperties, Map additionalJdbcUrlProperties, - OptionalInt fixedExposedPort, LaunchMode launchMode, Optional startupTimeout) { + Optional datasourceName, DevServicesDatasourceContainerConfig containerConfig, + LaunchMode launchMode, Optional startupTimeout) { try { - int port = fixedExposedPort.isPresent() ? fixedExposedPort.getAsInt() + int port = containerConfig.getFixedExposedPort().isPresent() + ? containerConfig.getFixedExposedPort().getAsInt() : 1527 + (launchMode == LaunchMode.TEST ? 0 : 1); NetworkServerControl server = new NetworkServerControl(InetAddress.getByName("localhost"), port); server.start(new PrintWriter(System.out)); @@ -58,7 +58,7 @@ public RunningDevServicesDatasource startDatabase(Optional username, Opt LOG.info("Dev Services for Derby started."); StringBuilder additionalArgs = new StringBuilder(); - for (Map.Entry i : additionalJdbcUrlProperties.entrySet()) { + for (Map.Entry i : containerConfig.getAdditionalJdbcUrlProperties().entrySet()) { additionalArgs.append(";"); additionalArgs.append(i.getKey()); additionalArgs.append("="); diff --git a/extensions/devservices/h2/src/main/java/io/quarkus/devservices/h2/deployment/H2DevServicesProcessor.java b/extensions/devservices/h2/src/main/java/io/quarkus/devservices/h2/deployment/H2DevServicesProcessor.java index 9b4fb7a7200aa..9c046786e658b 100644 --- a/extensions/devservices/h2/src/main/java/io/quarkus/devservices/h2/deployment/H2DevServicesProcessor.java +++ b/extensions/devservices/h2/src/main/java/io/quarkus/devservices/h2/deployment/H2DevServicesProcessor.java @@ -9,12 +9,12 @@ import java.time.Duration; import java.util.Map; import java.util.Optional; -import java.util.OptionalInt; import org.h2.tools.Server; import org.jboss.logging.Logger; import io.quarkus.datasource.common.runtime.DatabaseKind; +import io.quarkus.datasource.deployment.spi.DevServicesDatasourceContainerConfig; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProvider; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; @@ -29,17 +29,18 @@ DevServicesDatasourceProviderBuildItem setupH2() { return new DevServicesDatasourceProviderBuildItem(DatabaseKind.H2, new DevServicesDatasourceProvider() { @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, - Optional datasourceName, Optional imageName, - Map containerProperties, Map additionalJdbcUrlProperties, - OptionalInt port, LaunchMode launchMode, Optional startupTimeout) { + Optional datasourceName, DevServicesDatasourceContainerConfig containerConfig, + LaunchMode launchMode, Optional startupTimeout) { try { final Server tcpServer = Server.createTcpServer("-tcpPort", - port.isPresent() ? String.valueOf(port.getAsInt()) : "0", + containerConfig.getFixedExposedPort().isPresent() + ? String.valueOf(containerConfig.getFixedExposedPort().getAsInt()) + : "0", "-ifNotExists"); tcpServer.start(); StringBuilder additionalArgs = new StringBuilder(); - for (Map.Entry i : additionalJdbcUrlProperties.entrySet()) { + for (Map.Entry i : containerConfig.getAdditionalJdbcUrlProperties().entrySet()) { additionalArgs.append(";"); additionalArgs.append(i.getKey()); additionalArgs.append("="); 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 d0d6bc62f6428..4395801bbd20a 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 @@ -1,10 +1,7 @@ package io.quarkus.devservices.mariadb.deployment; -import java.io.Closeable; -import java.io.IOException; import java.time.Duration; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.OptionalInt; @@ -13,11 +10,13 @@ import org.testcontainers.utility.DockerImageName; import io.quarkus.datasource.common.runtime.DatabaseKind; +import io.quarkus.datasource.deployment.spi.DevServicesDatasourceContainerConfig; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProvider; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; import io.quarkus.devservices.common.ConfigureUtil; +import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.runtime.LaunchMode; public class MariaDBDevServicesProcessor { @@ -33,21 +32,23 @@ DevServicesDatasourceProviderBuildItem setupMariaDB( return new DevServicesDatasourceProviderBuildItem(DatabaseKind.MARIADB, new DevServicesDatasourceProvider() { @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, - Optional datasourceName, Optional imageName, - Map containerProperties, Map additionalJdbcUrlProperties, - OptionalInt fixedExposedPort, LaunchMode launchMode, Optional startupTimeout) { - QuarkusMariaDBContainer container = new QuarkusMariaDBContainer(imageName, fixedExposedPort, + Optional datasourceName, DevServicesDatasourceContainerConfig containerConfig, + LaunchMode launchMode, Optional startupTimeout) { + QuarkusMariaDBContainer container = new QuarkusMariaDBContainer(containerConfig.getImageName(), + containerConfig.getFixedExposedPort(), !devServicesSharedNetworkBuildItem.isEmpty()); startupTimeout.ifPresent(container::withStartupTimeout); container.withPassword(password.orElse("quarkus")) .withUsername(username.orElse("quarkus")) - .withDatabaseName(datasourceName.orElse("default")); + .withDatabaseName(datasourceName.orElse("default")) + .withReuse(true); - if (containerProperties.containsKey(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME)) { - container.withConfigurationOverride(containerProperties.get(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME)); + if (containerConfig.getContainerProperties().containsKey(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME)) { + container.withConfigurationOverride( + containerConfig.getContainerProperties().get(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME)); } - additionalJdbcUrlProperties.forEach(container::withUrlParam); + containerConfig.getAdditionalJdbcUrlProperties().forEach(container::withUrlParam); container.start(); LOG.info("Dev Services for MariaDB started."); @@ -56,14 +57,7 @@ public RunningDevServicesDatasource startDatabase(Optional username, Opt container.getEffectiveJdbcUrl(), container.getUsername(), container.getPassword(), - new Closeable() { - @Override - public void close() throws IOException { - container.stop(); - - LOG.info("Dev Services for MariaDB shut down."); - } - }); + new ContainerShutdownCloseable(container, "MariaDB")); } }); } diff --git a/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java b/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java index 412e0cf84c931..c8cb95a58bac8 100644 --- a/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java +++ b/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java @@ -1,10 +1,7 @@ package io.quarkus.devservices.mssql.deployment; -import java.io.Closeable; -import java.io.IOException; import java.time.Duration; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.OptionalInt; @@ -13,11 +10,13 @@ import org.testcontainers.utility.DockerImageName; import io.quarkus.datasource.common.runtime.DatabaseKind; +import io.quarkus.datasource.deployment.spi.DevServicesDatasourceContainerConfig; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProvider; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; import io.quarkus.devservices.common.ConfigureUtil; +import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.runtime.LaunchMode; public class MSSQLDevServicesProcessor { @@ -30,14 +29,15 @@ DevServicesDatasourceProviderBuildItem setupMSSQL( return new DevServicesDatasourceProviderBuildItem(DatabaseKind.MSSQL, new DevServicesDatasourceProvider() { @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, - Optional datasourceName, Optional imageName, - Map containerProperties, Map additionalJdbcUrlProperties, - OptionalInt fixedExposedPort, LaunchMode launchMode, Optional startupTimeout) { - QuarkusMSSQLServerContainer container = new QuarkusMSSQLServerContainer(imageName, fixedExposedPort, + Optional datasourceName, DevServicesDatasourceContainerConfig containerConfig, + LaunchMode launchMode, Optional startupTimeout) { + QuarkusMSSQLServerContainer container = new QuarkusMSSQLServerContainer(containerConfig.getImageName(), + containerConfig.getFixedExposedPort(), !devServicesSharedNetworkBuildItem.isEmpty()); startupTimeout.ifPresent(container::withStartupTimeout); - container.withPassword(password.orElse("Quarkuspassword1")); - additionalJdbcUrlProperties.forEach(container::withUrlParam); + container.withPassword(password.orElse("Quarkuspassword1")) + .withReuse(true); + containerConfig.getAdditionalJdbcUrlProperties().forEach(container::withUrlParam); container.start(); LOG.info("Dev Services for Microsoft SQL Server started."); @@ -46,14 +46,7 @@ public RunningDevServicesDatasource startDatabase(Optional username, Opt container.getEffectiveJdbcUrl(), container.getUsername(), container.getPassword(), - new Closeable() { - @Override - public void close() throws IOException { - container.stop(); - - LOG.info("Dev Services for Microsoft SQL Server shut down."); - } - }); + new ContainerShutdownCloseable(container, "Microsoft SQL Server")); } }); } diff --git a/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java b/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java index 2f6c834bff0eb..ffc0277dee99e 100644 --- a/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java +++ b/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java @@ -1,10 +1,7 @@ package io.quarkus.devservices.mysql.deployment; -import java.io.Closeable; -import java.io.IOException; import java.time.Duration; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.OptionalInt; @@ -13,11 +10,13 @@ import org.testcontainers.utility.DockerImageName; import io.quarkus.datasource.common.runtime.DatabaseKind; +import io.quarkus.datasource.deployment.spi.DevServicesDatasourceContainerConfig; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProvider; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; import io.quarkus.devservices.common.ConfigureUtil; +import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.runtime.LaunchMode; public class MySQLDevServicesProcessor { @@ -32,21 +31,23 @@ DevServicesDatasourceProviderBuildItem setupMysql( return new DevServicesDatasourceProviderBuildItem(DatabaseKind.MYSQL, new DevServicesDatasourceProvider() { @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, - Optional datasourceName, Optional imageName, - Map containerProperties, Map additionalJdbcUrlProperties, - OptionalInt fixedExposedPort, LaunchMode launchMode, Optional startupTimeout) { - QuarkusMySQLContainer container = new QuarkusMySQLContainer(imageName, fixedExposedPort, + Optional datasourceName, DevServicesDatasourceContainerConfig containerConfig, + LaunchMode launchMode, Optional startupTimeout) { + QuarkusMySQLContainer container = new QuarkusMySQLContainer(containerConfig.getImageName(), + containerConfig.getFixedExposedPort(), !devServicesSharedNetworkBuildItem.isEmpty()); startupTimeout.ifPresent(container::withStartupTimeout); container.withPassword(password.orElse("quarkus")) .withUsername(username.orElse("quarkus")) - .withDatabaseName(datasourceName.orElse("default")); + .withDatabaseName(datasourceName.orElse("default")) + .withReuse(true); - if (containerProperties.containsKey(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME)) { - container.withConfigurationOverride(containerProperties.get(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME)); + if (containerConfig.getContainerProperties().containsKey(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME)) { + container.withConfigurationOverride( + containerConfig.getContainerProperties().get(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME)); } - additionalJdbcUrlProperties.forEach(container::withUrlParam); + containerConfig.getAdditionalJdbcUrlProperties().forEach(container::withUrlParam); container.start(); @@ -56,14 +57,7 @@ public RunningDevServicesDatasource startDatabase(Optional username, Opt container.getEffectiveJdbcUrl(), container.getUsername(), container.getPassword(), - new Closeable() { - @Override - public void close() throws IOException { - container.stop(); - - LOG.info("Dev Services for MySQL shut down."); - } - }); + new ContainerShutdownCloseable(container, "MySQL")); } }); } diff --git a/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java b/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java index 3062fe714ba1e..e900eaf3a2b78 100644 --- a/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java +++ b/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java @@ -1,10 +1,7 @@ package io.quarkus.devservices.oracle.deployment; -import java.io.Closeable; -import java.io.IOException; import java.time.Duration; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.OptionalInt; @@ -13,11 +10,13 @@ import org.testcontainers.utility.DockerImageName; import io.quarkus.datasource.common.runtime.DatabaseKind; +import io.quarkus.datasource.deployment.spi.DevServicesDatasourceContainerConfig; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProvider; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; import io.quarkus.devservices.common.ConfigureUtil; +import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.runtime.LaunchMode; public class OracleDevServicesProcessor { @@ -36,16 +35,25 @@ DevServicesDatasourceProviderBuildItem setupOracle( return new DevServicesDatasourceProviderBuildItem(DatabaseKind.ORACLE, new DevServicesDatasourceProvider() { @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, - Optional datasourceName, Optional imageName, - Map containerProperties, Map additionalJdbcUrlProperties, - OptionalInt fixedExposedPort, LaunchMode launchMode, Optional startupTimeout) { - QuarkusOracleServerContainer container = new QuarkusOracleServerContainer(imageName, fixedExposedPort, + Optional datasourceName, DevServicesDatasourceContainerConfig containerConfig, + LaunchMode launchMode, Optional startupTimeout) { + QuarkusOracleServerContainer container = new QuarkusOracleServerContainer(containerConfig.getImageName(), + containerConfig.getFixedExposedPort(), !devServicesSharedNetworkBuildItem.isEmpty()); startupTimeout.ifPresent(container::withStartupTimeout); container.withUsername(username.orElse(DEFAULT_DATABASE_USER)) .withPassword(password.orElse(DEFAULT_DATABASE_PASSWORD)) - .withDatabaseName(datasourceName.orElse(DEFAULT_DATABASE_NAME)); - additionalJdbcUrlProperties.forEach(container::withUrlParam); + .withDatabaseName(datasourceName.orElse(DEFAULT_DATABASE_NAME)) + .withReuse(true); + + // We need to limit the maximum amount of CPUs being used by the container; + // otherwise the hardcoded memory configuration of the DB might not be enough to successfully boot it. + // See https://github.com/gvenzl/oci-oracle-xe/issues/64 + // I choose to limit it to "2 cpus": should be more than enough for any local testing needs, + // and keeps things simple. + container.withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withNanoCPUs(2_000_000_000l)); + + containerConfig.getAdditionalJdbcUrlProperties().forEach(container::withUrlParam); container.start(); LOG.info("Dev Services for Oracle started."); @@ -54,14 +62,7 @@ public RunningDevServicesDatasource startDatabase(Optional username, Opt container.getEffectiveJdbcUrl(), container.getUsername(), container.getPassword(), - new Closeable() { - @Override - public void close() throws IOException { - container.stop(); - - LOG.info("Dev Services for Oracle shut down."); - } - }); + new ContainerShutdownCloseable(container, "Oracle")); } }); } diff --git a/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java b/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java index 575e6612ba1d6..f57156575e4a7 100644 --- a/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java +++ b/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java @@ -1,10 +1,7 @@ package io.quarkus.devservices.postgresql.deployment; -import java.io.Closeable; -import java.io.IOException; import java.time.Duration; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.OptionalInt; @@ -13,6 +10,7 @@ import org.testcontainers.utility.DockerImageName; import io.quarkus.datasource.common.runtime.DatabaseKind; +import io.quarkus.datasource.deployment.spi.DevServicesDatasourceContainerConfig; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProvider; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; @@ -20,6 +18,7 @@ import io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; import io.quarkus.devservices.common.ConfigureUtil; +import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.runtime.LaunchMode; public class PostgresqlDevServicesProcessor { @@ -37,16 +36,17 @@ DevServicesDatasourceProviderBuildItem setupPostgres( return new DevServicesDatasourceProviderBuildItem(DatabaseKind.POSTGRESQL, new DevServicesDatasourceProvider() { @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, - Optional datasourceName, Optional imageName, - Map containerProperties, Map additionalJdbcUrlProperties, - OptionalInt fixedExposedPort, LaunchMode launchMode, Optional startupTimeout) { - QuarkusPostgreSQLContainer container = new QuarkusPostgreSQLContainer(imageName, fixedExposedPort, + Optional datasourceName, DevServicesDatasourceContainerConfig containerConfig, + LaunchMode launchMode, Optional startupTimeout) { + QuarkusPostgreSQLContainer container = new QuarkusPostgreSQLContainer(containerConfig.getImageName(), + containerConfig.getFixedExposedPort(), !devServicesSharedNetworkBuildItem.isEmpty()); startupTimeout.ifPresent(container::withStartupTimeout); container.withPassword(password.orElse("quarkus")) .withUsername(username.orElse("quarkus")) - .withDatabaseName(datasourceName.orElse("default")); - additionalJdbcUrlProperties.forEach(container::withUrlParam); + .withDatabaseName(datasourceName.orElse("default")) + .withReuse(true); + containerConfig.getAdditionalJdbcUrlProperties().forEach(container::withUrlParam); container.start(); @@ -56,14 +56,7 @@ public RunningDevServicesDatasource startDatabase(Optional username, Opt container.getEffectiveJdbcUrl(), container.getUsername(), container.getPassword(), - new Closeable() { - @Override - public void close() throws IOException { - container.stop(); - - LOG.info("Dev Services for PostgreSQL shut down."); - } - }); + new ContainerShutdownCloseable(container, "PostgreSQL")); } }); } diff --git a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java index 656c227ef6c71..bdfabafe8418e 100644 --- a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java +++ b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java @@ -16,12 +16,12 @@ import io.quarkus.builder.BuildException; import io.quarkus.deployment.Feature; -import io.quarkus.deployment.IsDockerWorking; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.console.ConsoleInstalledBuildItem; import io.quarkus.deployment.console.StartupLogCompressor; @@ -52,10 +52,9 @@ public class DevServicesElasticsearchProcessor { static volatile ElasticsearchDevServicesBuildTimeConfig cfg; static volatile boolean first = true; - private final IsDockerWorking isDockerWorking = new IsDockerWorking(true); - @BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) public DevServicesResultBuildItem startElasticsearchDevService( + DockerStatusBuildItem dockerStatusBuildItem, LaunchModeBuildItem launchMode, ElasticsearchDevServicesBuildTimeConfig configuration, List devServicesSharedNetworkBuildItem, @@ -86,10 +85,14 @@ public DevServicesResultBuildItem startElasticsearchDevService( (launchMode.isTest() ? "(test) " : "") + "Elasticsearch Dev Services Starting:", consoleInstalledBuildItem, loggingSetupBuildItem); try { - devService = startElasticsearch(configuration, buildItemsConfig, launchMode, + devService = startElasticsearch(dockerStatusBuildItem, configuration, buildItemsConfig, launchMode, !devServicesSharedNetworkBuildItem.isEmpty(), devServicesConfig.timeout); - compressor.close(); + if (devService == null) { + compressor.closeAndDumpCaptured(); + } else { + compressor.close(); + } } catch (Throwable t) { compressor.closeAndDumpCaptured(); throw new RuntimeException(t); @@ -141,7 +144,9 @@ private void shutdownElasticsearch() { } } - private DevServicesResultBuildItem.RunningDevService startElasticsearch(ElasticsearchDevServicesBuildTimeConfig config, + private DevServicesResultBuildItem.RunningDevService startElasticsearch( + DockerStatusBuildItem dockerStatusBuildItem, + ElasticsearchDevServicesBuildTimeConfig config, DevservicesElasticsearchBuildItemsConfiguration buildItemConfig, LaunchModeBuildItem launchMode, boolean useSharedNetwork, Optional timeout) throws BuildException { if (!config.enabled.orElse(true)) { @@ -158,7 +163,7 @@ private DevServicesResultBuildItem.RunningDevService startElasticsearch(Elastics } } - if (!isDockerWorking.getAsBoolean()) { + if (!dockerStatusBuildItem.isDockerAvailable()) { log.warnf("Docker isn't working, please configure the Elasticsearch hosts property (%s).", displayProperties(buildItemConfig.hostsConfigProperties)); return null; diff --git a/extensions/elasticsearch-rest-client/deployment/pom.xml b/extensions/elasticsearch-rest-client/deployment/pom.xml index a89c7e544c724..cec8af5407f64 100644 --- a/extensions/elasticsearch-rest-client/deployment/pom.xml +++ b/extensions/elasticsearch-rest-client/deployment/pom.xml @@ -68,7 +68,34 @@ + + maven-surefire-plugin + + true + + + + + test-elasticsearch + + + test-containers + + + + + + maven-surefire-plugin + + false + + + + + + + diff --git a/extensions/elasticsearch-rest-client/deployment/src/test/java/io/quarkus/elasticsearch/restclient/lowlevel/runtime/DevServicesElasticsearchDevModeTestCase.java b/extensions/elasticsearch-rest-client/deployment/src/test/java/io/quarkus/elasticsearch/restclient/lowlevel/runtime/DevServicesElasticsearchDevModeTestCase.java index f54fc813c6fcb..afe95533be43e 100644 --- a/extensions/elasticsearch-rest-client/deployment/src/test/java/io/quarkus/elasticsearch/restclient/lowlevel/runtime/DevServicesElasticsearchDevModeTestCase.java +++ b/extensions/elasticsearch-rest-client/deployment/src/test/java/io/quarkus/elasticsearch/restclient/lowlevel/runtime/DevServicesElasticsearchDevModeTestCase.java @@ -3,14 +3,11 @@ import static org.hamcrest.Matchers.equalTo; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnOs; -import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.test.QuarkusDevModeTest; import io.restassured.RestAssured; -@DisabledOnOs(OS.WINDOWS) public class DevServicesElasticsearchDevModeTestCase { @RegisterExtension static QuarkusDevModeTest test = new QuarkusDevModeTest() diff --git a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java index a4a3a1bc20114..1fa4f5a1cc47d 100644 --- a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java +++ b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java @@ -45,6 +45,7 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; import io.quarkus.deployment.builditem.IndexDependencyBuildItem; import io.quarkus.deployment.builditem.ServiceStartBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; @@ -81,6 +82,7 @@ IndexDependencyBuildItem indexFlyway() { MigrationStateBuildItem build(BuildProducer featureProducer, BuildProducer resourceProducer, BuildProducer reflectiveClassProducer, + BuildProducer hotDeploymentProducer, FlywayRecorder recorder, RecorderContext context, CombinedIndexBuildItem combinedIndexBuildItem, @@ -107,6 +109,9 @@ MigrationStateBuildItem build(BuildProducer featureProducer, Collection applicationMigrations = applicationMigrationsToDs.values().stream().collect(HashSet::new, AbstractCollection::addAll, HashSet::addAll); + for (String applicationMigration : applicationMigrations) { + hotDeploymentProducer.produce(new HotDeploymentWatchedFileBuildItem(applicationMigration)); + } recorder.setApplicationMigrationFiles(applicationMigrations); Set> javaMigrationClasses = new HashSet<>(); diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayDevModeModifyMigrationTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayDevModeModifyMigrationTest.java new file mode 100644 index 0000000000000..51fd003dd2805 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayDevModeModifyMigrationTest.java @@ -0,0 +1,58 @@ +package io.quarkus.flyway.test; + +import static org.hamcrest.Matchers.is; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.function.Function; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class FlywayDevModeModifyMigrationTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(RowCountEndpoint.class) + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("clean-and-migrate-at-start-config.properties", "application.properties")); + + @Test + public void testModifyingExistingMigrationScriptCausesRestart() { + RestAssured.get("/row-count").then().statusCode(200).body(is("0")); + config.modifyResourceFile("db/migration/V1.0.0__Quarkus.sql", new Function() { + @Override + public String apply(String s) { + return s + '\n' + "INSERT INTO quarked_flyway VALUES (1001, 'test')"; + } + }); + RestAssured.get("/row-count").then().statusCode(200).body(is("1")); + } + + @Path("/row-count") + public static class RowCountEndpoint { + + @Inject + AgroalDataSource dataSource; + + @GET + public int rowCount() throws SQLException { + try (Connection connection = dataSource.getConnection(); Statement stat = connection.createStatement()) { + try (ResultSet countQuery = stat.executeQuery("select count(1) from quarked_flyway")) { + return countQuery.first() ? countQuery.getInt(1) : 0; + } + } + } + } +} diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayCreator.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayCreator.java index bd8edd6289c5a..328a05a03487f 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayCreator.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayCreator.java @@ -59,6 +59,7 @@ public Flyway createFlyway(DataSource dataSource) { configure.validateOnMigrate(flywayRuntimeConfig.validateOnMigrate); configure.ignoreMissingMigrations(flywayRuntimeConfig.ignoreMissingMigrations); configure.ignoreFutureMigrations(flywayRuntimeConfig.ignoreFutureMigrations); + configure.cleanOnValidationError(flywayRuntimeConfig.cleanOnValidationError); configure.outOfOrder(flywayRuntimeConfig.outOfOrder); if (flywayRuntimeConfig.baselineVersion.isPresent()) { configure.baselineVersion(flywayRuntimeConfig.baselineVersion.get()); diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java index 8afa0e0781b68..35c41221c1b02 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java @@ -92,6 +92,12 @@ public static final FlywayDataSourceRuntimeConfig defaultConfig() { @ConfigItem public boolean cleanDisabled; + /** + * true to automatically call clean when a validation error occurs, false otherwise. + */ + @ConfigItem + public boolean cleanOnValidationError; + /** * true to execute Flyway automatically when the application starts, false otherwise. * diff --git a/extensions/flyway/runtime/src/test/java/io/quarkus/flyway/runtime/FlywayCreatorTest.java b/extensions/flyway/runtime/src/test/java/io/quarkus/flyway/runtime/FlywayCreatorTest.java index a4635481b29d1..825f3b84ee360 100644 --- a/extensions/flyway/runtime/src/test/java/io/quarkus/flyway/runtime/FlywayCreatorTest.java +++ b/extensions/flyway/runtime/src/test/java/io/quarkus/flyway/runtime/FlywayCreatorTest.java @@ -219,6 +219,22 @@ void testIgnoreFutureMigrations() { assertTrue(createdFlywayConfig().isIgnoreFutureMigrations()); } + @Test + @DisplayName("cleanOnValidationError defaults to false and is correctly set") + void testCleanOnValidationError() { + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertEquals(runtimeConfig.cleanOnValidationError, createdFlywayConfig().isCleanOnValidationError()); + assertFalse(runtimeConfig.cleanOnValidationError); + + runtimeConfig.cleanOnValidationError = false; + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertFalse(createdFlywayConfig().isCleanOnValidationError()); + + runtimeConfig.cleanOnValidationError = true; + creator = new FlywayCreator(runtimeConfig, buildConfig); + assertTrue(createdFlywayConfig().isCleanOnValidationError()); + } + @ParameterizedTest @MethodSource("validateOnMigrateOverwritten") @DisplayName("validate on migrate overwritten in configuration") diff --git a/extensions/funqy/funqy-knative-events/deployment/src/test/java/io/quarkus/funqy/test/ExposedCloudEventTest.java b/extensions/funqy/funqy-knative-events/deployment/src/test/java/io/quarkus/funqy/test/ExposedCloudEventTest.java index 46416f7c6671a..902da97ccf670 100644 --- a/extensions/funqy/funqy-knative-events/deployment/src/test/java/io/quarkus/funqy/test/ExposedCloudEventTest.java +++ b/extensions/funqy/funqy-knative-events/deployment/src/test/java/io/quarkus/funqy/test/ExposedCloudEventTest.java @@ -81,6 +81,18 @@ public void testGenericInput() { .body(equalTo("6")); } + @Test + public void testNullResponse() { + RestAssured.given().contentType("application/json") + .header("ce-id", "test-id") + .header("ce-specversion", "1.0") + .header("ce-type", "test-null-response") + .header("ce-source", "test-source") + .post() + .then() + .statusCode(204); + } + @ParameterizedTest @MethodSource("provideBinaryEncodingTestArgs") public void testBinaryEncoding(Map headers, String specversion, String dataSchemaHdrName) { diff --git a/extensions/funqy/funqy-knative-events/deployment/src/test/java/io/quarkus/funqy/test/ExposedCloudEvents.java b/extensions/funqy/funqy-knative-events/deployment/src/test/java/io/quarkus/funqy/test/ExposedCloudEvents.java index b44e6ff9a6ad1..1bb57a99fb4b9 100644 --- a/extensions/funqy/funqy-knative-events/deployment/src/test/java/io/quarkus/funqy/test/ExposedCloudEvents.java +++ b/extensions/funqy/funqy-knative-events/deployment/src/test/java/io/quarkus/funqy/test/ExposedCloudEvents.java @@ -65,6 +65,12 @@ public CloudEvent sum(CloudEvent> event) { .build(data); } + @Funq + @CloudEventMapping(trigger = "test-null-response") + public CloudEvent returnNull(CloudEvent ignore) { + return null; + } + public static class TestBean implements Serializable { private int i; private String s; diff --git a/extensions/funqy/funqy-knative-events/runtime/src/main/java/io/quarkus/funqy/runtime/bindings/knative/events/VertxRequestHandler.java b/extensions/funqy/funqy-knative-events/runtime/src/main/java/io/quarkus/funqy/runtime/bindings/knative/events/VertxRequestHandler.java index 5c3c2ce764e8c..f9d87c003ef05 100644 --- a/extensions/funqy/funqy-knative-events/runtime/src/main/java/io/quarkus/funqy/runtime/bindings/knative/events/VertxRequestHandler.java +++ b/extensions/funqy/funqy-knative-events/runtime/src/main/java/io/quarkus/funqy/runtime/bindings/knative/events/VertxRequestHandler.java @@ -261,6 +261,12 @@ private void processCloudEvent(RoutingContext routingContext) { outputCloudEvent = (CloudEvent) output; } + if (outputCloudEvent == null) { + routingContext.response().setStatusCode(204); + routingContext.response().end(); + return; + } + String id = outputCloudEvent.id(); if (id == null) { id = getResponseId(); diff --git a/extensions/google-cloud-functions/deployment/pom.xml b/extensions/google-cloud-functions/deployment/pom.xml old mode 100755 new mode 100644 diff --git a/extensions/google-cloud-functions/deployment/src/main/java/io/quarkus/gcp/functions/deployment/GoogleCloudFunctionsProcessor.java b/extensions/google-cloud-functions/deployment/src/main/java/io/quarkus/gcp/functions/deployment/GoogleCloudFunctionsProcessor.java old mode 100755 new mode 100644 diff --git a/extensions/google-cloud-functions/pom.xml b/extensions/google-cloud-functions/pom.xml old mode 100755 new mode 100644 diff --git a/extensions/google-cloud-functions/runtime/pom.xml b/extensions/google-cloud-functions/runtime/pom.xml old mode 100755 new mode 100644 diff --git a/extensions/google-cloud-functions/runtime/src/main/java/io/quarkus/gcp/functions/QuarkusHttpFunction.java b/extensions/google-cloud-functions/runtime/src/main/java/io/quarkus/gcp/functions/QuarkusHttpFunction.java old mode 100755 new mode 100644 diff --git a/extensions/grpc/deployment/src/main/resources/dev-templates/service.html b/extensions/grpc/deployment/src/main/resources/dev-templates/service.html index de554e31e6d9c..d3fde6ffe6abb 100644 --- a/extensions/grpc/deployment/src/main/resources/dev-templates/service.html +++ b/extensions/grpc/deployment/src/main/resources/dev-templates/service.html @@ -137,6 +137,7 @@ } $(document).ready(function(){ + connect(); if (!ideKnown()) { return; } @@ -165,7 +166,6 @@ editor.refresh(); }); - connect(); }); function disconnect(serviceName, methodName, methodType) { diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/devmode/StreamCollectorInterceptor.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/devmode/StreamCollectorInterceptor.java index 3d962de2780e3..f0044724e93b5 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/devmode/StreamCollectorInterceptor.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/devmode/StreamCollectorInterceptor.java @@ -5,6 +5,7 @@ import javax.interceptor.Interceptor; import javax.interceptor.InvocationContext; +import io.grpc.stub.ServerCallStreamObserver; import io.grpc.stub.StreamObserver; import io.quarkus.grpc.stubs.ServerCalls; import io.quarkus.grpc.stubs.StreamCollector; @@ -45,7 +46,7 @@ Object collect(InvocationContext context) throws Exception { Object[] newParams = new Object[params.length]; for (int i = 0; i < params.length; i++) { if (i == streamIndex) { - newParams[i] = new StreamObserverWrapper<>(stream); + newParams[i] = wrap(stream); } else { newParams[i] = params[i]; } @@ -54,6 +55,13 @@ Object collect(InvocationContext context) throws Exception { return context.proceed(); } + private StreamObserver wrap(StreamObserver stream) { + if (stream instanceof ServerCallStreamObserver) { + return new ServerCallStreamObserverWrapper<>((ServerCallStreamObserver) stream); + } + return new StreamObserverWrapper<>(stream); + } + private final class StreamObserverWrapper implements StreamObserver { private final StreamObserver delegate; @@ -81,4 +89,80 @@ public void onCompleted() { } + private final class ServerCallStreamObserverWrapper extends ServerCallStreamObserver { + + private final ServerCallStreamObserver delegate; + + public ServerCallStreamObserverWrapper(ServerCallStreamObserver delegate) { + this.delegate = delegate; + } + + @Override + public void onNext(T value) { + delegate.onNext(value); + } + + @Override + public void onError(Throwable t) { + delegate.onError(t); + streamCollector.remove(delegate); + } + + @Override + public void onCompleted() { + delegate.onCompleted(); + streamCollector.remove(delegate); + } + + @Override + public boolean isCancelled() { + return delegate.isCancelled(); + } + + @Override + public void setOnCancelHandler(Runnable runnable) { + delegate.setOnCancelHandler(runnable); + } + + @Override + public void setCompression(String s) { + delegate.setCompression(s); + } + + @Override + public void disableAutoRequest() { + delegate.disableAutoRequest(); + } + + @Override + public boolean isReady() { + return delegate.isReady(); + } + + @Override + public void setOnReadyHandler(Runnable runnable) { + delegate.setOnReadyHandler(runnable); + } + + @Override + public void request(int i) { + delegate.request(i); + } + + @Override + public void setMessageCompression(boolean b) { + delegate.setMessageCompression(b); + } + + @Override + public void setOnCloseHandler(Runnable onCloseHandler) { + delegate.setOnCloseHandler(onCloseHandler); + } + + @Override + public void disableAutoInboundFlowControl() { + delegate.disableAutoInboundFlowControl(); + } + } + } diff --git a/extensions/grpc/stubs/src/main/java/io/quarkus/grpc/stubs/ServerCalls.java b/extensions/grpc/stubs/src/main/java/io/quarkus/grpc/stubs/ServerCalls.java index a731f5c1d9530..9f39f0a36ee3c 100644 --- a/extensions/grpc/stubs/src/main/java/io/quarkus/grpc/stubs/ServerCalls.java +++ b/extensions/grpc/stubs/src/main/java/io/quarkus/grpc/stubs/ServerCalls.java @@ -13,6 +13,7 @@ import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.operators.multi.processors.UnicastProcessor; +import io.smallrye.mutiny.subscription.Cancellable; public class ServerCalls { private static final Logger log = Logger.getLogger(ServerCalls.class); @@ -64,7 +65,7 @@ public static void oneToMany(I request, StreamObserver response, Strin response.onError(Status.fromCode(Status.Code.INTERNAL).asException()); return; } - returnValue.subscribe().with( + handleSubscription(returnValue.subscribe().with( new Consumer() { @Override public void accept(O v) { @@ -82,7 +83,7 @@ public void accept(Throwable throwable) { public void run() { onCompleted(response); } - }); + }), response); } catch (Throwable throwable) { onError(response, toStatusFailure(throwable)); } @@ -124,6 +125,22 @@ public void accept(Throwable failure) { } } + private static void handleSubscription(Cancellable cancellable, StreamObserver response) { + if (response instanceof ServerCallStreamObserver) { + ServerCallStreamObserver serverCallResponse = (ServerCallStreamObserver) response; + + Runnable cancel = new Runnable() { + @Override + public void run() { + cancellable.cancel(); + } + }; + + serverCallResponse.setOnCloseHandler(cancel); + serverCallResponse.setOnCancelHandler(cancel); + } + } + public static StreamObserver manyToMany(StreamObserver response, Function, Multi> implementation) { try { @@ -137,7 +154,7 @@ public static StreamObserver manyToMany(StreamObserver response, response.onError(Status.fromCode(Status.Code.INTERNAL).asException()); return null; } - multi.subscribe().with( + handleSubscription(multi.subscribe().with( new Consumer() { @Override public void accept(O v) { @@ -155,7 +172,8 @@ public void accept(Throwable failure) { public void run() { onCompleted(response); } - }); + }), response); + return pump; } catch (Throwable throwable) { onError(response, toStatusFailure(throwable)); diff --git a/extensions/hal/deployment/pom.xml b/extensions/hal/deployment/pom.xml new file mode 100644 index 0000000000000..db8ffa13a6051 --- /dev/null +++ b/extensions/hal/deployment/pom.xml @@ -0,0 +1,60 @@ + + + + quarkus-hal-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-hal-deployment + Quarkus - HAL - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-hal + + + io.quarkus + quarkus-jackson-spi + + + io.quarkus + quarkus-jsonb-spi + + + io.quarkus + quarkus-junit5-internal + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/hal/deployment/src/main/java/io/quarkus/hal/deployment/HalProcessor.java b/extensions/hal/deployment/src/main/java/io/quarkus/hal/deployment/HalProcessor.java new file mode 100755 index 0000000000000..d8988b7c78d2e --- /dev/null +++ b/extensions/hal/deployment/src/main/java/io/quarkus/hal/deployment/HalProcessor.java @@ -0,0 +1,43 @@ +package io.quarkus.hal.deployment; + +import java.util.Arrays; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.hal.HalCollectionWrapper; +import io.quarkus.hal.HalCollectionWrapperJacksonSerializer; +import io.quarkus.hal.HalCollectionWrapperJsonbSerializer; +import io.quarkus.hal.HalEntityWrapper; +import io.quarkus.hal.HalEntityWrapperJacksonSerializer; +import io.quarkus.hal.HalEntityWrapperJsonbSerializer; +import io.quarkus.hal.HalLink; +import io.quarkus.hal.HalLinkJacksonSerializer; +import io.quarkus.hal.HalLinkJsonbSerializer; +import io.quarkus.jackson.spi.JacksonModuleBuildItem; +import io.quarkus.jsonb.spi.JsonbSerializerBuildItem; + +public class HalProcessor { + + @BuildStep + ReflectiveClassBuildItem registerReflection() { + return new ReflectiveClassBuildItem(true, true, HalLink.class); + } + + @BuildStep + JacksonModuleBuildItem registerJacksonSerializers() { + return new JacksonModuleBuildItem.Builder("hal-wrappers") + .addSerializer(HalEntityWrapperJacksonSerializer.class.getName(), HalEntityWrapper.class.getName()) + .addSerializer(HalCollectionWrapperJacksonSerializer.class.getName(), HalCollectionWrapper.class.getName()) + .addSerializer(HalLinkJacksonSerializer.class.getName(), HalLink.class.getName()) + .build(); + } + + @BuildStep + JsonbSerializerBuildItem registerJsonbSerializers() { + return new JsonbSerializerBuildItem(Arrays.asList( + HalEntityWrapperJsonbSerializer.class.getName(), + HalCollectionWrapperJsonbSerializer.class.getName(), + HalLinkJsonbSerializer.class.getName())); + } + +} diff --git a/extensions/hal/pom.xml b/extensions/hal/pom.xml new file mode 100644 index 0000000000000..567cb6c9fc0c3 --- /dev/null +++ b/extensions/hal/pom.xml @@ -0,0 +1,20 @@ + + + + quarkus-extensions-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-hal-parent + Quarkus - HAL + pom + + deployment + runtime + + diff --git a/extensions/hal/runtime/pom.xml b/extensions/hal/runtime/pom.xml new file mode 100644 index 0000000000000..038c280192eaf --- /dev/null +++ b/extensions/hal/runtime/pom.xml @@ -0,0 +1,94 @@ + + + + quarkus-hal-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-hal + Quarkus - HAL - Runtime + Hypertext Application Language (HAL) support + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-arc + + + org.jboss.spec.javax.ws.rs + jboss-jaxrs-api_2.1_spec + + + io.quarkus + quarkus-jackson + true + + + io.quarkus + quarkus-jsonb + true + + + org.junit.jupiter + junit-jupiter + test + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + io.quarkus.hal + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + + + jakarta-rewrite + + + jakarta-rewrite + + + + + + org.openrewrite.maven + rewrite-maven-plugin + + + io.quarkus.jakarta-jaxrs-switch + + + + + + + + diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapper.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapper.java new file mode 100644 index 0000000000000..6b1bcb8f64100 --- /dev/null +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapper.java @@ -0,0 +1,43 @@ +package io.quarkus.hal; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.core.Link; + +/** + * The Hal collection wrapper that includes the list of Hal entities {@link HalEntityWrapper}, the collection name and the Hal + * links. + * + * This type is serialized into Json using: + * - the JSON-B serializer: {@link HalCollectionWrapperJsonbSerializer} + * - the Jackson serializer: {@link HalCollectionWrapperJacksonSerializer} + */ +public class HalCollectionWrapper extends HalWrapper { + + private final Collection collection; + private final String collectionName; + + public HalCollectionWrapper(Collection collection, String collectionName, Link... links) { + this(collection, collectionName, new HashMap<>()); + + addLinks(links); + } + + public HalCollectionWrapper(Collection collection, String collectionName, Map links) { + super(links); + + this.collection = collection; + this.collectionName = collectionName; + } + + public Collection getCollection() { + return collection; + } + + public String getCollectionName() { + return collectionName; + } + +} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapperJacksonSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJacksonSerializer.java similarity index 62% rename from extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapperJacksonSerializer.java rename to extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJacksonSerializer.java index 28148fc8899a4..a922f9cec1c0b 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapperJacksonSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJacksonSerializer.java @@ -1,7 +1,6 @@ -package io.quarkus.rest.data.panache.runtime.hal; +package io.quarkus.hal; import java.io.IOException; -import java.util.Map; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; @@ -9,16 +8,6 @@ public class HalCollectionWrapperJacksonSerializer extends JsonSerializer { - private final HalLinksProvider linksExtractor; - - public HalCollectionWrapperJacksonSerializer() { - this.linksExtractor = new RestEasyHalLinksProvider(); - } - - HalCollectionWrapperJacksonSerializer(HalLinksProvider linksExtractor) { - this.linksExtractor = linksExtractor; - } - @Override public void serialize(HalCollectionWrapper wrapper, JsonGenerator generator, SerializerProvider serializers) throws IOException { @@ -35,18 +24,16 @@ private void writeEmbedded(HalCollectionWrapper wrapper, JsonGenerator generator generator.writeFieldName("_embedded"); generator.writeStartObject(); generator.writeFieldName(wrapper.getCollectionName()); - generator.writeStartArray(wrapper.getCollection().size()); - for (Object entity : wrapper.getCollection()) { - entitySerializer.serialize(new HalEntityWrapper(entity), generator, serializers); + generator.writeStartArray(); + for (HalEntityWrapper entity : wrapper.getCollection()) { + entitySerializer.serialize(entity, generator, serializers); } generator.writeEndArray(); generator.writeEndObject(); } private void writeLinks(HalCollectionWrapper wrapper, JsonGenerator generator) throws IOException { - Map links = linksExtractor.getLinks(wrapper.getElementType()); - links.putAll(wrapper.getLinks()); generator.writeFieldName("_links"); - generator.writeObject(links); + generator.writeObject(wrapper.getLinks()); } } diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapperJsonbSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJsonbSerializer.java similarity index 60% rename from extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapperJsonbSerializer.java rename to extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJsonbSerializer.java index 76e9f32016604..c12e137b805d0 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapperJsonbSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJsonbSerializer.java @@ -1,6 +1,4 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import java.util.Map; +package io.quarkus.hal; import javax.json.bind.serializer.JsonbSerializer; import javax.json.bind.serializer.SerializationContext; @@ -8,16 +6,6 @@ public class HalCollectionWrapperJsonbSerializer implements JsonbSerializer { - private final HalLinksProvider linksExtractor; - - public HalCollectionWrapperJsonbSerializer() { - this.linksExtractor = new RestEasyHalLinksProvider(); - } - - HalCollectionWrapperJsonbSerializer(HalLinksProvider linksExtractor) { - this.linksExtractor = linksExtractor; - } - @Override public void serialize(HalCollectionWrapper wrapper, JsonGenerator generator, SerializationContext context) { generator.writeStartObject(); @@ -31,16 +19,14 @@ private void writeEmbedded(HalCollectionWrapper wrapper, JsonGenerator generator generator.writeStartObject(); generator.writeKey(wrapper.getCollectionName()); generator.writeStartArray(); - for (Object entity : wrapper.getCollection()) { - context.serialize(new HalEntityWrapper(entity), generator); + for (HalEntityWrapper entity : wrapper.getCollection()) { + context.serialize(entity, generator); } generator.writeEnd(); generator.writeEnd(); } private void writeLinks(HalCollectionWrapper wrapper, JsonGenerator generator, SerializationContext context) { - Map links = linksExtractor.getLinks(wrapper.getElementType()); - links.putAll(wrapper.getLinks()); - context.serialize("_links", links, generator); + context.serialize("_links", wrapper.getLinks(), generator); } } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapper.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapper.java new file mode 100644 index 0000000000000..24b3a1e6b0852 --- /dev/null +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapper.java @@ -0,0 +1,34 @@ +package io.quarkus.hal; + +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.core.Link; + +/** + * The Hal entity wrapper that includes the entity and the Hal links. + * + * This type is serialized into Json using: + * - the JSON-B serializer: {@link HalEntityWrapperJsonbSerializer} + * - the Jackson serializer: {@link HalEntityWrapperJacksonSerializer} + */ +public class HalEntityWrapper extends HalWrapper { + + private final Object entity; + + public HalEntityWrapper(Object entity, Link... links) { + this(entity, new HashMap<>()); + + addLinks(links); + } + + public HalEntityWrapper(Object entity, Map links) { + super(links); + + this.entity = entity; + } + + public Object getEntity() { + return entity; + } +} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJacksonSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJacksonSerializer.java similarity index 72% rename from extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJacksonSerializer.java rename to extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJacksonSerializer.java index 8efc74b8b5d83..58e27950e82eb 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJacksonSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJacksonSerializer.java @@ -1,4 +1,4 @@ -package io.quarkus.rest.data.panache.runtime.hal; +package io.quarkus.hal; import java.io.IOException; import java.util.List; @@ -14,25 +14,16 @@ public class HalEntityWrapperJacksonSerializer extends JsonSerializer { - private final HalLinksProvider linksExtractor; - - public HalEntityWrapperJacksonSerializer() { - this.linksExtractor = new RestEasyHalLinksProvider(); - } - - HalEntityWrapperJacksonSerializer(HalLinksProvider linksExtractor) { - this.linksExtractor = linksExtractor; - } - @Override public void serialize(HalEntityWrapper wrapper, JsonGenerator generator, SerializerProvider serializers) throws IOException { + Object entity = wrapper.getEntity(); generator.writeStartObject(); - for (BeanPropertyDefinition property : getPropertyDefinitions(serializers, wrapper.getEntity().getClass())) { + for (BeanPropertyDefinition property : getPropertyDefinitions(serializers, entity.getClass())) { AnnotatedMember accessor = property.getAccessor(); if (accessor != null) { - Object value = accessor.getValue(wrapper.getEntity()); + Object value = accessor.getValue(entity); generator.writeFieldName(property.getName()); if (value == null) { generator.writeNull(); @@ -41,12 +32,11 @@ public void serialize(HalEntityWrapper wrapper, JsonGenerator generator, Seriali } } } - writeLinks(wrapper.getEntity(), generator); + writeLinks(wrapper.getLinks(), generator); generator.writeEndObject(); } - private void writeLinks(Object entity, JsonGenerator generator) throws IOException { - Map links = linksExtractor.getLinks(entity); + private void writeLinks(Map links, JsonGenerator generator) throws IOException { generator.writeFieldName("_links"); generator.writeObject(links); } diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJsonbSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJsonbSerializer.java similarity index 74% rename from extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJsonbSerializer.java rename to extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJsonbSerializer.java index b860fbbcbe92e..a1d83f4bb3353 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJsonbSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJsonbSerializer.java @@ -1,4 +1,4 @@ -package io.quarkus.rest.data.panache.runtime.hal; +package io.quarkus.hal; import java.util.Map; @@ -12,16 +12,6 @@ public class HalEntityWrapperJsonbSerializer implements JsonbSerializer { - private final HalLinksProvider linksExtractor; - - public HalEntityWrapperJsonbSerializer() { - this.linksExtractor = new RestEasyHalLinksProvider(); - } - - HalEntityWrapperJsonbSerializer(HalLinksProvider linksExtractor) { - this.linksExtractor = linksExtractor; - } - @Override public void serialize(HalEntityWrapper wrapper, JsonGenerator generator, SerializationContext context) { Marshaller marshaller = (Marshaller) context; @@ -41,7 +31,7 @@ public void serialize(HalEntityWrapper wrapper, JsonGenerator generator, Seriali } } - writeLinks(entity, generator, context); + writeLinks(wrapper.getLinks(), generator, context); generator.writeEnd(); } finally { marshaller.removeProcessedObject(entity); @@ -56,8 +46,7 @@ private void writeValue(String name, Object value, JsonGenerator generator, Seri } } - private void writeLinks(Object entity, JsonGenerator generator, SerializationContext context) { - Map links = linksExtractor.getLinks(entity); + private void writeLinks(Map links, JsonGenerator generator, SerializationContext context) { context.serialize("_links", links, generator); } } diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLink.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java similarity index 78% rename from extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLink.java rename to extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java index 6b4b24a0a4c0a..accf86b916e7e 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLink.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java @@ -1,4 +1,4 @@ -package io.quarkus.rest.data.panache.runtime.hal; +package io.quarkus.hal; public class HalLink { diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinkJacksonSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java similarity index 91% rename from extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinkJacksonSerializer.java rename to extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java index 4ff10038509b4..01873dba72aa5 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinkJacksonSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java @@ -1,4 +1,4 @@ -package io.quarkus.rest.data.panache.runtime.hal; +package io.quarkus.hal; import java.io.IOException; diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinkJsonbSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java similarity index 90% rename from extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinkJsonbSerializer.java rename to extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java index 3f5b0c407883f..f4759eb53bf99 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinkJsonbSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java @@ -1,4 +1,4 @@ -package io.quarkus.rest.data.panache.runtime.hal; +package io.quarkus.hal; import javax.json.bind.serializer.JsonbSerializer; import javax.json.bind.serializer.SerializationContext; diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalService.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalService.java new file mode 100644 index 0000000000000..a4fc08cf0bcc9 --- /dev/null +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalService.java @@ -0,0 +1,82 @@ +package io.quarkus.hal; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Service with Hal utilities. This service is used by the Resteasy Links, Resteasy Reactive Links and the + * Rest Data Panache extensions. + */ +@SuppressWarnings("unused") +public abstract class HalService { + + private static final String SELF_REF = "self"; + + /** + * Wrap a collection of objects into a Hal collection wrapper by resolving the Hal links. + * The Hal collection wrapper is then serialized by either json or jackson. + * + * @param collection The collection of objects to wrap. + * @param collectionName The name that will include the collection of objects within the `_embedded` Hal object. + * @param entityClass The class of the objects in the collection. If null, it will not resolve the links for these objects. + * @return The Hal collection wrapper instance. + */ + public HalCollectionWrapper toHalCollectionWrapper(Collection collection, String collectionName, + Class entityClass) { + List items = new ArrayList<>(); + for (Object entity : collection) { + items.add(toHalWrapper(entity)); + } + + Map classLinks = Collections.emptyMap(); + if (entityClass != null) { + classLinks = getClassLinks(entityClass); + } + + return new HalCollectionWrapper(items, collectionName, classLinks); + } + + /** + * Wrap an entity into a Hal instance by including the entity itself and the Hal links. + * + * @param entity The entity to wrap. + * @return The Hal entity wrapper. + */ + public HalEntityWrapper toHalWrapper(Object entity) { + return new HalEntityWrapper(entity, getInstanceLinks(entity)); + } + + /** + * Get the HREF link with reference `self` from the Hal links of the entity instance. + * + * @param entity The entity instance where to get the Hal links. + * @return the HREF link with rel `self`. + */ + public String getSelfLink(Object entity) { + HalLink halLink = getInstanceLinks(entity).get(SELF_REF); + if (halLink != null) { + return halLink.getHref(); + } + + return null; + } + + /** + * Get the Hal links using the entity type class. + * + * @param entityClass The entity class to get the Hal links. + * @return a map with the Hal links which keys are the rel attributes, and the values are the href attributes. + */ + protected abstract Map getClassLinks(Class entityClass); + + /** + * Get the Hal links using the entity instance. + * + * @param entity the Object instance. + * @return a map with the Hal links which keys are the rel attributes, and the values are the href attributes. + */ + protected abstract Map getInstanceLinks(Object entity); +} diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java new file mode 100644 index 0000000000000..80d5953b21f2b --- /dev/null +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java @@ -0,0 +1,30 @@ +package io.quarkus.hal; + +import java.util.Map; + +import javax.ws.rs.core.Link; + +public abstract class HalWrapper { + + private final Map links; + + public HalWrapper(Map links) { + this.links = links; + } + + public Map getLinks() { + return links; + } + + /** + * This method is used by Rest Data Panache to programmatically add links to the Hal wrapper. + * + * @param links The links to add into the Hal wrapper. + */ + @SuppressWarnings("unused") + public void addLinks(Link... links) { + for (Link link : links) { + this.links.put(link.getRel(), new HalLink(link.getUri().toString())); + } + } +} diff --git a/extensions/hal/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/hal/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..67954d3344abf --- /dev/null +++ b/extensions/hal/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,15 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Hypertext Application Language (HAL)" +metadata: + keywords: + - "jsonb" + - "json-b" + - "jackson" + - "hal" + - "rest" + - "jaxrs" + - "links" + categories: + - "web" + status: "experimental" \ No newline at end of file diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/multiplepersistenceunits/MultiplePersistenceUnitsCdiEntityManagerTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/multiplepersistenceunits/MultiplePersistenceUnitsCdiEntityManagerTest.java index 0b0d76fe1408e..e54a57bd14ca8 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/multiplepersistenceunits/MultiplePersistenceUnitsCdiEntityManagerTest.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/multiplepersistenceunits/MultiplePersistenceUnitsCdiEntityManagerTest.java @@ -1,10 +1,14 @@ package io.quarkus.hibernate.orm.multiplepersistenceunits; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; +import javax.enterprise.context.ContextNotActiveException; +import javax.enterprise.context.control.ActivateRequestContext; import javax.inject.Inject; import javax.persistence.EntityManager; +import javax.persistence.TransactionRequiredException; import javax.transaction.Transactional; import org.junit.jupiter.api.Test; @@ -39,7 +43,7 @@ public class MultiplePersistenceUnitsCdiEntityManagerTest { @Test @Transactional - public void testDefault() { + public void defaultEntityManagerInTransaction() { DefaultEntity defaultEntity = new DefaultEntity("default"); defaultEntityManager.persist(defaultEntity); @@ -47,9 +51,34 @@ public void testDefault() { assertEquals(defaultEntity.getName(), savedDefaultEntity.getName()); } + @Test + @ActivateRequestContext + public void defaultEntityManagerInRequestNoTransaction() { + // Reads are allowed + assertThatCode(() -> defaultEntityManager.createQuery("select count(*) from DefaultEntity")) + .doesNotThrowAnyException(); + // Writes are not + DefaultEntity defaultEntity = new DefaultEntity("default"); + assertThatThrownBy(() -> defaultEntityManager.persist(defaultEntity)) + .isInstanceOf(TransactionRequiredException.class) + .hasMessageContaining( + "Transaction is not active, consider adding @Transactional to your method to automatically activate one"); + } + + @Test + public void defaultEntityManagerNoRequestNoTransaction() { + DefaultEntity defaultEntity = new DefaultEntity("default"); + assertThatThrownBy(() -> defaultEntityManager.persist(defaultEntity)) + .isInstanceOf(ContextNotActiveException.class) + .hasMessageContainingAll( + "Cannot use the EntityManager/Session because neither a transaction nor a CDI request context is active", + "Consider adding @Transactional to your method to automatically activate a transaction", + "@ActivateRequestContext if you have valid reasons not to use transactions"); + } + @Test @Transactional - public void testUser() { + public void usersEntityManagerInTransaction() { User user = new User("gsmet"); usersEntityManager.persist(user); @@ -57,9 +86,34 @@ public void testUser() { assertEquals(user.getName(), savedUser.getName()); } + @Test + @ActivateRequestContext + public void usersEntityManagerInRequestNoTransaction() { + // Reads are allowed + assertThatCode(() -> usersEntityManager.createQuery("select count(*) from User")) + .doesNotThrowAnyException(); + // Writes are not + User user = new User("gsmet"); + assertThatThrownBy(() -> usersEntityManager.persist(user)) + .isInstanceOf(TransactionRequiredException.class) + .hasMessageContaining( + "Transaction is not active, consider adding @Transactional to your method to automatically activate one"); + } + + @Test + public void usersEntityManagerNoRequestNoTransaction() { + User user = new User("gsmet"); + assertThatThrownBy(() -> usersEntityManager.persist(user)) + .isInstanceOf(ContextNotActiveException.class) + .hasMessageContainingAll( + "Cannot use the EntityManager/Session because neither a transaction nor a CDI request context is active", + "Consider adding @Transactional to your method to automatically activate a transaction", + "@ActivateRequestContext if you have valid reasons not to use transactions"); + } + @Test @Transactional - public void testPlane() { + public void inventoryEntityManagerInTransaction() { Plane plane = new Plane("Airbus A380"); inventoryEntityManager.persist(plane); @@ -67,6 +121,31 @@ public void testPlane() { assertEquals(plane.getName(), savedPlane.getName()); } + @Test + @ActivateRequestContext + public void inventoryEntityManagerInRequestNoTransaction() { + // Reads are allowed + assertThatCode(() -> inventoryEntityManager.createQuery("select count(*) from Plane")) + .doesNotThrowAnyException(); + // Writes are not + Plane plane = new Plane("Airbus A380"); + assertThatThrownBy(() -> inventoryEntityManager.persist(plane)) + .isInstanceOf(TransactionRequiredException.class) + .hasMessageContaining( + "Transaction is not active, consider adding @Transactional to your method to automatically activate one"); + } + + @Test + public void inventoryEntityManagerNoRequestNoTransaction() { + Plane plane = new Plane("Airbus A380"); + assertThatThrownBy(() -> inventoryEntityManager.persist(plane)) + .isInstanceOf(ContextNotActiveException.class) + .hasMessageContainingAll( + "Cannot use the EntityManager/Session because neither a transaction nor a CDI request context is active", + "Consider adding @Transactional to your method to automatically activate a transaction", + "@ActivateRequestContext if you have valid reasons not to use transactions"); + } + @Test @Transactional public void testUserInInventoryEntityManager() { diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/multiplepersistenceunits/MultiplePersistenceUnitsCdiSessionTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/multiplepersistenceunits/MultiplePersistenceUnitsCdiSessionTest.java index db36a956e6b62..298c8cbcb3cb5 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/multiplepersistenceunits/MultiplePersistenceUnitsCdiSessionTest.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/multiplepersistenceunits/MultiplePersistenceUnitsCdiSessionTest.java @@ -1,9 +1,13 @@ package io.quarkus.hibernate.orm.multiplepersistenceunits; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; +import javax.enterprise.context.ContextNotActiveException; +import javax.enterprise.context.control.ActivateRequestContext; import javax.inject.Inject; +import javax.persistence.TransactionRequiredException; import javax.transaction.Transactional; import org.hibernate.Session; @@ -39,7 +43,7 @@ public class MultiplePersistenceUnitsCdiSessionTest { @Test @Transactional - public void testDefault() { + public void defaultEntityManagerInTransaction() { DefaultEntity defaultEntity = new DefaultEntity("default"); defaultSession.persist(defaultEntity); @@ -47,9 +51,34 @@ public void testDefault() { assertEquals(defaultEntity.getName(), savedDefaultEntity.getName()); } + @Test + @ActivateRequestContext + public void defaultEntityManagerInRequestNoTransaction() { + // Reads are allowed + assertThatCode(() -> defaultSession.createQuery("select count(*) from DefaultEntity")) + .doesNotThrowAnyException(); + // Writes are not + DefaultEntity defaultEntity = new DefaultEntity("default"); + assertThatThrownBy(() -> defaultSession.persist(defaultEntity)) + .isInstanceOf(TransactionRequiredException.class) + .hasMessageContaining( + "Transaction is not active, consider adding @Transactional to your method to automatically activate one"); + } + + @Test + public void defaultEntityManagerNoRequestNoTransaction() { + DefaultEntity defaultEntity = new DefaultEntity("default"); + assertThatThrownBy(() -> defaultSession.persist(defaultEntity)) + .isInstanceOf(ContextNotActiveException.class) + .hasMessageContainingAll( + "Cannot use the EntityManager/Session because neither a transaction nor a CDI request context is active", + "Consider adding @Transactional to your method to automatically activate a transaction", + "@ActivateRequestContext if you have valid reasons not to use transactions"); + } + @Test @Transactional - public void testUser() { + public void usersEntityManagerInTransaction() { User user = new User("gsmet"); usersSession.persist(user); @@ -57,9 +86,34 @@ public void testUser() { assertEquals(user.getName(), savedUser.getName()); } + @Test + @ActivateRequestContext + public void usersEntityManagerInRequestNoTransaction() { + // Reads are allowed + assertThatCode(() -> usersSession.createQuery("select count(*) from User")) + .doesNotThrowAnyException(); + // Writes are not + User user = new User("gsmet"); + assertThatThrownBy(() -> usersSession.persist(user)) + .isInstanceOf(TransactionRequiredException.class) + .hasMessageContaining( + "Transaction is not active, consider adding @Transactional to your method to automatically activate one"); + } + + @Test + public void usersEntityManagerNoRequestNoTransaction() { + User user = new User("gsmet"); + assertThatThrownBy(() -> usersSession.persist(user)) + .isInstanceOf(ContextNotActiveException.class) + .hasMessageContainingAll( + "Cannot use the EntityManager/Session because neither a transaction nor a CDI request context is active", + "Consider adding @Transactional to your method to automatically activate a transaction", + "@ActivateRequestContext if you have valid reasons not to use transactions"); + } + @Test @Transactional - public void testPlane() { + public void inventoryEntityManagerInTransaction() { Plane plane = new Plane("Airbus A380"); inventorySession.persist(plane); @@ -67,6 +121,31 @@ public void testPlane() { assertEquals(plane.getName(), savedPlane.getName()); } + @Test + @ActivateRequestContext + public void inventoryEntityManagerInRequestNoTransaction() { + // Reads are allowed + assertThatCode(() -> inventorySession.createQuery("select count(*) from Plane")) + .doesNotThrowAnyException(); + // Writes are not + Plane plane = new Plane("Airbus A380"); + assertThatThrownBy(() -> inventorySession.persist(plane)) + .isInstanceOf(TransactionRequiredException.class) + .hasMessageContaining( + "Transaction is not active, consider adding @Transactional to your method to automatically activate one"); + } + + @Test + public void inventoryEntityManagerNoRequestNoTransaction() { + Plane plane = new Plane("Airbus A380"); + assertThatThrownBy(() -> inventorySession.persist(plane)) + .isInstanceOf(ContextNotActiveException.class) + .hasMessageContainingAll( + "Cannot use the EntityManager/Session because neither a transaction nor a CDI request context is active", + "Consider adding @Transactional to your method to automatically activate a transaction", + "@ActivateRequestContext if you have valid reasons not to use transactions"); + } + @Test @Transactional public void testUserInInventorySession() { diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/singlepersistenceunit/SinglePersistenceUnitCdiEntityManagerTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/singlepersistenceunit/SinglePersistenceUnitCdiEntityManagerTest.java index 4ec4a334aa694..f032a4230e5d0 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/singlepersistenceunit/SinglePersistenceUnitCdiEntityManagerTest.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/singlepersistenceunit/SinglePersistenceUnitCdiEntityManagerTest.java @@ -1,9 +1,14 @@ package io.quarkus.hibernate.orm.singlepersistenceunit; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; +import javax.enterprise.context.ContextNotActiveException; +import javax.enterprise.context.control.ActivateRequestContext; import javax.inject.Inject; import javax.persistence.EntityManager; +import javax.persistence.TransactionRequiredException; import javax.transaction.Transactional; import org.junit.jupiter.api.Test; @@ -24,7 +29,7 @@ public class SinglePersistenceUnitCdiEntityManagerTest { @Test @Transactional - public void test() { + public void inTransaction() { DefaultEntity defaultEntity = new DefaultEntity("default"); entityManager.persist(defaultEntity); @@ -32,4 +37,29 @@ public void test() { assertEquals(defaultEntity.getName(), savedDefaultEntity.getName()); } + @Test + @ActivateRequestContext + public void inRequestNoTransaction() { + // Reads are allowed + assertThatCode(() -> entityManager.createQuery("select count(*) from DefaultEntity")) + .doesNotThrowAnyException(); + // Writes are not + DefaultEntity defaultEntity = new DefaultEntity("default"); + assertThatThrownBy(() -> entityManager.persist(defaultEntity)) + .isInstanceOf(TransactionRequiredException.class) + .hasMessageContaining( + "Transaction is not active, consider adding @Transactional to your method to automatically activate one"); + } + + @Test + public void noRequestNoTransaction() { + DefaultEntity defaultEntity = new DefaultEntity("default"); + assertThatThrownBy(() -> entityManager.persist(defaultEntity)) + .isInstanceOf(ContextNotActiveException.class) + .hasMessageContainingAll( + "Cannot use the EntityManager/Session because neither a transaction nor a CDI request context is active", + "Consider adding @Transactional to your method to automatically activate a transaction", + "@ActivateRequestContext if you have valid reasons not to use transactions"); + } + } diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/singlepersistenceunit/SinglePersistenceUnitCdiSessionTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/singlepersistenceunit/SinglePersistenceUnitCdiSessionTest.java index a5b83067021a0..e6745a17c01f7 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/singlepersistenceunit/SinglePersistenceUnitCdiSessionTest.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/singlepersistenceunit/SinglePersistenceUnitCdiSessionTest.java @@ -1,8 +1,13 @@ package io.quarkus.hibernate.orm.singlepersistenceunit; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; +import javax.enterprise.context.ContextNotActiveException; +import javax.enterprise.context.control.ActivateRequestContext; import javax.inject.Inject; +import javax.persistence.TransactionRequiredException; import javax.transaction.Transactional; import org.hibernate.Session; @@ -24,7 +29,7 @@ public class SinglePersistenceUnitCdiSessionTest { @Test @Transactional - public void test() { + public void inTransaction() { DefaultEntity defaultEntity = new DefaultEntity("default"); session.persist(defaultEntity); @@ -32,4 +37,29 @@ public void test() { assertEquals(defaultEntity.getName(), savedDefaultEntity.getName()); } + @Test + @ActivateRequestContext + public void inRequestNoTransaction() { + // Reads are allowed + assertThatCode(() -> session.createQuery("select count(*) from DefaultEntity")) + .doesNotThrowAnyException(); + // Writes are not + DefaultEntity defaultEntity = new DefaultEntity("default"); + assertThatThrownBy(() -> session.persist(defaultEntity)) + .isInstanceOf(TransactionRequiredException.class) + .hasMessageContaining( + "Transaction is not active, consider adding @Transactional to your method to automatically activate one"); + } + + @Test + public void noRequestNoTransaction() { + DefaultEntity defaultEntity = new DefaultEntity("default"); + assertThatThrownBy(() -> session.persist(defaultEntity)) + .isInstanceOf(ContextNotActiveException.class) + .hasMessageContainingAll( + "Cannot use the EntityManager/Session because neither a transaction nor a CDI request context is active", + "Consider adding @Transactional to your method to automatically activate a transaction", + "@ActivateRequestContext if you have valid reasons not to use transactions"); + } + } diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/sql_load_script/ImportMultipleSqlLoadScriptsFileAbsentTestCase.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/sql_load_script/ImportMultipleSqlLoadScriptsFileAbsentTestCase.java old mode 100755 new mode 100644 diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/sql_load_script/ImportMultipleSqlLoadScriptsTestCase.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/sql_load_script/ImportMultipleSqlLoadScriptsTestCase.java old mode 100755 new mode 100644 diff --git a/extensions/hibernate-orm/deployment/src/test/resources/application-import-multiple-load-scripts-test.properties b/extensions/hibernate-orm/deployment/src/test/resources/application-import-multiple-load-scripts-test.properties old mode 100755 new mode 100644 diff --git a/extensions/hibernate-orm/deployment/src/test/resources/import-multiple-load-scripts-1.sql b/extensions/hibernate-orm/deployment/src/test/resources/import-multiple-load-scripts-1.sql old mode 100755 new mode 100644 diff --git a/extensions/hibernate-orm/deployment/src/test/resources/import-multiple-load-scripts-2.sql b/extensions/hibernate-orm/deployment/src/test/resources/import-multiple-load-scripts-2.sql old mode 100755 new mode 100644 diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedSession.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedSession.java index 5229c4ca38c97..6c591049a4141 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedSession.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedSession.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; +import javax.enterprise.context.ContextNotActiveException; import javax.enterprise.inject.Instance; import javax.persistence.EntityGraph; import javax.persistence.EntityManagerFactory; @@ -49,6 +50,7 @@ import org.hibernate.query.Query; import org.hibernate.stat.SessionStatistics; +import io.quarkus.arc.Arc; import io.quarkus.hibernate.orm.runtime.RequestScopedSessionHolder; import io.quarkus.runtime.BlockingOperationControl; import io.quarkus.runtime.BlockingOperationNotAllowedException; @@ -96,13 +98,15 @@ SessionResult acquireSession() { // - org.hibernate.internal.SessionImpl.beforeTransactionCompletion // - org.hibernate.internal.SessionImpl.afterTransactionCompletion return new SessionResult(newSession, false, true); - } else { - //this will throw an exception if the request scope is not active - //this is expected as either the request scope or an active transaction - //is required to properly managed the EM lifecycle + } else if (Arc.container().requestContext().isActive()) { RequestScopedSessionHolder requestScopedSessions = this.requestScopedSessions.get(); return new SessionResult(requestScopedSessions.getOrCreateSession(unitName, sessionFactory), false, false); + } else { + throw new ContextNotActiveException( + "Cannot use the EntityManager/Session because neither a transaction nor a CDI request context is active." + + " Consider adding @Transactional to your method to automatically activate a transaction," + + " or @ActivateRequestContext if you have valid reasons not to use transactions."); } } diff --git a/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java b/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java index 7c8b6f1973c6c..e1bed0663bbb3 100644 --- a/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java +++ b/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java @@ -54,7 +54,6 @@ import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.BuiltinScope; import io.quarkus.arc.processor.DotNames; -import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -142,14 +141,15 @@ void registerAdditionalBeans(HibernateValidatorRecorder hibernateValidatorRecord // The CDI interceptor which will validate the methods annotated with @MethodValidated additionalBeans.produce(new AdditionalBeanBuildItem(MethodValidationInterceptor.class)); + additionalBeans.produce(new AdditionalBeanBuildItem( + "io.quarkus.hibernate.validator.runtime.locale.LocaleResolversWrapper")); + if (capabilities.isPresent(Capability.RESTEASY)) { // The CDI interceptor which will validate the methods annotated with @JaxrsEndPointValidated additionalBeans.produce(new AdditionalBeanBuildItem( "io.quarkus.hibernate.validator.runtime.jaxrs.JaxrsEndPointValidationInterceptor")); additionalBeans.produce(new AdditionalBeanBuildItem( - "io.quarkus.hibernate.validator.runtime.locale.LocaleResolversWrapper")); - additionalBeans.produce(new AdditionalBeanBuildItem( - "io.quarkus.hibernate.validator.runtime.locale.ResteasyContextLocaleResolver")); + "io.quarkus.hibernate.validator.runtime.locale.ResteasyClassicLocaleResolver")); syntheticBeanBuildItems.produce(SyntheticBeanBuildItem.configure(ResteasyConfigSupport.class) .scope(Singleton.class) .unremovable() @@ -161,15 +161,7 @@ void registerAdditionalBeans(HibernateValidatorRecorder hibernateValidatorRecord additionalBeans.produce(new AdditionalBeanBuildItem( "io.quarkus.hibernate.validator.runtime.jaxrs.ResteasyReactiveEndPointValidationInterceptor")); additionalBeans.produce(new AdditionalBeanBuildItem( - "io.quarkus.hibernate.validator.runtime.locale.LocaleResolversWrapper")); - additionalBeans.produce(new AdditionalBeanBuildItem( - "io.quarkus.hibernate.validator.runtime.locale.VertxLocaleResolver")); - } - if (capabilities.isPresent(Capability.SMALLRYE_GRAPHQL)) { - additionalBeans.produce(new AdditionalBeanBuildItem( - "io.quarkus.hibernate.validator.runtime.locale.LocaleResolversWrapper")); - additionalBeans.produce(new AdditionalBeanBuildItem( - "io.quarkus.hibernate.validator.runtime.locale.VertxLocaleResolver")); + "io.quarkus.hibernate.validator.runtime.locale.ResteasyReactiveLocaleResolver")); } // A constraint validator with an injection point but no scope is added as @Dependent @@ -364,11 +356,8 @@ void optionalResouceBundles(BuildProducer re AbstractMessageInterpolator.CONTRIBUTOR_VALIDATION_MESSAGES }; for (String potentialHibernateValidatorResourceBundle : potentialHibernateValidatorResourceBundles) { - for (ClassPathElement cpe : QuarkusClassLoader.getElements(potentialHibernateValidatorResourceBundle, false)) { - if (cpe.isRuntime()) { - resourceBundles.produce(new NativeImageResourceBundleBuildItem(potentialHibernateValidatorResourceBundle)); - break; - } + if (QuarkusClassLoader.isResourcePresentAtRuntime(potentialHibernateValidatorResourceBundle)) { + resourceBundles.produce(new NativeImageResourceBundleBuildItem(potentialHibernateValidatorResourceBundle)); } } } diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/LocaleResolversWrapper.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/LocaleResolversWrapper.java index 31af15b426010..53b5d41217a0a 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/LocaleResolversWrapper.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/LocaleResolversWrapper.java @@ -18,14 +18,16 @@ public class LocaleResolversWrapper implements LocaleResolver { @Inject - Instance resolvers; + Instance resolvers; @Override public Locale resolve(LocaleResolverContext context) { - for (AbstractLocaleResolver resolver : resolvers) { - Locale locale = resolver.resolve(context); - if (locale != null) { - return locale; + for (LocaleResolver resolver : resolvers) { + if (!resolver.equals(this)) { + Locale locale = resolver.resolve(context); + if (locale != null) { + return locale; + } } } return context.getDefaultLocale(); diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/ResteasyContextLocaleResolver.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/ResteasyClassicLocaleResolver.java similarity index 89% rename from extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/ResteasyContextLocaleResolver.java rename to extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/ResteasyClassicLocaleResolver.java index a0f4f0ef3359b..237df733e51b2 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/ResteasyContextLocaleResolver.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/ResteasyClassicLocaleResolver.java @@ -9,7 +9,7 @@ import org.jboss.resteasy.core.ResteasyContext; @Singleton -public class ResteasyContextLocaleResolver extends AbstractLocaleResolver { +public class ResteasyClassicLocaleResolver extends AbstractLocaleResolver { @Override protected Map> getHeaders() { diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/VertxLocaleResolver.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/ResteasyReactiveLocaleResolver.java similarity index 93% rename from extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/VertxLocaleResolver.java rename to extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/ResteasyReactiveLocaleResolver.java index c21bdf6d62f52..e40a354c3e221 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/VertxLocaleResolver.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/locale/ResteasyReactiveLocaleResolver.java @@ -16,7 +16,7 @@ * Currently used for handling GraphQL requests. */ @Singleton -public class VertxLocaleResolver extends AbstractLocaleResolver { +public class ResteasyReactiveLocaleResolver extends AbstractLocaleResolver { @Inject CurrentVertxRequest currentVertxRequest; diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java index 9a55fc0593ab6..8d4075e42114b 100644 --- a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java +++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java @@ -20,13 +20,13 @@ import org.jetbrains.annotations.NotNull; import io.quarkus.deployment.Feature; -import io.quarkus.deployment.IsDockerWorking; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.console.ConsoleInstalledBuildItem; import io.quarkus.deployment.console.StartupLogCompressor; @@ -56,10 +56,10 @@ public class InfinispanDevServiceProcessor { private static volatile List devServices; private static volatile InfinispanClientDevServiceBuildTimeConfig.DevServiceConfiguration capturedDevServicesConfiguration; private static volatile boolean first = true; - private static volatile Boolean dockerRunning = null; @BuildStep(onlyIfNot = IsNormal.class, onlyIf = { GlobalDevServicesConfig.Enabled.class }) public List startInfinispanContainers(LaunchModeBuildItem launchMode, + DockerStatusBuildItem dockerStatusBuildItem, List devServicesSharedNetworkBuildItem, InfinispanClientDevServiceBuildTimeConfig config, Optional consoleInstalledBuildItem, @@ -91,11 +91,11 @@ public List startInfinispanContainers(LaunchModeBuil (launchMode.isTest() ? "(test) " : "") + "Infinispan Dev Services Starting:", consoleInstalledBuildItem, loggingSetupBuildItem); try { - RunningDevService devService = startContainer(config.devService.devservices, + RunningDevService devService = startContainer(dockerStatusBuildItem, config.devService.devservices, launchMode.getLaunchMode(), !devServicesSharedNetworkBuildItem.isEmpty(), devServicesConfig.timeout); if (devService == null) { - compressor.close(); + compressor.closeAndDumpCaptured(); return null; } newDevServices.add(devService); @@ -112,7 +112,6 @@ public List startInfinispanContainers(LaunchModeBuil if (first) { first = false; Runnable closeTask = () -> { - dockerRunning = null; if (devServices != null) { for (Closeable closeable : devServices) { try { @@ -131,7 +130,7 @@ public List startInfinispanContainers(LaunchModeBuil return devServices.stream().map(RunningDevService::toBuildItem).collect(Collectors.toList()); } - private RunningDevService startContainer( + private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuildItem, InfinispanDevServicesConfig devServicesConfig, LaunchMode launchMode, boolean useSharedNetwork, Optional timeout) { if (!devServicesConfig.enabled) { @@ -148,11 +147,7 @@ private RunningDevService startContainer( return null; } - if (dockerRunning == null) { - dockerRunning = new IsDockerWorking.IsDockerRunningSilent().getAsBoolean(); - } - - if (!dockerRunning) { + if (!dockerStatusBuildItem.isDockerAvailable()) { log.warn("Please configure 'quarkus.infinispan-client.server-list' or get a working docker instance"); return null; } 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 old mode 100755 new mode 100644 index 8c8f677d92561..b43497f26ce67 --- 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 @@ -32,6 +32,7 @@ import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; @@ -99,6 +100,8 @@ void register( new ReflectiveClassBuildItem(true, false, "com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector", "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")); IndexView index = combinedIndexBuildItem.getIndex(); @@ -231,10 +234,8 @@ void autoRegisterModules(BuildProducer classPat private void registerModuleIfOnClassPath(String moduleClassName, BuildProducer classPathJacksonModules) { - try { - Class.forName(moduleClassName, false, Thread.currentThread().getContextClassLoader()); + if (QuarkusClassLoader.isClassPresentAtRuntime(moduleClassName)) { classPathJacksonModules.produce(new ClassPathJacksonModuleBuildItem(moduleClassName)); - } catch (Exception ignored) { } } diff --git a/extensions/jsonb/deployment/src/main/java/io/quarkus/jsonb/deployment/JsonbProcessor.java b/extensions/jsonb/deployment/src/main/java/io/quarkus/jsonb/deployment/JsonbProcessor.java old mode 100755 new mode 100644 diff --git a/extensions/jsonp/deployment/src/main/java/io/quarkus/jsonp/deployment/JsonpProcessor.java b/extensions/jsonp/deployment/src/main/java/io/quarkus/jsonp/deployment/JsonpProcessor.java old mode 100755 new mode 100644 diff --git a/extensions/jsonp/runtime/pom.xml b/extensions/jsonp/runtime/pom.xml index 25796f087106c..00f95364afe33 100644 --- a/extensions/jsonp/runtime/pom.xml +++ b/extensions/jsonp/runtime/pom.xml @@ -53,4 +53,28 @@ + + + + jakarta-rewrite + + + jakarta-rewrite + + + + + + org.openrewrite.maven + rewrite-maven-plugin + + + io.quarkus.jakarta-json-switch + + + + + + + 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 6e603d381bdde..22ecf83c495b7 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 @@ -25,13 +25,13 @@ import org.testcontainers.utility.DockerImageName; import io.quarkus.deployment.Feature; -import io.quarkus.deployment.IsDockerWorking; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.console.ConsoleInstalledBuildItem; import io.quarkus.deployment.console.StartupLogCompressor; @@ -65,10 +65,9 @@ public class DevServicesKafkaProcessor { static volatile KafkaDevServiceCfg cfg; static volatile boolean first = true; - private final IsDockerWorking isDockerWorking = new IsDockerWorking(true); - @BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) public DevServicesResultBuildItem startKafkaDevService( + DockerStatusBuildItem dockerStatusBuildItem, LaunchModeBuildItem launchMode, KafkaBuildTimeConfig kafkaClientBuildTimeConfig, List devServicesSharedNetworkBuildItem, @@ -91,10 +90,14 @@ public DevServicesResultBuildItem startKafkaDevService( (launchMode.isTest() ? "(test) " : "") + "Kafka Dev Services Starting:", consoleInstalledBuildItem, loggingSetupBuildItem); try { - devService = startKafka(configuration, launchMode, + devService = startKafka(dockerStatusBuildItem, configuration, launchMode, !devServicesSharedNetworkBuildItem.isEmpty(), devServicesConfig.timeout); - compressor.close(); + if (devService == null) { + compressor.closeAndDumpCaptured(); + } else { + compressor.close(); + } } catch (Throwable t) { compressor.closeAndDumpCaptured(); throw new RuntimeException(t); @@ -188,7 +191,7 @@ private void shutdownBroker() { } } - private RunningDevService startKafka(KafkaDevServiceCfg config, + private RunningDevService startKafka(DockerStatusBuildItem dockerStatusBuildItem, KafkaDevServiceCfg config, LaunchModeBuildItem launchMode, boolean useSharedNetwork, Optional timeout) { if (!config.devServicesEnabled) { // explicitly disabled @@ -208,7 +211,7 @@ private RunningDevService startKafka(KafkaDevServiceCfg config, return null; } - if (!isDockerWorking.getAsBoolean()) { + if (!dockerStatusBuildItem.isDockerAvailable()) { log.warn( "Docker isn't working, please configure the Kafka bootstrap servers property (kafka.bootstrap.servers)."); return null; 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 6f508df693bc2..138d5752fc483 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 @@ -56,6 +56,7 @@ import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; @@ -177,13 +178,11 @@ void relaxSaslElytron(BuildProducer config // If elytron is on the classpath and the Kafka connection uses SASL, the Elytron client SASL implementation // is stricter than what Kafka expects. In this case, configure the SASL client to relax some constraints. // See https://github.com/quarkusio/quarkus/issues/20088. - try { - Class.forName("org.wildfly.security.sasl.gssapi.AbstractGssapiMechanism", false, - Thread.currentThread().getContextClassLoader()); - config.produce(new RunTimeConfigurationDefaultBuildItem("kafka.wildfly.sasl.relax-compliance", "true")); - } catch (Exception e) { - // AbstractGssapiMechanism is not on the classpath, do not set wildfly.sasl.relax-compliance + if (!QuarkusClassLoader.isClassPresentAtRuntime("org.wildfly.security.sasl.gssapi.AbstractGssapiMechanism")) { + return; } + + config.produce(new RunTimeConfigurationDefaultBuildItem("kafka.wildfly.sasl.relax-compliance", "true")); } @BuildStep @@ -299,41 +298,35 @@ void checkBoostrapServers(KafkaRecorder recorder, Capabilities capabilities) { private void handleOpenTracing(BuildProducer reflectiveClass, Capabilities capabilities) { //opentracing contrib kafka interceptors: https://github.com/opentracing-contrib/java-kafka-client - if (capabilities.isPresent(Capability.OPENTRACING)) { - try { - Class.forName("io.opentracing.contrib.kafka.TracingProducerInterceptor", false, - Thread.currentThread().getContextClassLoader()); - reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, false, - "io.opentracing.contrib.kafka.TracingProducerInterceptor", - "io.opentracing.contrib.kafka.TracingConsumerInterceptor")); - } catch (ClassNotFoundException e) { - //ignore, opentracing contrib kafka is not in the classpath - } + if (!capabilities.isPresent(Capability.OPENTRACING) + || !QuarkusClassLoader.isClassPresentAtRuntime("io.opentracing.contrib.kafka.TracingProducerInterceptor")) { + return; } + + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, false, + "io.opentracing.contrib.kafka.TracingProducerInterceptor", + "io.opentracing.contrib.kafka.TracingConsumerInterceptor")); } private void handleStrimziOAuth(BuildProducer reflectiveClass) { - try { - Class.forName("io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler", false, - Thread.currentThread().getContextClassLoader()); - - 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")); - } catch (ClassNotFoundException e) { - //ignore, Strimzi OAuth Client is not on the classpath + if (!QuarkusClassLoader.isClassPresentAtRuntime("io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler")) { + return; } + + 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")); } private void handleAvro(BuildProducer reflectiveClass, @@ -344,64 +337,14 @@ private void handleAvro(BuildProducer reflectiveClass, // Avro - for both Confluent and Apicurio // --- Confluent --- - try { - Class.forName("io.confluent.kafka.serializers.KafkaAvroDeserializer", false, - Thread.currentThread().getContextClassLoader()); - reflectiveClass - .produce(new ReflectiveClassBuildItem(true, false, - "io.confluent.kafka.serializers.KafkaAvroDeserializer", - "io.confluent.kafka.serializers.KafkaAvroSerializer")); - - reflectiveClass - .produce(new ReflectiveClassBuildItem(true, false, false, - "io.confluent.kafka.serializers.context.NullContextNameStrategy")); - - reflectiveClass - .produce(new ReflectiveClassBuildItem(true, true, false, - "io.confluent.kafka.serializers.subject.TopicNameStrategy", - "io.confluent.kafka.serializers.subject.TopicRecordNameStrategy", - "io.confluent.kafka.serializers.subject.RecordNameStrategy")); - - reflectiveClass - .produce(new ReflectiveClassBuildItem(true, true, false, - "io.confluent.kafka.schemaregistry.client.rest.entities.ErrorMessage", - "io.confluent.kafka.schemaregistry.client.rest.entities.Schema", - "io.confluent.kafka.schemaregistry.client.rest.entities.Config", - "io.confluent.kafka.schemaregistry.client.rest.entities.SchemaReference", - "io.confluent.kafka.schemaregistry.client.rest.entities.SchemaString", - "io.confluent.kafka.schemaregistry.client.rest.entities.SchemaTypeConverter", - "io.confluent.kafka.schemaregistry.client.rest.entities.ServerClusterId", - "io.confluent.kafka.schemaregistry.client.rest.entities.SujectVersion")); - - reflectiveClass - .produce(new ReflectiveClassBuildItem(true, true, false, - "io.confluent.kafka.schemaregistry.client.rest.entities.requests.CompatibilityCheckResponse", - "io.confluent.kafka.schemaregistry.client.rest.entities.requests.ConfigUpdateRequest", - "io.confluent.kafka.schemaregistry.client.rest.entities.requests.ModeGetResponse", - "io.confluent.kafka.schemaregistry.client.rest.entities.requests.ModeUpdateRequest", - "io.confluent.kafka.schemaregistry.client.rest.entities.requests.RegisterSchemaRequest", - "io.confluent.kafka.schemaregistry.client.rest.entities.requests.RegisterSchemaResponse")); - } catch (ClassNotFoundException e) { - //ignore, Confluent Avro is not in the classpath - } - - try { - Class.forName("io.confluent.kafka.schemaregistry.client.security.basicauth.BasicAuthCredentialProvider", false, - Thread.currentThread().getContextClassLoader()); - serviceProviders - .produce(new ServiceProviderBuildItem( - "io.confluent.kafka.schemaregistry.client.security.basicauth.BasicAuthCredentialProvider", - "io.confluent.kafka.schemaregistry.client.security.basicauth.SaslBasicAuthCredentialProvider", - "io.confluent.kafka.schemaregistry.client.security.basicauth.UrlBasicAuthCredentialProvider", - "io.confluent.kafka.schemaregistry.client.security.basicauth.UserInfoCredentialProvider")); - } catch (ClassNotFoundException e) { - // ignore, Confluent schema registry client not in the classpath + if (QuarkusClassLoader.isClassPresentAtRuntime("io.confluent.kafka.serializers.KafkaAvroDeserializer") + && !capabilities.isPresent(Capability.CONFLUENT_REGISTRY_AVRO)) { + throw new RuntimeException( + "Confluent Avro classes detected, please use the quarkus-confluent-registry-avro extension"); } // --- Apicurio Registry 1.x --- - try { - Class.forName("io.apicurio.registry.utils.serde.AvroKafkaDeserializer", false, - Thread.currentThread().getContextClassLoader()); + if (QuarkusClassLoader.isClassPresentAtRuntime("io.apicurio.registry.utils.serde.AvroKafkaDeserializer")) { reflectiveClass.produce( new ReflectiveClassBuildItem(true, true, false, "io.apicurio.registry.utils.serde.AvroKafkaDeserializer", @@ -423,22 +366,13 @@ private void handleAvro(BuildProducer reflectiveClass, // Apicurio uses dynamic proxies, register them proxies.produce(new NativeImageProxyDefinitionBuildItem("io.apicurio.registry.client.RegistryService", "java.lang.AutoCloseable")); - - } catch (ClassNotFoundException e) { - // ignore, Apicurio Avro is not in the classpath } // --- Apicurio Registry 2.x --- - try { - Class.forName("io.apicurio.registry.serde.avro.AvroKafkaDeserializer", false, - Thread.currentThread().getContextClassLoader()); - - if (!capabilities.isPresent(Capability.APICURIO_REGISTRY_AVRO)) { - throw new RuntimeException( - "Apicurio Registry 2.x Avro classes detected, please use the quarkus-apicurio-registry-avro extension"); - } - } catch (ClassNotFoundException e) { - // ignore, Apicurio Avro is not in the classpath + if (QuarkusClassLoader.isClassPresentAtRuntime("io.apicurio.registry.serde.avro.AvroKafkaDeserializer") + && !capabilities.isPresent(Capability.APICURIO_REGISTRY_AVRO)) { + throw new RuntimeException( + "Apicurio Registry 2.x Avro classes detected, please use the quarkus-apicurio-registry-avro extension"); } } diff --git a/extensions/keycloak-admin-client-reactive/runtime/pom.xml b/extensions/keycloak-admin-client-reactive/runtime/pom.xml index c596ca7c1c8db..f1215eed58b43 100644 --- a/extensions/keycloak-admin-client-reactive/runtime/pom.xml +++ b/extensions/keycloak-admin-client-reactive/runtime/pom.xml @@ -74,4 +74,28 @@ + + + + jakarta-rewrite + + + jakarta-rewrite + + + + + + org.openrewrite.maven + rewrite-maven-plugin + + + io.quarkus.keycloak-admin-client + + + + + + + diff --git a/extensions/keycloak-admin-client/runtime/pom.xml b/extensions/keycloak-admin-client/runtime/pom.xml index f36648ad0ef28..17a47feddbc50 100644 --- a/extensions/keycloak-admin-client/runtime/pom.xml +++ b/extensions/keycloak-admin-client/runtime/pom.xml @@ -90,4 +90,28 @@ + + + + jakarta-rewrite + + + jakarta-rewrite + + + + + + org.openrewrite.maven + rewrite-maven-plugin + + + io.quarkus.keycloak-admin-client + + + + + + + diff --git a/extensions/keycloak-authorization/deployment/pom.xml b/extensions/keycloak-authorization/deployment/pom.xml index 3f6f8823b5298..2891f83f0f1ee 100644 --- a/extensions/keycloak-authorization/deployment/pom.xml +++ b/extensions/keycloak-authorization/deployment/pom.xml @@ -84,113 +84,4 @@ - - - - test-keycloak - - - test-containers - - - - - - maven-surefire-plugin - - false - - ${keycloak.url} - - - - - - - - - docker-keycloak - - - start-containers - - - - http://localhost:8180/auth - - - - - io.fabric8 - docker-maven-plugin - - - - ${keycloak.docker.legacy.image} - quarkus-test-keycloak - - - 8180:8080 - - - admin - admin - -server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djava.awt.headless=true -Dkeycloak.profile.feature.upload_scripts=enabled - - - 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} - - - - - - - - - diff --git a/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/KeycloakTestResource.java b/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/KeycloakTestResource.java deleted file mode 100644 index 732127c8cb7b0..0000000000000 --- a/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/KeycloakTestResource.java +++ /dev/null @@ -1,218 +0,0 @@ -package io.quarkus.keycloak.pep.test; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; - -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.KeycloakBuilder; -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.representations.idm.authorization.PolicyRepresentation; -import org.keycloak.representations.idm.authorization.ResourceRepresentation; -import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; - -import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; - -public class KeycloakTestResource implements QuarkusTestResourceLifecycleManager { - - private static final String KEYCLOAK_SERVER_URL = System.getProperty("keycloak.url", "http://localhost:8180/auth"); - private static final String KEYCLOAK_REALM = "quarkus"; - - private Keycloak keycloak; - - @Override - public Map start() { - - RealmRepresentation realm = createRealm(KEYCLOAK_REALM); - - realm.getClients().add(createClient("quarkus-app")); - realm.getUsers().add(createUser("alice", "user")); - realm.getUsers().add(createUser("admin", "user", "admin")); - realm.getUsers().add(createUser("jdoe", "user", "confidential")); - - keycloak = KeycloakBuilder.builder() - .serverUrl(KEYCLOAK_SERVER_URL) - .realm("master") - .clientId("admin-cli") - .username("admin") - .password("admin") - .build(); - keycloak.realms().create(realm); - - return Collections.emptyMap(); - } - - private static RealmRepresentation createRealm(String name) { - RealmRepresentation realm = new RealmRepresentation(); - - realm.setRealm(name); - realm.setEnabled(true); - realm.setUsers(new ArrayList<>()); - realm.setClients(new ArrayList<>()); - - RolesRepresentation roles = new RolesRepresentation(); - List realmRoles = new ArrayList<>(); - - roles.setRealm(realmRoles); - realm.setRoles(roles); - - realm.getRoles().getRealm().add(new RoleRepresentation("user", null, false)); - realm.getRoles().getRealm().add(new RoleRepresentation("admin", null, false)); - realm.getRoles().getRealm().add(new RoleRepresentation("confidential", null, false)); - - return realm; - } - - private static ClientRepresentation createClient(String clientId) { - ClientRepresentation client = new ClientRepresentation(); - - client.setClientId(clientId); - client.setPublicClient(false); - client.setSecret("secret"); - client.setDirectAccessGrantsEnabled(true); - client.setEnabled(true); - - client.setAuthorizationServicesEnabled(true); - - ResourceServerRepresentation authorizationSettings = new ResourceServerRepresentation(); - - authorizationSettings.setResources(new ArrayList<>()); - authorizationSettings.setPolicies(new ArrayList<>()); - - configurePermissionResourcePermission(authorizationSettings); - configureClaimBasedPermission(authorizationSettings); - configureHttpResponseClaimBasedPermission(authorizationSettings); - configureBodyClaimBasedPermission(authorizationSettings); - configurePaths(authorizationSettings); - - client.setAuthorizationSettings(authorizationSettings); - - return client; - } - - private static void configurePermissionResourcePermission(ResourceServerRepresentation settings) { - PolicyRepresentation policy = createJSPolicy("Confidential Policy", "var identity = $evaluation.context.identity;\n" + - "\n" + - "if (identity.hasRealmRole(\"confidential\")) {\n" + - "$evaluation.grant();\n" + - "}", settings); - createPermission(settings, createResource(settings, "Permission Resource", "/api/permission"), policy); - } - - private static void configureClaimBasedPermission(ResourceServerRepresentation settings) { - PolicyRepresentation policy = createJSPolicy("Claim-Based Policy", "var context = $evaluation.getContext();\n" - + "var attributes = context.getAttributes();\n" - + "\n" - + "if (attributes.containsValue('grant', 'true')) {\n" - + " $evaluation.grant();\n" - + "}", settings); - createPermission(settings, createResource(settings, "Claim Protected Resource", "/api/permission/claim-protected"), - policy); - } - - private static void configureHttpResponseClaimBasedPermission(ResourceServerRepresentation settings) { - PolicyRepresentation policy = createJSPolicy("Http Response Claim-Based Policy", - "var context = $evaluation.getContext();\n" - + "var attributes = context.getAttributes();\n" - + "\n" - + "if (attributes.containsValue('user-name', 'alice')) {\n" - + " $evaluation.grant();\n" - + "}", - settings); - createPermission(settings, createResource(settings, "Http Response Claim Protected Resource", - "/api/permission/http-response-claim-protected"), policy); - } - - private static void configureBodyClaimBasedPermission(ResourceServerRepresentation settings) { - PolicyRepresentation policy = createJSPolicy("Body Claim-Based Policy", - "var context = $evaluation.getContext();\n" - + "print(context.getAttributes().toMap());" - + "var attributes = context.getAttributes();\n" - + "\n" - + "if (attributes.containsValue('from-body', 'grant')) {\n" - + " $evaluation.grant();\n" - + "}", - settings); - createPermission(settings, createResource(settings, "Body Claim Protected Resource", - "/api/permission/body-claim"), policy); - } - - private static void configurePaths(ResourceServerRepresentation settings) { - createResource(settings, "Root", null); - createResource(settings, "API", "/api2/*"); - createResource(settings, "Hello", "/hello"); - } - - private static void createPermission(ResourceServerRepresentation settings, ResourceRepresentation resource, - PolicyRepresentation policy) { - PolicyRepresentation permission = new PolicyRepresentation(); - - permission.setName(resource.getName() + " Permission"); - permission.setType("resource"); - permission.setResources(new HashSet<>()); - permission.getResources().add(resource.getName()); - permission.setPolicies(new HashSet<>()); - permission.getPolicies().add(policy.getName()); - - settings.getPolicies().add(permission); - } - - private static ResourceRepresentation createResource(ResourceServerRepresentation authorizationSettings, String name, - String uri) { - ResourceRepresentation resource = new ResourceRepresentation(name); - - if (uri != null) { - resource.setUris(Collections.singleton(uri)); - } - - authorizationSettings.getResources().add(resource); - return resource; - } - - private static PolicyRepresentation createJSPolicy(String name, String code, ResourceServerRepresentation settings) { - PolicyRepresentation policy = new PolicyRepresentation(); - - policy.setName(name); - policy.setType("js"); - policy.setConfig(new HashMap<>()); - policy.getConfig().put("code", code); - - settings.getPolicies().add(policy); - - return policy; - } - - private static UserRepresentation createUser(String username, String... realmRoles) { - UserRepresentation user = new UserRepresentation(); - - user.setUsername(username); - user.setEnabled(true); - user.setCredentials(new ArrayList<>()); - user.setRealmRoles(Arrays.asList(realmRoles)); - - CredentialRepresentation credential = new CredentialRepresentation(); - - credential.setType(CredentialRepresentation.PASSWORD); - credential.setValue(username); - credential.setTemporary(false); - - user.getCredentials().add(credential); - - return user; - } - - @Override - public void stop() { - keycloak.realm(KEYCLOAK_REALM).remove(); - - } -} diff --git a/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/PolicyEnforcerTest.java b/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/PolicyEnforcerTest.java deleted file mode 100644 index 2fe1d68707503..0000000000000 --- a/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/PolicyEnforcerTest.java +++ /dev/null @@ -1,147 +0,0 @@ -package io.quarkus.keycloak.pep.test; - -import java.util.function.Supplier; - -import org.hamcrest.Matchers; -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 org.keycloak.representations.AccessTokenResponse; - -import io.quarkus.test.QuarkusDevModeTest; -import io.quarkus.test.common.QuarkusTestResource; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; - -/** - * @author Pedro Igor - */ -//to start docker manually -//docker run -p 8180:8080 --rm -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -e JAVA_OPTS="-Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djava.awt.headless=true -Dkeycloak.profile.feature.upload_scripts=enabled" quay.io/keycloak/keycloak:10.0.0 -@QuarkusTestResource(KeycloakTestResource.class) -public class PolicyEnforcerTest { - - private static final String KEYCLOAK_SERVER_URL = System.getProperty("keycloak.url", "http://localhost:8180/auth"); - private static final String KEYCLOAK_REALM = "quarkus"; - - @RegisterExtension - static QuarkusDevModeTest devModeTest = new QuarkusDevModeTest() - .setArchiveProducer(new Supplier<>() { - @Override - public JavaArchive get() { - return ShrinkWrap.create(JavaArchive.class) - .addAsResource("application.properties") - .addClasses(ProtectedResource.class, ProtectedResource2.class, PublicResource.class, - UsersResource.class); - } - }); - - @Test - public void testUserHasRoleConfidential() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/permission") - .then() - .statusCode(403); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission") - .then() - .statusCode(200) - .and().body(Matchers.containsString("Permission Resource")); - ; - RestAssured.given().auth().oauth2(getAccessToken("admin")) - .when().get("/api/permission") - .then() - .statusCode(403); - } - - @Test - public void testRequestParameterAsClaim() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/permission/claim-protected?grant=true") - .then() - .statusCode(200) - .and().body(Matchers.containsString("Claim Protected Resource")); - ; - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/permission/claim-protected?grant=false") - .then() - .statusCode(403); - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/permission/claim-protected") - .then() - .statusCode(403); - } - - @Test - public void testHttpResponseFromExternalServiceAsClaim() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/permission/http-response-claim-protected") - .then() - .statusCode(200) - .and().body(Matchers.containsString("Http Response Claim Protected Resource")); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/http-response-claim-protected") - .then() - .statusCode(403); - } - - @Test - public void testBodyClaim() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .contentType(ContentType.JSON) - .body("{\"from-body\": \"grant\"}") - .when() - .post("/api/permission/body-claim") - .then() - .statusCode(200) - .and().body(Matchers.containsString("Body Claim Protected Resource")); - } - - @Test - public void testPublicResource() { - RestAssured.given() - .when().get("/api/public") - .then() - .statusCode(204); - } - - @Test - public void testHealthCheck() { - RestAssured.given() - .when().get("/q/health/live") - .then() - .statusCode(200); - } - - @Test - public void testPathConfigurationPrecedenceWhenPathCacheNotDefined() { - RestAssured.given() - .when().get("/api2/resource") - .then() - .statusCode(401); - - RestAssured.given() - .when().get("/hello") - .then() - .statusCode(404); - - RestAssured.given() - .when().get("/") - .then() - .statusCode(404); - } - - private String getAccessToken(String userName) { - return RestAssured - .given() - .param("grant_type", "password") - .param("username", userName) - .param("password", userName) - .param("client_id", "quarkus-app") - .param("client_secret", "secret") - .when() - .post(KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM + "/protocol/openid-connect/token") - .as(AccessTokenResponse.class).getToken(); - } -} diff --git a/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/ProtectedResource.java b/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/ProtectedResource.java deleted file mode 100644 index 33a7d42dad4a6..0000000000000 --- a/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/ProtectedResource.java +++ /dev/null @@ -1,70 +0,0 @@ -package io.quarkus.keycloak.pep.test; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -import javax.inject.Inject; -import javax.security.auth.AuthPermission; -import javax.ws.rs.Consumes; -import javax.ws.rs.ForbiddenException; -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.Context; -import javax.ws.rs.core.MediaType; - -import org.keycloak.representations.idm.authorization.Permission; - -import io.quarkus.security.identity.SecurityIdentity; -import io.smallrye.mutiny.Uni; -import io.vertx.core.http.HttpServerRequest; - -@Path("/api/permission") -public class ProtectedResource { - - @Inject - SecurityIdentity identity; - - @GET - @Produces(MediaType.APPLICATION_JSON) - public Uni> permissions() { - return identity.checkPermission(new AuthPermission("Permission Resource")) - .onItem().transform(new Function>() { - @Override - public List apply(Boolean granted) { - if (granted) { - return identity.getAttribute("permissions"); - } - throw new ForbiddenException(); - } - }); - } - - @Path("/claim-protected") - @GET - @Produces(MediaType.APPLICATION_JSON) - public List claimProtected() { - return identity.getAttribute("permissions"); - } - - @Path("/http-response-claim-protected") - @GET - @Produces(MediaType.APPLICATION_JSON) - public List httpResponseClaimProtected() { - return identity.getAttribute("permissions"); - } - - @Path("/body-claim") - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public List bodyClaim(Map body, @Context HttpServerRequest request) { - if (body == null || !body.containsKey("from-body")) { - return Collections.emptyList(); - } - return identity.getAttribute("permissions"); - } -} diff --git a/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/ProtectedResource2.java b/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/ProtectedResource2.java deleted file mode 100644 index 8d189a669a29d..0000000000000 --- a/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/ProtectedResource2.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.quarkus.keycloak.pep.test; - -import javax.ws.rs.GET; -import javax.ws.rs.Path; - -import io.quarkus.security.Authenticated; - -@Path("/api2/resource") -@Authenticated -public class ProtectedResource2 { - - @GET - public String testResource() { - // This method must not be invoked - throw new RuntimeException(); - } -} diff --git a/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/PublicResource.java b/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/PublicResource.java deleted file mode 100644 index 76b03add47c91..0000000000000 --- a/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/PublicResource.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.quarkus.keycloak.pep.test; - -import javax.ws.rs.GET; -import javax.ws.rs.Path; - -@Path("/api/public") -public class PublicResource { - - @GET - public void serve() { - // no-op - } -} diff --git a/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/UsersResource.java b/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/UsersResource.java deleted file mode 100644 index 43e55ea4756b2..0000000000000 --- a/extensions/keycloak-authorization/deployment/src/test/java/io/quarkus/keycloak/pep/test/UsersResource.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.quarkus.keycloak.pep.test; - -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.security.identity.SecurityIdentity; - -@Path("/api/users") -public class UsersResource { - - @Inject - SecurityIdentity keycloakSecurityContext; - - @GET - @Path("/me") - @Produces(MediaType.APPLICATION_JSON) - public User me() { - return new User(keycloakSecurityContext); - } - - public static class User { - - private final String userName; - - User(SecurityIdentity securityContext) { - this.userName = securityContext.getPrincipal().getName(); - } - - public String getUserName() { - return userName; - } - } -} diff --git a/extensions/keycloak-authorization/deployment/src/test/resources/application.properties b/extensions/keycloak-authorization/deployment/src/test/resources/application.properties deleted file mode 100644 index 0abbfbdc6c30d..0000000000000 --- a/extensions/keycloak-authorization/deployment/src/test/resources/application.properties +++ /dev/null @@ -1,37 +0,0 @@ -# Configuration file -quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus -quarkus.oidc.client-id=quarkus-app -quarkus.oidc.credentials.secret=secret -quarkus.http.cors=true - -# Enable Policy Enforcement -quarkus.keycloak.policy-enforcer.enable=true - -# Defines a global claim to be sent to Keycloak when evaluating permissions for any requesting coming to the application -quarkus.keycloak.policy-enforcer.claim-information-point.claims.request-uri={request.relativePath} -quarkus.keycloak.policy-enforcer.claim-information-point.claims.request-method={request.method} - -# Defines a static claim that is only sent to Keycloak when evaluating permissions for a specific path -quarkus.keycloak.policy-enforcer.paths.1.name=Permission Resource -quarkus.keycloak.policy-enforcer.paths.1.path=/api/permission -quarkus.keycloak.policy-enforcer.paths.1.claim-information-point.claims.static-claim=static-claim - -# Defines a claim which value references a request parameter -quarkus.keycloak.policy-enforcer.paths.2.path=/api/permission/claim-protected -quarkus.keycloak.policy-enforcer.paths.2.claim-information-point.claims.grant={request.parameter['grant']} - -# Defines a claim which value is based on the response from an external service -quarkus.keycloak.policy-enforcer.paths.3.path=/api/permission/http-response-claim-protected -quarkus.keycloak.policy-enforcer.paths.3.claim-information-point.http.claims.user-name=/userName -quarkus.keycloak.policy-enforcer.paths.3.claim-information-point.http.url=http://localhost:8080/api/users/me -quarkus.keycloak.policy-enforcer.paths.3.claim-information-point.http.method=GET -quarkus.keycloak.policy-enforcer.paths.3.claim-information-point.http.headers.Content-Type=application/x-www-form-urlencoded -quarkus.keycloak.policy-enforcer.paths.3.claim-information-point.http.headers.Authorization=Bearer {keycloak.access_token} - -# Defines a claim which value is based on the response from an external service -quarkus.keycloak.policy-enforcer.paths.5.path=/api/permission/body-claim -quarkus.keycloak.policy-enforcer.paths.5.claim-information-point.claims.from-body={request.body['/from-body']} - -quarkus.keycloak.policy-enforcer.paths.6.name=Root -quarkus.keycloak.policy-enforcer.paths.6.path=/* -quarkus.keycloak.policy-enforcer.paths.6.enforcement-mode=DISABLED \ No newline at end of file diff --git a/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinCompilationProvider.java b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinCompilationProvider.java index 6975c1ed65704..a7e30b4e854c3 100644 --- a/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinCompilationProvider.java +++ b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinCompilationProvider.java @@ -3,6 +3,7 @@ import java.io.File; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; @@ -31,6 +32,11 @@ public class KotlinCompilationProvider implements CompilationProvider { private final static Pattern OPTION_PATTERN = Pattern.compile("([^:]+):([^=]+)=(.*)"); private static final String KOTLIN_PACKAGE = "org.jetbrains.kotlin"; + @Override + public String getProviderKey() { + return "kotlin"; + } + @Override public Set handledExtensions() { return Collections.singleton(".kt"); @@ -74,6 +80,12 @@ public void compile(Set filesToCompile, Context context) { SimpleKotlinCompilerMessageCollector messageCollector = new SimpleKotlinCompilerMessageCollector(); K2JVMCompiler compiler = new K2JVMCompiler(); + Collection compilerOptions = context.getCompilerOptions(getProviderKey()); + + if (compilerOptions != null && !compilerOptions.isEmpty()) { + compiler.parseArguments(compilerOptions.toArray(new String[0]), compilerArguments); + } + ExitCode exitCode = compiler.exec( messageCollector, new Services.Builder().build(), diff --git a/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java index 61cd0e1ece20a..e031aaa262d1c 100644 --- a/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java +++ b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java @@ -2,6 +2,7 @@ import static io.quarkus.deployment.builditem.nativeimage.NativeImageResourcePatternsBuildItem.builder; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -27,11 +28,11 @@ FeatureBuildItem feature() { */ @BuildStep void registerKotlinJacksonModule(BuildProducer classPathJacksonModules) { - try { - Class.forName(KOTLIN_JACKSON_MODULE, false, Thread.currentThread().getContextClassLoader()); - classPathJacksonModules.produce(new ClassPathJacksonModuleBuildItem(KOTLIN_JACKSON_MODULE)); - } catch (Exception ignored) { + if (!QuarkusClassLoader.isClassPresentAtRuntime(KOTLIN_JACKSON_MODULE)) { + return; } + + classPathJacksonModules.produce(new ClassPathJacksonModuleBuildItem(KOTLIN_JACKSON_MODULE)); } /** diff --git a/extensions/kubernetes-client/runtime/pom.xml b/extensions/kubernetes-client/runtime/pom.xml index 56471229b8c50..762cfe6c75831 100644 --- a/extensions/kubernetes-client/runtime/pom.xml +++ b/extensions/kubernetes-client/runtime/pom.xml @@ -63,6 +63,11 @@ io.quarkus quarkus-bootstrap-maven-plugin + + + io.quarkus.kubernetes.client + + maven-compiler-plugin diff --git a/extensions/kubernetes-service-binding/deployment/src/main/java/io/quarkus/kubernetes/service/binding/buildtime/AddServiceBindingResourceDecorator.java b/extensions/kubernetes-service-binding/deployment/src/main/java/io/quarkus/kubernetes/service/binding/buildtime/AddServiceBindingResourceDecorator.java index 66b18fc75464f..af8191f87428a 100644 --- a/extensions/kubernetes-service-binding/deployment/src/main/java/io/quarkus/kubernetes/service/binding/buildtime/AddServiceBindingResourceDecorator.java +++ b/extensions/kubernetes-service-binding/deployment/src/main/java/io/quarkus/kubernetes/service/binding/buildtime/AddServiceBindingResourceDecorator.java @@ -42,6 +42,7 @@ public void visit(KubernetesListFluent list) { .withName(name) .endApplication() .withBindAsFiles(config.bindAsFiles) + .withDetectBindingResources(config.detectBindingResources) .withMountPath(config.mountPath.orElse(null)); String group = service.getApiVersion().contains("/") diff --git a/extensions/kubernetes-service-binding/deployment/src/main/java/io/quarkus/kubernetes/service/binding/buildtime/KubernetesServiceBindingConfig.java b/extensions/kubernetes-service-binding/deployment/src/main/java/io/quarkus/kubernetes/service/binding/buildtime/KubernetesServiceBindingConfig.java index 651ea02662569..a7fa2efa5f5af 100644 --- a/extensions/kubernetes-service-binding/deployment/src/main/java/io/quarkus/kubernetes/service/binding/buildtime/KubernetesServiceBindingConfig.java +++ b/extensions/kubernetes-service-binding/deployment/src/main/java/io/quarkus/kubernetes/service/binding/buildtime/KubernetesServiceBindingConfig.java @@ -30,4 +30,10 @@ public class KubernetesServiceBindingConfig { */ @ConfigItem(defaultValue = "true") public Boolean bindAsFiles; + + /** + * Detects the binding data from resources owned by the backing service. + */ + @ConfigItem(defaultValue = "false") + public Boolean detectBindingResources; } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddStatefulSetResourceDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddStatefulSetResourceDecorator.java index 139d270cde1f7..39440562adcd7 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddStatefulSetResourceDecorator.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddStatefulSetResourceDecorator.java @@ -1,43 +1,96 @@ package io.quarkus.kubernetes.deployment; +import static io.quarkus.kubernetes.deployment.Constants.STATEFULSET; + import java.util.HashMap; +import java.util.List; +import java.util.function.Function; import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator; +import io.dekorate.utils.Strings; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesListFluent; +import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder; +import io.fabric8.kubernetes.api.model.apps.StatefulSetFluent; public class AddStatefulSetResourceDecorator extends ResourceProvidingDecorator> { private final String name; private final PlatformConfiguration config; + public AddStatefulSetResourceDecorator(String name, PlatformConfiguration config) { + this.name = name; + this.config = config; + } + + @SuppressWarnings("deprecation") @Override public void visit(KubernetesListFluent list) { - list.addToItems(new StatefulSetBuilder() - .withNewMetadata() - .withName(name) - .endMetadata() - .withNewSpec() - .withReplicas(1) - .withServiceName(name) - .withNewSelector() - .withMatchLabels(new HashMap()) + StatefulSetBuilder builder = list.getItems().stream() + .filter(this::containsStatefulSetResource) + .map(replaceExistingStatefulSetResource(list)) + .findAny() + .orElseGet(this::createStatefulSetResource) + .accept(StatefulSetBuilder.class, this::initStatefulSetResourceWithDefaults); + + list.addToItems(builder.build()); + } + + private boolean containsStatefulSetResource(HasMetadata metadata) { + return STATEFULSET.equalsIgnoreCase(metadata.getKind()) && name.equals(metadata.getMetadata().getName()); + } + + private void initStatefulSetResourceWithDefaults(StatefulSetBuilder builder) { + StatefulSetFluent.SpecNested spec = builder.editOrNewSpec(); + + spec.editOrNewSelector() .endSelector() - .withNewTemplate() - .withNewSpec() - .withTerminationGracePeriodSeconds(10L) - .addNewContainer() - .withName(name) - .endContainer() - .endSpec() - .endTemplate() + .editOrNewTemplate() + .editOrNewSpec() .endSpec() - .build()); + .endTemplate(); + + // defaults for: + // - replicas + if (spec.getReplicas() == null) { + spec.withReplicas(1); + } + // - service name + if (Strings.isNullOrEmpty(spec.getServiceName())) { + spec.withServiceName(name); + } + // - match labels + if (spec.getSelector().getMatchLabels() == null) { + spec.editSelector().withMatchLabels(new HashMap<>()).endSelector(); + } + // - termination grace period seconds + if (spec.getTemplate().getSpec().getTerminationGracePeriodSeconds() == null) { + spec.editTemplate().editSpec().withTerminationGracePeriodSeconds(10L).endSpec().endTemplate(); + } + // - container + if (!containsContainerWithName(spec)) { + spec.editTemplate().editSpec().addNewContainer().withName(name).endContainer().endSpec().endTemplate(); + } + + spec.endSpec(); } - public AddStatefulSetResourceDecorator(String name, PlatformConfiguration config) { - this.name = name; - this.config = config; + private StatefulSetBuilder createStatefulSetResource() { + return new StatefulSetBuilder().withNewMetadata().withName(name).endMetadata(); + } + + private Function replaceExistingStatefulSetResource(KubernetesListFluent list) { + return metadata -> { + list.removeFromItems(metadata); + return new StatefulSetBuilder((StatefulSet) metadata); + }; + } + + private boolean containsContainerWithName(StatefulSetFluent.SpecNested spec) { + List containers = spec.getTemplate().getSpec().getContainers(); + return containers == null || containers.stream().anyMatch(c -> name.equals(c.getName())); } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java index f10ef9eac00c3..cab1095a06df9 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java @@ -388,6 +388,7 @@ public EnvVarsConfig getEnv() { /** * Whether or not this service is cluster-local. * Cluster local services are not exposed to the outside world. + * More information in this link. */ @ConfigItem public boolean clusterLocal; diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java index 27c98fe46f953..3d4db237b55cb 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java @@ -175,7 +175,7 @@ public List createDecorators(ApplicationInfoBuildItem applic if (config.clusterLocal) { result.add(new DecoratorBuildItem(KNATIVE, - new AddLabelDecorator(name, "serving.knative.dev/visibility", "cluster-local"))); + new AddLabelDecorator(name, "networking.knative.dev/visibility", "cluster-local"))); } /** diff --git a/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/GelfConfig.java b/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/GelfConfig.java index d4e6deabf313e..e67d288e8d14b 100644 --- a/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/GelfConfig.java +++ b/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/GelfConfig.java @@ -1,6 +1,7 @@ package io.quarkus.logging.gelf; import java.util.Map; +import java.util.Optional; import java.util.logging.Level; import io.quarkus.runtime.annotations.ConfigDocMapKey; @@ -117,4 +118,17 @@ public class GelfConfig { */ @ConfigItem(defaultValue = "true") public boolean includeLocation; + + /** + * Origin hostname + */ + @ConfigItem + public Optional originHost; + + /** + * Bypass hostname resolution. If you didn't set the {@code originHost} property, and resolution is disabled, the value + * “unknown” will be used as hostname + */ + @ConfigItem + public boolean skipHostnameResolution; } diff --git a/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/GelfLogHandlerRecorder.java b/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/GelfLogHandlerRecorder.java index 66947c6d26886..6000d36215459 100644 --- a/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/GelfLogHandlerRecorder.java +++ b/extensions/logging-gelf/runtime/src/main/java/io/quarkus/logging/gelf/GelfLogHandlerRecorder.java @@ -1,5 +1,7 @@ package io.quarkus.logging.gelf; +import static biz.paluch.logging.RuntimeContainerProperties.PROPERTY_LOGSTASH_GELF_SKIP_HOSTNAME_RESOLUTION; + import java.util.Map; import java.util.Optional; import java.util.logging.Handler; @@ -15,7 +17,18 @@ public RuntimeValue> initializeHandler(final GelfConfig config return new RuntimeValue<>(Optional.empty()); } + String previousSkipHostnameResolution = null; + if (config.skipHostnameResolution) { + previousSkipHostnameResolution = System.setProperty(PROPERTY_LOGSTASH_GELF_SKIP_HOSTNAME_RESOLUTION, "true"); + } final JBoss7GelfLogHandler handler = new JBoss7GelfLogHandler(); + if (config.skipHostnameResolution) { + if (previousSkipHostnameResolution == null) { + System.clearProperty(PROPERTY_LOGSTASH_GELF_SKIP_HOSTNAME_RESOLUTION); + } else { + System.setProperty(PROPERTY_LOGSTASH_GELF_SKIP_HOSTNAME_RESOLUTION, previousSkipHostnameResolution); + } + } handler.setVersion(config.version); handler.setFacility(config.facility); String extractStackTrace = String.valueOf(config.extractStackTrace); @@ -32,6 +45,9 @@ public RuntimeValue> initializeHandler(final GelfConfig config handler.setMaximumMessageSize(config.maximumMessageSize); handler.setIncludeLocation(config.includeLocation); handler.setIncludeLogMessageParameters(config.includeLogMessageParameters); + if (config.originHost.isPresent()) { + handler.setOriginHost(config.originHost.get()); + } // handle additional fields if (!config.additionalField.isEmpty()) { diff --git a/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonProcessor.java b/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonProcessor.java new file mode 100644 index 0000000000000..b313ae8d92465 --- /dev/null +++ b/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonProcessor.java @@ -0,0 +1,24 @@ +package io.quarkus.logging.json.deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.LogConsoleFormatBuildItem; +import io.quarkus.deployment.builditem.LogFileFormatBuildItem; +import io.quarkus.logging.json.runtime.JsonLogConfig; +import io.quarkus.logging.json.runtime.LoggingJsonRecorder; + +public final class LoggingJsonProcessor { + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public LogConsoleFormatBuildItem setUpConsoleFormatter(LoggingJsonRecorder recorder, JsonLogConfig config) { + return new LogConsoleFormatBuildItem(recorder.initializeConsoleJsonLogging(config)); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public LogFileFormatBuildItem setUpFileFormatter(LoggingJsonRecorder recorder, JsonLogConfig config) { + return new LogFileFormatBuildItem(recorder.initializeFileJsonLogging(config)); + } +} diff --git a/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonSteps.java b/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonSteps.java deleted file mode 100644 index f09287aae3204..0000000000000 --- a/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonSteps.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.quarkus.logging.json.deployment; - -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.annotations.ExecutionTime; -import io.quarkus.deployment.annotations.Record; -import io.quarkus.deployment.builditem.LogConsoleFormatBuildItem; -import io.quarkus.logging.json.runtime.JsonConfig; -import io.quarkus.logging.json.runtime.LoggingJsonRecorder; - -public final class LoggingJsonSteps { - - @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - public LogConsoleFormatBuildItem setUpFormatter(LoggingJsonRecorder recorder, JsonConfig config) { - return new LogConsoleFormatBuildItem(recorder.initializeJsonLogging(config)); - } -} diff --git a/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/ConsoleJsonFormatterCustomConfigTest.java b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/ConsoleJsonFormatterCustomConfigTest.java new file mode 100644 index 0000000000000..e9113992293ad --- /dev/null +++ b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/ConsoleJsonFormatterCustomConfigTest.java @@ -0,0 +1,72 @@ +package io.quarkus.logging.json; + +import static io.quarkus.logging.json.ConsoleJsonFormatterDefaultConfigTest.getJsonFormatter; +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.ZoneId; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import org.assertj.core.api.Assertions; +import org.jboss.logmanager.formatters.StructuredFormatter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.logging.json.runtime.AdditionalFieldConfig; +import io.quarkus.logging.json.runtime.JsonFormatter; +import io.quarkus.test.QuarkusUnitTest; + +public class ConsoleJsonFormatterCustomConfigTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addClasses(ConsoleJsonFormatterDefaultConfigTest.class)) + .withConfigurationResource("application-console-json-formatter-custom.properties"); + + @Test + public void jsonFormatterCustomConfigurationTest() { + JsonFormatter jsonFormatter = getJsonFormatter(); + assertThat(jsonFormatter.isPrettyPrint()).isTrue(); + assertThat(jsonFormatter.getDateTimeFormatter().toString()) + .isEqualTo("Value(DayOfMonth)' 'Text(MonthOfYear,SHORT)' 'Value(Year,4,19,EXCEEDS_PAD)"); + assertThat(jsonFormatter.getDateTimeFormatter().getZone()).isEqualTo(ZoneId.of("UTC+05:00")); + assertThat(jsonFormatter.getExceptionOutputType()) + .isEqualTo(StructuredFormatter.ExceptionOutputType.DETAILED_AND_FORMATTED); + assertThat(jsonFormatter.getRecordDelimiter()).isEqualTo("\n;"); + assertThat(jsonFormatter.isPrintDetails()).isTrue(); + assertThat(jsonFormatter.getExcludedKeys()).containsExactly("timestamp", "sequence"); + assertThat(jsonFormatter.getAdditionalFields().size()).isEqualTo(2); + assertThat(jsonFormatter.getAdditionalFields().containsKey("foo")).isTrue(); + assertThat(jsonFormatter.getAdditionalFields().get("foo").type).isEqualTo(AdditionalFieldConfig.Type.INT); + assertThat(jsonFormatter.getAdditionalFields().get("foo").value).isEqualTo("42"); + assertThat(jsonFormatter.getAdditionalFields().containsKey("bar")).isTrue(); + assertThat(jsonFormatter.getAdditionalFields().get("bar").type).isEqualTo(AdditionalFieldConfig.Type.STRING); + assertThat(jsonFormatter.getAdditionalFields().get("bar").value).isEqualTo("baz"); + } + + @Test + public void jsonFormatterOutputTest() throws Exception { + JsonFormatter jsonFormatter = getJsonFormatter(); + String line = jsonFormatter.format(new LogRecord(Level.INFO, "Hello, World!")); + + JsonNode node = new ObjectMapper().readTree(line); + // "level" has been renamed to HEY + Assertions.assertThat(node.has("level")).isFalse(); + Assertions.assertThat(node.has("HEY")).isTrue(); + Assertions.assertThat(node.get("HEY").asText()).isEqualTo("INFO"); + + // excluded fields + Assertions.assertThat(node.has("timestamp")).isFalse(); + Assertions.assertThat(node.has("sequence")).isFalse(); + + // additional fields + Assertions.assertThat(node.has("foo")).isTrue(); + Assertions.assertThat(node.get("foo").asInt()).isEqualTo(42); + Assertions.assertThat(node.has("bar")).isTrue(); + Assertions.assertThat(node.get("bar").asText()).isEqualTo("baz"); + Assertions.assertThat(node.get("message").asText()).isEqualTo("Hello, World!"); + } +} diff --git a/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/JsonFormatterDefaultConfigTest.java b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/ConsoleJsonFormatterDefaultConfigTest.java similarity index 94% rename from extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/JsonFormatterDefaultConfigTest.java rename to extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/ConsoleJsonFormatterDefaultConfigTest.java index c1eb13430d0e9..43cd7d1856c00 100644 --- a/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/JsonFormatterDefaultConfigTest.java +++ b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/ConsoleJsonFormatterDefaultConfigTest.java @@ -21,11 +21,11 @@ import io.quarkus.logging.json.runtime.JsonFormatter; import io.quarkus.test.QuarkusUnitTest; -public class JsonFormatterDefaultConfigTest { +public class ConsoleJsonFormatterDefaultConfigTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() - .withConfigurationResource("application-json-formatter-default.properties"); + .withConfigurationResource("application-console-json-formatter-default.properties"); @Test public void jsonFormatterDefaultConfigurationTest() { diff --git a/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/JsonFormatterCustomConfigTest.java b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/FileJsonFormatterCustomConfigTest.java similarity index 90% rename from extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/JsonFormatterCustomConfigTest.java rename to extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/FileJsonFormatterCustomConfigTest.java index dfd99cf01eb5f..7f59c11bf0cb8 100644 --- a/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/JsonFormatterCustomConfigTest.java +++ b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/FileJsonFormatterCustomConfigTest.java @@ -1,6 +1,6 @@ package io.quarkus.logging.json; -import static io.quarkus.logging.json.JsonFormatterDefaultConfigTest.getJsonFormatter; +import static io.quarkus.logging.json.FileJsonFormatterDefaultConfigTest.getJsonFormatter; import static org.assertj.core.api.Assertions.assertThat; import java.time.ZoneId; @@ -19,12 +19,12 @@ import io.quarkus.logging.json.runtime.JsonFormatter; import io.quarkus.test.QuarkusUnitTest; -public class JsonFormatterCustomConfigTest { +public class FileJsonFormatterCustomConfigTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() - .withApplicationRoot((jar) -> jar.addClasses(JsonFormatterDefaultConfigTest.class)) - .withConfigurationResource("application-json-formatter-custom.properties"); + .withApplicationRoot((jar) -> jar.addClasses(FileJsonFormatterDefaultConfigTest.class)) + .withConfigurationResource("application-file-json-formatter-custom.properties"); @Test public void jsonFormatterCustomConfigurationTest() { diff --git a/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/FileJsonFormatterDefaultConfigTest.java b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/FileJsonFormatterDefaultConfigTest.java new file mode 100644 index 0000000000000..05ee8a5f6359a --- /dev/null +++ b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/FileJsonFormatterDefaultConfigTest.java @@ -0,0 +1,62 @@ +package io.quarkus.logging.json; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +import org.jboss.logmanager.formatters.StructuredFormatter; +import org.jboss.logmanager.handlers.SizeRotatingFileHandler; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.bootstrap.logging.InitialConfigurator; +import io.quarkus.bootstrap.logging.QuarkusDelayedHandler; +import io.quarkus.logging.json.runtime.JsonFormatter; +import io.quarkus.test.QuarkusUnitTest; + +public class FileJsonFormatterDefaultConfigTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-file-json-formatter-default.properties"); + + @Test + public void jsonFormatterDefaultConfigurationTest() { + JsonFormatter jsonFormatter = getJsonFormatter(); + assertThat(jsonFormatter.isPrettyPrint()).isFalse(); + assertThat(jsonFormatter.getDateTimeFormatter().toString()) + .isEqualTo(DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.systemDefault()).toString()); + assertThat(jsonFormatter.getDateTimeFormatter().getZone()).isEqualTo(ZoneId.systemDefault()); + assertThat(jsonFormatter.getExceptionOutputType()).isEqualTo(StructuredFormatter.ExceptionOutputType.DETAILED); + assertThat(jsonFormatter.getRecordDelimiter()).isEqualTo("\n"); + assertThat(jsonFormatter.isPrintDetails()).isFalse(); + assertThat(jsonFormatter.getExcludedKeys()).isEmpty(); + assertThat(jsonFormatter.getAdditionalFields().entrySet()).isEmpty(); + } + + public static JsonFormatter getJsonFormatter() { + LogManager logManager = LogManager.getLogManager(); + assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); + + QuarkusDelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; + assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); + assertThat(delayedHandler.getLevel()).isEqualTo(Level.ALL); + + Handler handler = Arrays.stream(delayedHandler.getHandlers()) + .filter(h -> (h instanceof SizeRotatingFileHandler)) + .findFirst().orElse(null); + assertThat(handler).isNotNull(); + assertThat(handler.getLevel()).isEqualTo(Level.WARNING); + + Formatter formatter = handler.getFormatter(); + assertThat(formatter).isInstanceOf(JsonFormatter.class); + return (JsonFormatter) formatter; + } +} diff --git a/extensions/logging-json/deployment/src/test/resources/application-json-formatter-custom.properties b/extensions/logging-json/deployment/src/test/resources/application-console-json-formatter-custom.properties similarity index 100% rename from extensions/logging-json/deployment/src/test/resources/application-json-formatter-custom.properties rename to extensions/logging-json/deployment/src/test/resources/application-console-json-formatter-custom.properties diff --git a/extensions/logging-json/deployment/src/test/resources/application-json-formatter-default.properties b/extensions/logging-json/deployment/src/test/resources/application-console-json-formatter-default.properties similarity index 100% rename from extensions/logging-json/deployment/src/test/resources/application-json-formatter-default.properties rename to extensions/logging-json/deployment/src/test/resources/application-console-json-formatter-default.properties diff --git a/extensions/logging-json/deployment/src/test/resources/application-file-json-formatter-custom.properties b/extensions/logging-json/deployment/src/test/resources/application-file-json-formatter-custom.properties new file mode 100644 index 0000000000000..1546815d507d9 --- /dev/null +++ b/extensions/logging-json/deployment/src/test/resources/application-file-json-formatter-custom.properties @@ -0,0 +1,16 @@ +quarkus.log.level=INFO +quarkus.log.file.enable=true +quarkus.log.file.level=WARNING +quarkus.log.file.json=true +quarkus.log.file.json.pretty-print=true +quarkus.log.file.json.date-format=d MMM uuuu +quarkus.log.file.json.record-delimiter=\n; +quarkus.log.file.json.zone-id=UTC+05:00 +quarkus.log.file.json.exception-output-type=DETAILED_AND_FORMATTED +quarkus.log.file.json.print-details=true +quarkus.log.file.json.key-overrides=level=HEY +quarkus.log.file.json.excluded-keys=timestamp,sequence +quarkus.log.file.json.additional-field.foo.value=42 +quarkus.log.file.json.additional-field.foo.type=int +quarkus.log.file.json.additional-field.bar.value=baz +quarkus.log.file.json.additional-field.bar.type=string diff --git a/extensions/logging-json/deployment/src/test/resources/application-file-json-formatter-default.properties b/extensions/logging-json/deployment/src/test/resources/application-file-json-formatter-default.properties new file mode 100644 index 0000000000000..550e49ef5dc7b --- /dev/null +++ b/extensions/logging-json/deployment/src/test/resources/application-file-json-formatter-default.properties @@ -0,0 +1,5 @@ +quarkus.log.level=INFO +quarkus.log.file.enable=true +quarkus.log.file.level=WARNING +quarkus.log.file.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n +quarkus.log.file.json=true diff --git a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonConfig.java b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonConfig.java deleted file mode 100644 index 9bd6b340e08dc..0000000000000 --- a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonConfig.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.quarkus.logging.json.runtime; - -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -import org.jboss.logmanager.formatters.StructuredFormatter; - -import io.quarkus.runtime.annotations.ConfigDocMapKey; -import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.runtime.annotations.ConfigPhase; -import io.quarkus.runtime.annotations.ConfigRoot; - -/** - * Configuration for JSON log formatting. - */ -@ConfigRoot(phase = ConfigPhase.RUN_TIME, name = "log.console.json") -public class JsonConfig { - /** - * Determine whether to enable the JSON console formatting extension, which disables "normal" console formatting. - */ - @ConfigItem(name = ConfigItem.PARENT, defaultValue = "true") - boolean enable; - /** - * Enable "pretty printing" of the JSON record. Note that some JSON parsers will fail to read pretty printed output. - */ - @ConfigItem - boolean prettyPrint; - /** - * The date format to use. The special string "default" indicates that the default format should be used. - */ - @ConfigItem(defaultValue = "default") - String dateFormat; - /** - * The special end-of-record delimiter to be used. By default, newline is used as delimiter. - */ - @ConfigItem - Optional recordDelimiter; - /** - * The zone ID to use. The special string "default" indicates that the default zone should be used. - */ - @ConfigItem(defaultValue = "default") - String zoneId; - /** - * The exception output type to specify. - */ - @ConfigItem(defaultValue = "detailed") - StructuredFormatter.ExceptionOutputType exceptionOutputType; - /** - * Enable printing of more details in the log. - *

- * Printing the details can be expensive as the values are retrieved from the caller. The details include the - * source class name, source file name, source method name and source line number. - */ - @ConfigItem - boolean printDetails; - /** - * Override keys with custom values. Omitting this value indicates that no key overrides will be applied. - */ - @ConfigItem - Optional keyOverrides; - - /** - * Keys to be excluded from the Json output. - */ - @ConfigItem - Optional> excludedKeys; - - /** - * Additional fields to be appended in the json logs. - */ - @ConfigItem - @ConfigDocMapKey("field-name") - Map additionalField; -} diff --git a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonLogConfig.java b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonLogConfig.java new file mode 100644 index 0000000000000..ed0ac718c3dd8 --- /dev/null +++ b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonLogConfig.java @@ -0,0 +1,95 @@ +package io.quarkus.logging.json.runtime; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.jboss.logmanager.formatters.StructuredFormatter; + +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * Configuration for JSON log formatting. + */ +@ConfigRoot(phase = ConfigPhase.RUN_TIME, name = "log") +public class JsonLogConfig { + + /** + * Console logging. + */ + @ConfigDocSection + @ConfigItem(name = "console.json") + JsonConfig consoleJson; + + /** + * File logging. + */ + @ConfigDocSection + @ConfigItem(name = "file.json") + JsonConfig fileJson; + + @ConfigGroup + public static class JsonConfig { + /** + * Determine whether to enable the JSON console formatting extension, which disables "normal" console formatting. + */ + @ConfigItem(name = ConfigItem.PARENT, defaultValue = "true") + boolean enable; + /** + * Enable "pretty printing" of the JSON record. Note that some JSON parsers will fail to read pretty printed output. + */ + @ConfigItem + boolean prettyPrint; + /** + * The date format to use. The special string "default" indicates that the default format should be used. + */ + @ConfigItem(defaultValue = "default") + String dateFormat; + /** + * The special end-of-record delimiter to be used. By default, newline is used as delimiter. + */ + @ConfigItem + Optional recordDelimiter; + /** + * The zone ID to use. The special string "default" indicates that the default zone should be used. + */ + @ConfigItem(defaultValue = "default") + String zoneId; + /** + * The exception output type to specify. + */ + @ConfigItem(defaultValue = "detailed") + StructuredFormatter.ExceptionOutputType exceptionOutputType; + /** + * Enable printing of more details in the log. + *

+ * Printing the details can be expensive as the values are retrieved from the caller. The details include the + * source class name, source file name, source method name and source line number. + */ + @ConfigItem + boolean printDetails; + /** + * Override keys with custom values. Omitting this value indicates that no key overrides will be applied. + */ + @ConfigItem + Optional keyOverrides; + + /** + * Keys to be excluded from the Json output. + */ + @ConfigItem + Optional> excludedKeys; + + /** + * Additional fields to be appended in the json logs. + */ + @ConfigItem + @ConfigDocMapKey("field-name") + Map additionalField; + } +} diff --git a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/LoggingJsonRecorder.java b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/LoggingJsonRecorder.java index 40b87674cdf21..86ad507b6bd81 100644 --- a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/LoggingJsonRecorder.java +++ b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/LoggingJsonRecorder.java @@ -3,12 +3,22 @@ import java.util.Optional; import java.util.logging.Formatter; +import io.quarkus.logging.json.runtime.JsonLogConfig.JsonConfig; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; @Recorder public class LoggingJsonRecorder { - public RuntimeValue> initializeJsonLogging(final JsonConfig config) { + + public RuntimeValue> initializeConsoleJsonLogging(final JsonLogConfig config) { + return getFormatter(config.consoleJson); + } + + public RuntimeValue> initializeFileJsonLogging(final JsonLogConfig config) { + return getFormatter(config.fileJson); + } + + private RuntimeValue> getFormatter(JsonConfig config) { if (!config.enable) { return new RuntimeValue<>(Optional.empty()); } diff --git a/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java b/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java index 834ba12f32a26..26aa613c50028 100644 --- a/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java +++ b/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java @@ -4,6 +4,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.BooleanSupplier; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.DotName; @@ -18,10 +19,12 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.mailer.MailTemplate; import io.quarkus.mailer.runtime.BlockingMailerImpl; +import io.quarkus.mailer.runtime.MailBuildTimeConfig; import io.quarkus.mailer.runtime.MailClientProducer; import io.quarkus.mailer.runtime.MailTemplateProducer; import io.quarkus.mailer.runtime.MailerSupportProducer; @@ -35,6 +38,14 @@ public class MailerProcessor { private static final DotName MAIL_TEMPLATE = DotName.createSimple(MailTemplate.class.getName()); + public static class CacheAttachmentsEnabled implements BooleanSupplier { + MailBuildTimeConfig config; + + public boolean getAsBoolean() { + return config.cacheAttachments; + } + } + @BuildStep void registerBeans(BuildProducer beans) { beans.produce(AdditionalBeanBuildItem.builder().setUnremovable() @@ -80,6 +91,11 @@ FeatureBuildItem feature() { return new FeatureBuildItem(Feature.MAILER); } + @BuildStep(onlyIf = CacheAttachmentsEnabled.class) + SystemPropertyBuildItem cacheAttachmentBuildItem() { + return new SystemPropertyBuildItem("vertx.mail.attachment.cache.file", "true"); + } + @BuildStep void validateMailTemplates( List templatePaths, ValidationPhaseBuildItem validationPhase, diff --git a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/DkimSignOptionsConfig.java b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/DkimSignOptionsConfig.java new file mode 100644 index 0000000000000..cf6c136f2b001 --- /dev/null +++ b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/DkimSignOptionsConfig.java @@ -0,0 +1,96 @@ +package io.quarkus.mailer.runtime; + +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.OptionalLong; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class DkimSignOptionsConfig { + + /** + * Enables DKIM signing. + */ + @ConfigItem(defaultValue = "false") + public boolean enabled; + + /** + * Configures the PKCS#8 format private key used to sign the email. + */ + @ConfigItem + public Optional privateKey; + + /** + * Configures the PKCS#8 format private key file path. + */ + @ConfigItem + public Optional privateKeyPath; + + /** + * Configures the Agent or User Identifier(AUID). + */ + @ConfigItem + public Optional auid; + + /** + * Configures the selector used to query the public key. + */ + @ConfigItem + public Optional selector; + + /** + * Configures the Signing Domain Identifier. + */ + @ConfigItem + public Optional sdid; + + /** + * Configures the canonicalization algorithm for signed headers. + */ + @ConfigItem + public Optional headerCanonAlgo; + + /** + * Configures the canonicalization algorithm for mail body. + */ + @ConfigItem + public Optional bodyCanonAlgo; + + /** + * Configures the body limit to sign. + * + * Must be greater than zero. + */ + @ConfigItem + public OptionalInt bodyLimit; + + /** + * Configures to enable or disable signature sign timestmap. + */ + @ConfigItem + public Optional signatureTimestamp; + + /** + * Configures the expire time in seconds when the signature sign will be expired. + * + * Must be greater than zero. + */ + @ConfigItem + public OptionalLong expireTime; + + /** + * Configures the signed headers in DKIM, separated by commas. + * + * The order in the list matters. + */ + @ConfigItem + public Optional> signedHeaders; + + public enum CanonicalizationAlgorithmOption { + SIMPLE, + RELAXED + } +} diff --git a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailBuildTimeConfig.java b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailBuildTimeConfig.java new file mode 100644 index 0000000000000..afdbba290a49a --- /dev/null +++ b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailBuildTimeConfig.java @@ -0,0 +1,17 @@ +package io.quarkus.mailer.runtime; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "mailer", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public class MailBuildTimeConfig { + + /** + * Caches data from attachment's Stream to a temporary file. + * It tries to delete it after sending email. + */ + @ConfigItem(defaultValue = "false") + public boolean cacheAttachments; + +} diff --git a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailClientProducer.java b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailClientProducer.java index 4e0eed65bf80e..8603f97864d02 100644 --- a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailClientProducer.java +++ b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailClientProducer.java @@ -18,6 +18,8 @@ import io.vertx.core.net.PemTrustOptions; import io.vertx.core.net.PfxOptions; import io.vertx.core.net.TrustOptions; +import io.vertx.ext.mail.CanonicalizationAlgorithm; +import io.vertx.ext.mail.DKIMSignOptions; import io.vertx.ext.mail.LoginOption; import io.vertx.ext.mail.MailClient; import io.vertx.ext.mail.StartTLSOptions; @@ -60,6 +62,68 @@ private MailClient mailClient(Vertx vertx, MailConfig config, TlsConfig tlsConfi return MailClient.createShared(vertx, cfg); } + private io.vertx.ext.mail.DKIMSignOptions toVertxDkimSignOptions(DkimSignOptionsConfig optionsConfig) { + DKIMSignOptions vertxDkimOptions = new io.vertx.ext.mail.DKIMSignOptions(); + + String sdid = optionsConfig.sdid + .orElseThrow(() -> { + throw new ConfigurationException("Must provide the Signing Domain Identifier (sdid)."); + }); + vertxDkimOptions.setSdid(sdid); + + String selector = optionsConfig.selector + .orElseThrow(() -> { + throw new ConfigurationException("Must provide the selector."); + }); + vertxDkimOptions.setSelector(selector); + + if (optionsConfig.auid.isPresent()) { + vertxDkimOptions.setAuid(optionsConfig.auid.get()); + } + + if (optionsConfig.bodyLimit.isPresent()) { + int bodyLimit = optionsConfig.bodyLimit.getAsInt(); + vertxDkimOptions.setBodyLimit(bodyLimit); + } + + if (optionsConfig.expireTime.isPresent()) { + long expireTime = optionsConfig.expireTime.getAsLong(); + vertxDkimOptions.setExpireTime(expireTime); + } + + if (optionsConfig.bodyCanonAlgo.isPresent()) { + vertxDkimOptions.setBodyCanonAlgo(CanonicalizationAlgorithm.valueOf(optionsConfig.bodyCanonAlgo.get().toString())); + } + + if (optionsConfig.headerCanonAlgo.isPresent()) { + vertxDkimOptions + .setHeaderCanonAlgo(CanonicalizationAlgorithm.valueOf(optionsConfig.headerCanonAlgo.get().toString())); + } + + if (optionsConfig.privateKey.isPresent()) { + vertxDkimOptions.setPrivateKey(optionsConfig.privateKey.get()); + } else if (optionsConfig.privateKeyPath.isPresent()) { + vertxDkimOptions.setPrivateKeyPath(optionsConfig.privateKeyPath.get()); + } + + if (optionsConfig.signatureTimestamp.isPresent()) { + vertxDkimOptions.setSignatureTimestamp(optionsConfig.signatureTimestamp.get()); + } + + if (optionsConfig.signedHeaders.isPresent()) { + List headers = optionsConfig.signedHeaders.get(); + + if (headers.stream().noneMatch(header -> header.equalsIgnoreCase("from"))) { + throw new ConfigurationException( + "The \"From\" header must always be included to the list of headers to sign."); + } + + vertxDkimOptions.setSignedHeaders(headers); + } + + return vertxDkimOptions; + } + private io.vertx.ext.mail.MailConfig toVertxMailConfig(MailConfig config, TlsConfig tlsConfig) { io.vertx.ext.mail.MailConfig cfg = new io.vertx.ext.mail.MailConfig(); if (config.authMethods.isPresent()) { @@ -86,6 +150,11 @@ private io.vertx.ext.mail.MailConfig toVertxMailConfig(MailConfig config, TlsCon cfg.setPort(config.port.getAsInt()); } + if (config.dkim != null && config.dkim.enabled) { + cfg.setEnableDKIM(true); + cfg.addDKIMSignOption(toVertxDkimSignOptions(config.dkim)); + } + cfg.setSsl(config.ssl); cfg.setStarttls(StartTLSOptions.valueOf(config.startTLS.toUpperCase())); cfg.setMultiPartOnly(config.multiPartOnly); diff --git a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailConfig.java b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailConfig.java index 634bc619ef528..9bb604fcc9004 100644 --- a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailConfig.java +++ b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailConfig.java @@ -116,6 +116,12 @@ public class MailConfig { @ConfigItem(defaultValue = "OPTIONAL") public String startTLS; + /** + * Configures DKIM signature verification. + */ + @ConfigItem + public DkimSignOptionsConfig dkim; + /** * Sets the login mode for the connection. * Either {@code NONE}, @{code DISABLED}, {@code OPTIONAL}, {@code REQUIRED} or {@code XOAUTH2}. diff --git a/extensions/micrometer/deployment/pom.xml b/extensions/micrometer/deployment/pom.xml index aefd488718bec..d566d65767896 100644 --- a/extensions/micrometer/deployment/pom.xml +++ b/extensions/micrometer/deployment/pom.xml @@ -48,6 +48,11 @@ io.quarkus quarkus-undertow-spi + + + io.quarkus + quarkus-jsonp-deployment + diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java index f3c45726769d0..e3e2e6e8076e2 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java @@ -75,6 +75,14 @@ public boolean getAsBoolean() { MicrometerConfig mConfig; + /** + * config objects are beans, but they are not unremoveable by default + */ + @BuildStep + UnremovableBeanBuildItem mpConfigAsBean() { + return UnremovableBeanBuildItem.beanTypes(MicrometerConfig.class); + } + @BuildStep(onlyIf = MicrometerEnabled.class) FeatureBuildItem feature() { return new FeatureBuildItem(Feature.MICROMETER); diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/JsonRegistryProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/JsonRegistryProcessor.java index d97cf11de2986..e190e04ec2c97 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/JsonRegistryProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/JsonRegistryProcessor.java @@ -44,6 +44,7 @@ public void initializeJsonRegistry(MicrometerConfig config, routes.produce(nonApplicationRootPathBuildItem.routeBuilder() .routeFunction(config.export.json.path, recorder.route()) + .routeConfigKey("quarkus.micrometer.export.json.path") .handler(recorder.getHandler()) .blockingRoute() .build()); diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java index 81b2583e0c4e1..d50bf067ad168 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java @@ -66,6 +66,7 @@ void createPrometheusRoute(BuildProducer routes, // Exact match for resources matched to the root path routes.produce(nonApplicationRootPathBuildItem.routeBuilder() .routeFunction(pConfig.path, recorder.route()) + .routeConfigKey("quarkus.micrometer.export.prometheus.path") .handler(recorder.getHandler()) .displayOnNotFoundPage("Metrics") .blockingRoute() diff --git a/extensions/micrometer/deployment/src/main/resources/dev-templates/embedded.html b/extensions/micrometer/deployment/src/main/resources/dev-templates/embedded.html new file mode 100644 index 0000000000000..f41e419a269d5 --- /dev/null +++ b/extensions/micrometer/deployment/src/main/resources/dev-templates/embedded.html @@ -0,0 +1,11 @@ +{#if config:property('quarkus.micrometer.export.prometheus.enabled') is "true"} + + + Prometheus Metrics +
+{/if} +{#if config:property('quarkus.micrometer.export.json.enabled') is "true"} + + + JSON Metrics +{/if} \ No newline at end of file diff --git a/extensions/micrometer/runtime/pom.xml b/extensions/micrometer/runtime/pom.xml index 0e1f4d533ec8b..76c28b8b8518b 100644 --- a/extensions/micrometer/runtime/pom.xml +++ b/extensions/micrometer/runtime/pom.xml @@ -30,6 +30,12 @@ quarkus-vertx-http + + + io.quarkus + quarkus-jsonp + + io.micrometer micrometer-core @@ -39,13 +45,6 @@ slf4j-jboss-logmanager - - - org.glassfish - jakarta.json - - - diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/RestClientMetricsListener.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/RestClientMetricsListener.java index 82457606eb9e5..5a67b2d89d59c 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/RestClientMetricsListener.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/RestClientMetricsListener.java @@ -17,6 +17,7 @@ import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.Timer; import io.quarkus.arc.Arc; +import io.quarkus.micrometer.runtime.config.MicrometerConfig; /** * This is initialized via ServiceFactory (static/non-CDI initialization) @@ -25,16 +26,19 @@ public class RestClientMetricsListener implements RestClientListener { private final static String REQUEST_METRIC_PROPERTY = "restClientMetrics"; - final MeterRegistry registry = Metrics.globalRegistry; - boolean initialized = false; - boolean clientMetricsEnabled = false; + private final MeterRegistry registry = Metrics.globalRegistry; + private boolean initialized = false; + private boolean clientMetricsEnabled = false; - HttpBinderConfiguration httpMetricsConfig; - MetricsClientRequestFilter clientRequestFilter; - MetricsClientResponseFilter clientResponseFilter; + private MetricsClientRequestFilter clientRequestFilter; + private MetricsClientResponseFilter clientResponseFilter; @Override public void onNewClient(Class serviceInterface, RestClientBuilder builder) { + MicrometerConfig micrometerConfig = Arc.container().instance(MicrometerConfig.class).get(); + if (!micrometerConfig.enabled) { + return; + } if (prepClientMetrics()) { // This must run AFTER the OpenTelmetry client request filter builder.register(this.clientRequestFilter, Priorities.HEADER_DECORATOR + 1); @@ -46,11 +50,11 @@ public void onNewClient(Class serviceInterface, RestClientBuilder builder) { boolean prepClientMetrics() { boolean clientMetricsEnabled = this.clientMetricsEnabled; if (!this.initialized) { - this.httpMetricsConfig = Arc.container().instance(HttpBinderConfiguration.class).get(); + HttpBinderConfiguration httpMetricsConfig = Arc.container().instance(HttpBinderConfiguration.class).get(); clientMetricsEnabled = httpMetricsConfig.isClientEnabled(); if (clientMetricsEnabled) { - this.clientRequestFilter = new MetricsClientRequestFilter(httpMetricsConfig); - this.clientResponseFilter = new MetricsClientResponseFilter(); + this.clientRequestFilter = new MetricsClientRequestFilter(registry); + this.clientResponseFilter = new MetricsClientResponseFilter(registry, httpMetricsConfig); } this.clientMetricsEnabled = clientMetricsEnabled; this.initialized = true; @@ -58,11 +62,11 @@ boolean prepClientMetrics() { return clientMetricsEnabled; } - class MetricsClientRequestFilter implements ClientRequestFilter { - HttpBinderConfiguration binderConfiguration; + static class MetricsClientRequestFilter implements ClientRequestFilter { + private final MeterRegistry registry; - MetricsClientRequestFilter(HttpBinderConfiguration binderConfiguration) { - this.binderConfiguration = binderConfiguration; + MetricsClientRequestFilter(MeterRegistry registry) { + this.registry = registry; } @Override @@ -73,7 +77,16 @@ public void filter(ClientRequestContext requestContext) throws IOException { } } - class MetricsClientResponseFilter implements ClientResponseFilter { + static class MetricsClientResponseFilter implements ClientResponseFilter { + private final MeterRegistry registry; + private final HttpBinderConfiguration httpMetricsConfig; + + MetricsClientResponseFilter(MeterRegistry registry, + HttpBinderConfiguration httpMetricsConfig) { + this.registry = registry; + this.httpMetricsConfig = httpMetricsConfig; + } + @Override public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { RequestMetricInfo requestMetric = getRequestMetric(requestContext); @@ -115,7 +128,7 @@ private Tag clientName(ClientRequestContext requestContext) { } } - class RestClientMetricInfo extends RequestMetricInfo { + static class RestClientMetricInfo extends RequestMetricInfo { ClientRequestContext requestContext; RestClientMetricInfo(ClientRequestContext requestContext) { diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/handlers/PrometheusHandler.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/handlers/PrometheusHandler.java index 1a3283734d26d..1b05851c58c37 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/handlers/PrometheusHandler.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/handlers/PrometheusHandler.java @@ -8,6 +8,8 @@ import io.micrometer.prometheus.PrometheusMeterRegistry; import io.prometheus.client.exporter.common.TextFormat; +import io.quarkus.arc.Arc; +import io.quarkus.arc.ManagedContext; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpServerResponse; @@ -31,11 +33,25 @@ public void handle(RoutingContext routingContext) { response.setStatusCode(500) .setStatusMessage("Unable to resolve Prometheus registry instance"); } else { - response.putHeader("Content-Type", TextFormat.CONTENT_TYPE_004) - .end(Buffer.buffer(registry.scrape())); + ManagedContext requestContext = Arc.container().requestContext(); + if (requestContext.isActive()) { + doHandle(response); + } else { + requestContext.activate(); + try { + doHandle(response); + } finally { + requestContext.terminate(); + } + } } } + private void doHandle(HttpServerResponse response) { + response.putHeader("Content-Type", TextFormat.CONTENT_TYPE_004) + .end(Buffer.buffer(registry.scrape())); + } + private void setup() { Instance registries = CDI.current().select(PrometheusMeterRegistry.class, Default.Literal.INSTANCE); diff --git a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java index 5669aa8da82a3..a4829e429bd99 100644 --- a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java +++ b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java @@ -22,13 +22,13 @@ import com.github.dockerjava.zerodep.shaded.org.apache.hc.core5.net.URLEncodedUtils; import io.quarkus.deployment.Feature; -import io.quarkus.deployment.IsDockerWorking; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.console.ConsoleInstalledBuildItem; import io.quarkus.deployment.console.StartupLogCompressor; @@ -46,10 +46,9 @@ public class DevServicesMongoProcessor { static volatile Map capturedProperties; static volatile boolean first = true; - private final IsDockerWorking isDockerWorking = new IsDockerWorking(true); - @BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) public List startMongo(List mongoConnections, + DockerStatusBuildItem dockerStatusBuildItem, MongoClientBuildTimeConfig mongoClientBuildTimeConfig, List devServicesSharedNetworkBuildItem, Optional consoleInstalledBuildItem, @@ -101,9 +100,13 @@ public List startMongo(List timeout) { + private RunningDevService startMongo(DockerStatusBuildItem dockerStatusBuildItem, String connectionName, + CapturedProperties capturedProperties, boolean useSharedNetwork, Optional timeout) { if (!capturedProperties.devServicesEnabled) { // explicitly disabled log.debug("Not starting devservices for " + (isDefault(connectionName) ? "default datasource" : connectionName) @@ -158,7 +161,7 @@ private RunningDevService startMongo(String connectionName, CapturedProperties c return null; } - if (!isDockerWorking.getAsBoolean()) { + if (!dockerStatusBuildItem.isDockerAvailable()) { log.warn("Please configure datasource URL for " + (isDefault(connectionName) ? "default datasource" : connectionName) + " or get a working docker instance"); diff --git a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java index 962c20e270933..cac6579a1ecd6 100644 --- a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java +++ b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java @@ -19,6 +19,7 @@ import org.bson.codecs.configuration.CodecProvider; import org.bson.codecs.pojo.PropertyCodecProvider; import org.bson.codecs.pojo.annotations.BsonDiscriminator; +import org.bson.types.ObjectId; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; @@ -52,6 +53,7 @@ import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.SslNativeConfigBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.mongodb.MongoClientName; @@ -409,4 +411,9 @@ void registerServiceBinding(Capabilities capabilities, BuildProducer runtimeInitializedClasses) { + runtimeInitializedClasses.produce(new RuntimeInitializedClassBuildItem(ObjectId.class.getName())); + } } diff --git a/extensions/netty/deployment/src/main/java/io/quarkus/netty/deployment/NettyProcessor.java b/extensions/netty/deployment/src/main/java/io/quarkus/netty/deployment/NettyProcessor.java index ad04bbcd96b9a..a0346428a2c39 100644 --- a/extensions/netty/deployment/src/main/java/io/quarkus/netty/deployment/NettyProcessor.java +++ b/extensions/netty/deployment/src/main/java/io/quarkus/netty/deployment/NettyProcessor.java @@ -17,6 +17,7 @@ import io.netty.util.internal.logging.InternalLoggerFactory; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; @@ -107,59 +108,49 @@ NativeImageConfigBuildItem build( .addRuntimeInitializedClass("io.netty.buffer.ByteBufUtil") .addNativeImageSystemProperty("io.netty.leakDetection.level", "DISABLED"); - try { - Class.forName("io.netty.handler.codec.http.HttpObjectEncoder"); + if (QuarkusClassLoader.isClassPresentAtRuntime("io.netty.handler.codec.http.HttpObjectEncoder")) { builder .addRuntimeInitializedClass("io.netty.handler.codec.http.HttpObjectEncoder") .addRuntimeInitializedClass("io.netty.handler.codec.http.websocketx.extensions.compression.DeflateDecoder") .addRuntimeInitializedClass("io.netty.handler.codec.http.websocketx.WebSocket00FrameEncoder"); - } catch (ClassNotFoundException e) { - //ignore + } else { log.debug("Not registering Netty HTTP classes as they were not found"); } - try { - Class.forName("io.netty.handler.codec.http2.Http2CodecUtil"); + if (QuarkusClassLoader.isClassPresentAtRuntime("io.netty.handler.codec.http2.Http2CodecUtil")) { builder .addRuntimeInitializedClass("io.netty.handler.codec.http2.Http2CodecUtil") .addRuntimeInitializedClass("io.netty.handler.codec.http2.Http2ClientUpgradeCodec") .addRuntimeInitializedClass("io.netty.handler.codec.http2.DefaultHttp2FrameWriter") .addRuntimeInitializedClass("io.netty.handler.codec.http2.Http2ConnectionHandler"); - } catch (ClassNotFoundException e) { - //ignore + } else { log.debug("Not registering Netty HTTP2 classes as they were not found"); } - try { - Class.forName("io.netty.channel.unix.UnixChannel"); + if (QuarkusClassLoader.isClassPresentAtRuntime("io.netty.channel.unix.UnixChannel")) { builder.addRuntimeInitializedClass("io.netty.channel.unix.Errors") .addRuntimeInitializedClass("io.netty.channel.unix.FileDescriptor") .addRuntimeInitializedClass("io.netty.channel.unix.IovArray") .addRuntimeInitializedClass("io.netty.channel.unix.Limits"); - } catch (ClassNotFoundException e) { - //ignore + } else { log.debug("Not registering Netty native unix classes as they were not found"); } - try { - Class.forName("io.netty.channel.epoll.EpollMode"); + if (QuarkusClassLoader.isClassPresentAtRuntime("io.netty.channel.epoll.EpollMode")) { builder.addRuntimeInitializedClass("io.netty.channel.epoll.Epoll") .addRuntimeInitializedClass("io.netty.channel.epoll.EpollEventArray") .addRuntimeInitializedClass("io.netty.channel.epoll.EpollEventLoop") .addRuntimeInitializedClass("io.netty.channel.epoll.Native"); - } catch (ClassNotFoundException e) { - //ignore + } else { log.debug("Not registering Netty native epoll classes as they were not found"); } - try { - Class.forName("io.netty.channel.kqueue.AcceptFilter"); + if (QuarkusClassLoader.isClassPresentAtRuntime("io.netty.channel.kqueue.AcceptFilter")) { builder.addRuntimeInitializedClass("io.netty.channel.kqueue.KQueue") .addRuntimeInitializedClass("io.netty.channel.kqueue.KQueueEventArray") .addRuntimeInitializedClass("io.netty.channel.kqueue.KQueueEventLoop") .addRuntimeInitializedClass("io.netty.channel.kqueue.Native"); - } catch (ClassNotFoundException e) { - //ignore + } else { log.debug("Not registering Netty native kqueue classes as they were not found"); } 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 7508e906ecb39..f7c12963d7771 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 @@ -41,7 +41,8 @@ public void filter(ResteasyReactiveClientRequestContext requestContext) { super.getTokens().subscribe().with(new Consumer() { @Override public void accept(Tokens tokens) { - requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_SCHEME_WITH_SPACE + tokens.getAccessToken()); + requestContext.getHeaders().putSingle(HttpHeaders.AUTHORIZATION, + BEARER_SCHEME_WITH_SPACE + tokens.getAccessToken()); requestContext.resume(); } }, new Consumer() { diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java index 33021951f897c..2f798015f5e62 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java @@ -21,6 +21,7 @@ import io.quarkus.oidc.common.runtime.OidcConstants; import io.smallrye.mutiny.Uni; import io.vertx.core.http.HttpHeaders; +import io.vertx.core.json.DecodeException; import io.vertx.core.json.JsonObject; import io.vertx.mutiny.core.MultiMap; import io.vertx.mutiny.core.buffer.Buffer; @@ -181,6 +182,8 @@ private static JsonObject decodeJwtToken(String accessToken) { return new JsonObject(new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8)); } catch (IllegalArgumentException ex) { LOG.debug("JWT token can not be decoded using the Base64Url encoding scheme"); + } catch (DecodeException ex) { + LOG.debug("JWT token can not be decoded"); } } else { LOG.debug("Access token is not formatted as the encoded JWT token"); 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 c8512c4ae35b6..12a109c3e9f45 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 @@ -439,6 +439,14 @@ public enum Verification { @ConfigItem public Optional keyStoreFileType = Optional.empty(); + /** + * An optional parameter to specify a provider of the key store file. If not given, the provider is automatically + * detected + * based on the key store file type. + */ + @ConfigItem + public Optional keyStoreProvider; + /** * A parameter to specify the password of the key store file. If not given, the default ("password") is used. */ @@ -484,6 +492,14 @@ public enum Verification { @ConfigItem public Optional trustStoreFileType = Optional.empty(); + /** + * An optional parameter to specify a provider of the trust store file. If not given, the provider is automatically + * detected + * based on the trust store file type. + */ + @ConfigItem + public Optional trustStoreProvider; + public Optional getVerification() { return verification; } @@ -516,6 +532,22 @@ public void setTrustStoreCertAlias(String trustStoreCertAlias) { this.trustStoreCertAlias = Optional.of(trustStoreCertAlias); } + public Optional getKeyStoreProvider() { + return keyStoreProvider; + } + + public void setKeyStoreProvider(String keyStoreProvider) { + this.keyStoreProvider = Optional.of(keyStoreProvider); + } + + public Optional getTrustStoreProvider() { + return trustStoreProvider; + } + + public void setTrustStoreProvider(String trustStoreProvider) { + this.trustStoreProvider = Optional.of(trustStoreProvider); + } + } @ConfigGroup diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java index 744dcc9e6e1ff..5ad51f3bc1e1f 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java @@ -131,7 +131,8 @@ public static void setHttpClientOptions(OidcCommonConfig oidcConfig, TlsConfig t .setPassword(oidcConfig.tls.getTrustStorePassword().orElse("password")) .setAlias(oidcConfig.tls.getTrustStoreCertAlias().orElse(null)) .setValue(io.vertx.core.buffer.Buffer.buffer(trustStoreData)) - .setType(getStoreType(oidcConfig.tls.trustStoreFileType, oidcConfig.tls.trustStoreFile.get())); + .setType(getStoreType(oidcConfig.tls.trustStoreFileType, oidcConfig.tls.trustStoreFile.get())) + .setProvider(oidcConfig.tls.trustStoreProvider.orElse(null)); options.setTrustOptions(trustStoreOptions); if (Verification.CERTIFICATE_VALIDATION == oidcConfig.tls.verification.orElse(Verification.REQUIRED)) { options.setVerifyHost(false); @@ -150,7 +151,8 @@ public static void setHttpClientOptions(OidcCommonConfig oidcConfig, TlsConfig t .setAlias(oidcConfig.tls.keyStoreKeyAlias.orElse(null)) .setAliasPassword(oidcConfig.tls.keyStoreKeyPassword.orElse(null)) .setValue(io.vertx.core.buffer.Buffer.buffer(keyStoreData)) - .setType(getStoreType(oidcConfig.tls.keyStoreFileType, oidcConfig.tls.keyStoreFile.get())); + .setType(getStoreType(oidcConfig.tls.keyStoreFileType, oidcConfig.tls.keyStoreFile.get())) + .setProvider(oidcConfig.tls.keyStoreProvider.orElse(null)); options.setKeyCertOptions(keyStoreOptions); } catch (IOException ex) { 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 e969184e2d343..df8a0dad18dd1 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 @@ -28,7 +28,7 @@ public class DevServicesConfig { * * Image with a Quarkus based distribution is used by default. * Image with a WildFly based distribution can be selected instead, for example: - * 'quay.io/keycloak/keycloak:17.0.1-legacy'. + * 'quay.io/keycloak/keycloak:18.0.0-legacy'. *

* Note Keycloak Quarkus and Keycloak WildFly images are initialized differently. * By default, Dev Services for Keycloak will assume it is a Keycloak Quarkus image if the image version does not end with a @@ -36,7 +36,7 @@ public class DevServicesConfig { * string. * Set 'quarkus.keycloak.devservices.keycloak-x-image' to override this check. */ - @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:17.0.1") + @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:18.0.0") 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 8596e113253c2..e98fb1c9acc88 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 @@ -14,6 +14,7 @@ import java.nio.file.attribute.FileTime; import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -24,6 +25,7 @@ import java.util.Set; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.logging.Logger; @@ -39,7 +41,6 @@ import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; -import io.quarkus.deployment.IsDockerWorking; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -47,6 +48,7 @@ import io.quarkus.deployment.builditem.DevServicesResultBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.console.ConsoleInstalledBuildItem; import io.quarkus.deployment.console.StartupLogCompressor; @@ -121,10 +123,10 @@ public class KeycloakDevServicesProcessor { static volatile DevServicesConfig capturedDevServicesConfiguration; private static volatile boolean first = true; private static volatile FileTime capturedRealmFileLastModifiedDate; - private final IsDockerWorking isDockerWorking = new IsDockerWorking(true); @BuildStep(onlyIfNot = IsNormal.class, onlyIf = { IsEnabled.class, GlobalDevServicesConfig.Enabled.class }) public DevServicesResultBuildItem startKeycloakContainer( + DockerStatusBuildItem dockerStatusBuildItem, BuildProducer keycloakBuildItemBuildProducer, List devServicesSharedNetworkBuildItem, Optional oidcProviderBuildItem, @@ -155,7 +157,14 @@ public DevServicesResultBuildItem startKeycloakContainer( } } if (!restartRequired) { - return devService.toBuildItem(); + DevServicesResultBuildItem result = devService.toBuildItem(); + String usersString = result.getConfig().get(OIDC_USERS); + Map users = (usersString == null || usersString.isBlank()) ? Map.of() + : Arrays.stream(usersString.split(",")) + .map(s -> s.split("=")).collect(Collectors.toMap(s -> s[0], s -> s[1])); + keycloakBuildItemBuildProducer + .produce(new KeycloakDevServicesConfigBuildItem(result.getConfig(), Map.of(OIDC_USERS, users))); + return result; } try { devService.close(); @@ -173,7 +182,7 @@ public DevServicesResultBuildItem startKeycloakContainer( vertxInstance = Vertx.vertx(); } try { - RunningDevService newDevService = startContainer(keycloakBuildItemBuildProducer, + RunningDevService newDevService = startContainer(dockerStatusBuildItem, keycloakBuildItemBuildProducer, !devServicesSharedNetworkBuildItem.isEmpty(), devServicesConfig.timeout); if (newDevService == null) { @@ -213,7 +222,11 @@ public void run() { } capturedRealmFileLastModifiedDate = getRealmFileLastModifiedDate(capturedDevServicesConfiguration.realmPath); - compressor.close(); + if (devService == null) { + compressor.closeAndDumpCaptured(); + } else { + compressor.close(); + } } catch (Throwable t) { compressor.closeAndDumpCaptured(); throw new RuntimeException(t); @@ -257,6 +270,8 @@ private Map prepareConfiguration( configProperties.put(APPLICATION_TYPE_CONFIG_KEY, oidcApplicationType); configProperties.put(CLIENT_ID_CONFIG_KEY, oidcClientId); configProperties.put(CLIENT_SECRET_CONFIG_KEY, oidcClientSecret); + configProperties.put(OIDC_USERS, users.entrySet().stream() + .map(e -> e.toString()).collect(Collectors.joining(","))); keycloakBuildItemBuildProducer .produce(new KeycloakDevServicesConfigBuildItem(configProperties, Map.of(OIDC_USERS, users))); @@ -272,7 +287,8 @@ private String getDefaultRealmName() { return capturedDevServicesConfiguration.realmName.orElse("quarkus"); } - private RunningDevService startContainer(BuildProducer keycloakBuildItemBuildProducer, + private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuildItem, + BuildProducer keycloakBuildItemBuildProducer, boolean useSharedNetwork, Optional timeout) { if (!capturedDevServicesConfiguration.enabled) { // explicitly disabled @@ -292,7 +308,7 @@ private RunningDevService startContainer(BuildProducer managedChannelBuilder, byte[] trustedCertificatesPem) + public static void setClientKeysAndTrustedCertificatesPem( + ManagedChannelBuilder managedChannelBuilder, byte[] privateKeyPem, byte[] certificatePem, + byte[] trustedCertificatesPem) throws SSLException { requireNonNull(managedChannelBuilder, "managedChannelBuilder"); requireNonNull(trustedCertificatesPem, "trustedCertificatesPem"); diff --git a/extensions/opentelemetry/opentelemetry-exporter-otlp/runtime/src/main/java/io/quarkus/opentelemetry/exporter/otlp/runtime/graal/OtlpExporterSubstitutions.java b/extensions/opentelemetry/opentelemetry-exporter-otlp/runtime/src/main/java/io/quarkus/opentelemetry/exporter/otlp/runtime/graal/OtlpExporterSubstitutions.java index d47b99eee7071..6290a2144142e 100644 --- a/extensions/opentelemetry/opentelemetry-exporter-otlp/runtime/src/main/java/io/quarkus/opentelemetry/exporter/otlp/runtime/graal/OtlpExporterSubstitutions.java +++ b/extensions/opentelemetry/opentelemetry-exporter-otlp/runtime/src/main/java/io/quarkus/opentelemetry/exporter/otlp/runtime/graal/OtlpExporterSubstitutions.java @@ -3,6 +3,7 @@ import static java.util.Objects.requireNonNull; import javax.net.ssl.SSLException; +import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509TrustManager; import com.oracle.svm.core.annotate.Substitute; @@ -11,6 +12,7 @@ import io.grpc.ManagedChannelBuilder; import io.grpc.netty.GrpcSslContexts; import io.grpc.netty.NettyChannelBuilder; +import io.opentelemetry.exporter.internal.TlsUtil; /** * Replace the {@code setTrustedCertificatesPem()} method in native because the upstream code supports using @@ -20,19 +22,27 @@ final class Target_io_opentelemetry_exporter_otlp_internal_grpc_ManagedChannelUtil { @Substitute - public static void setTrustedCertificatesPem( - ManagedChannelBuilder managedChannelBuilder, byte[] trustedCertificatesPem) + public static void setClientKeysAndTrustedCertificatesPem( + ManagedChannelBuilder managedChannelBuilder, + byte[] privateKeyPem, + byte[] certificatePem, + byte[] trustedCertificatesPem) throws SSLException { requireNonNull(managedChannelBuilder, "managedChannelBuilder"); requireNonNull(trustedCertificatesPem, "trustedCertificatesPem"); - X509TrustManager tm = io.opentelemetry.exporter.internal.TlsUtil.trustManager(trustedCertificatesPem); + X509TrustManager tmf = TlsUtil.trustManager(trustedCertificatesPem); + X509KeyManager kmf = null; + if (privateKeyPem != null && certificatePem != null) { + kmf = TlsUtil.keyManager(privateKeyPem, certificatePem); + } // gRPC does not abstract TLS configuration so we need to check the implementation and act // accordingly. if (managedChannelBuilder.getClass().getName().equals("io.grpc.netty.NettyChannelBuilder")) { NettyChannelBuilder nettyBuilder = (NettyChannelBuilder) managedChannelBuilder; - nettyBuilder.sslContext(GrpcSslContexts.forClient().trustManager(tm).build()); + nettyBuilder.sslContext( + GrpcSslContexts.forClient().keyManager(kmf).trustManager(tmf).build()); } else { throw new SSLException( "TLS certificate configuration not supported for unrecognized ManagedChannelBuilder " @@ -40,6 +50,3 @@ public static void setTrustedCertificatesPem( } } } - -class OtlpExporterSubstitutions { -} diff --git a/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java b/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java index 5d6bac8fc451d..8374f7bf61a1f 100644 --- a/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java +++ b/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java @@ -14,6 +14,7 @@ import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.InterceptorBindingRegistrarBuildItem; import io.quarkus.arc.processor.InterceptorBindingRegistrar; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -130,11 +131,6 @@ void storeVertxOnContextStorage(OpenTelemetryRecorder recorder, CoreVertxBuildIt } public static boolean isClassPresent(String classname) { - try { - Class.forName(classname, false, Thread.currentThread().getContextClassLoader()); - return true; - } catch (ClassNotFoundException e) { - return false; - } + return QuarkusClassLoader.isClassPresentAtRuntime(classname); } } diff --git a/extensions/opentelemetry/opentelemetry/runtime/pom.xml b/extensions/opentelemetry/opentelemetry/runtime/pom.xml index 00d2c9ed05a1e..8901277f01a71 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/pom.xml +++ b/extensions/opentelemetry/opentelemetry/runtime/pom.xml @@ -58,18 +58,22 @@ opentelemetry-extension-annotations - io.opentelemetry.instrumentation - opentelemetry-instrumentation-api + io.opentelemetry + opentelemetry-sdk-extension-autoconfigure-spi - com.google.code.findbugs - jsr305 + * + * + + io.opentelemetry + opentelemetry-semconv + io.opentelemetry.instrumentation - opentelemetry-instrumentation-api-annotation-support + opentelemetry-instrumentation-api com.google.code.findbugs @@ -78,18 +82,18 @@ - io.opentelemetry - opentelemetry-sdk-extension-autoconfigure-spi + io.opentelemetry.instrumentation + opentelemetry-instrumentation-api-annotation-support - * - * + com.google.code.findbugs + jsr305 - io.opentelemetry - opentelemetry-semconv + io.opentelemetry.instrumentation + opentelemetry-instrumentation-api-semconv diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/WithSpanInterceptor.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/WithSpanInterceptor.java index c50518ecd4540..4ca623453e196 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/WithSpanInterceptor.java +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/WithSpanInterceptor.java @@ -21,7 +21,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.SpanNames; +import io.opentelemetry.instrumentation.api.util.SpanNames; @SuppressWarnings("CdiInterceptorInspection") @Interceptor diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/grpc/GrpcAttributesExtractor.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/grpc/GrpcAttributesGetter.java similarity index 52% rename from extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/grpc/GrpcAttributesExtractor.java rename to extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/grpc/GrpcAttributesGetter.java index a124c8b9ac681..5e2a9df02ca40 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/grpc/GrpcAttributesExtractor.java +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/grpc/GrpcAttributesGetter.java @@ -1,21 +1,22 @@ package io.quarkus.opentelemetry.runtime.tracing.grpc; -import io.grpc.Status; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesGetter; + +enum GrpcAttributesGetter implements RpcAttributesGetter { + INSTANCE; -class GrpcAttributesExtractor extends RpcAttributesExtractor { @Override - protected String system(final GrpcRequest grpcRequest) { + public String system(final GrpcRequest grpcRequest) { return "grpc"; } @Override - protected String service(final GrpcRequest grpcRequest) { + public String service(final GrpcRequest grpcRequest) { return grpcRequest.getMethodDescriptor().getServiceName(); } @Override - protected String method(final GrpcRequest grpcRequest) { + public String method(final GrpcRequest grpcRequest) { return grpcRequest.getMethodDescriptor().getBareMethodName(); } } diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/grpc/GrpcTracingClientInterceptor.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/grpc/GrpcTracingClientInterceptor.java index 050276b14df77..6b054906ee89d 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/grpc/GrpcTracingClientInterceptor.java +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/grpc/GrpcTracingClientInterceptor.java @@ -19,6 +19,7 @@ import io.opentelemetry.context.propagation.TextMapSetter; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcClientAttributesExtractor; import io.quarkus.grpc.GlobalInterceptor; @Singleton @@ -35,7 +36,7 @@ public GrpcTracingClientInterceptor(final OpenTelemetry openTelemetry) { INSTRUMENTATION_NAME, new GrpcSpanNameExtractor()); - builder.addAttributesExtractor(new GrpcAttributesExtractor()) + builder.addAttributesExtractor(RpcClientAttributesExtractor.create(GrpcAttributesGetter.INSTANCE)) .addAttributesExtractor(new GrpcStatusCodeExtractor()) .setSpanStatusExtractor(new GrpcSpanStatusExtractor()); diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/grpc/GrpcTracingServerInterceptor.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/grpc/GrpcTracingServerInterceptor.java index 779ab5f29d18f..959188539fe3c 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/grpc/GrpcTracingServerInterceptor.java +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/grpc/GrpcTracingServerInterceptor.java @@ -24,6 +24,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.net.InetSocketAddressNetServerAttributesGetter; import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcServerAttributesExtractor; import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import io.quarkus.grpc.GlobalInterceptor; @@ -38,7 +39,7 @@ public GrpcTracingServerInterceptor(final OpenTelemetry openTelemetry) { INSTRUMENTATION_NAME, new GrpcSpanNameExtractor()); - builder.addAttributesExtractor(new GrpcAttributesExtractor()) + builder.addAttributesExtractor(RpcServerAttributesExtractor.create(GrpcAttributesGetter.INSTANCE)) .addAttributesExtractor(NetServerAttributesExtractor.create(new GrpcServerNetServerAttributesGetter())) .addAttributesExtractor(new GrpcStatusCodeExtractor()) .setSpanStatusExtractor(new GrpcSpanStatusExtractor()); diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/vertx/EventBusInstrumenterVertxTracer.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/vertx/EventBusInstrumenterVertxTracer.java index d2ce9f9f07d8a..4471d4dfcc990 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/vertx/EventBusInstrumenterVertxTracer.java +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/vertx/EventBusInstrumenterVertxTracer.java @@ -8,8 +8,8 @@ import io.opentelemetry.context.propagation.TextMapGetter; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation; import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingSpanNameExtractor; import io.vertx.core.eventbus.Message; import io.vertx.core.spi.tracing.TagExtractor; @@ -50,14 +50,12 @@ public Instrumenter getReceiveResponseInstrumenter() { } private static Instrumenter getConsumerInstrumenter(final OpenTelemetry openTelemetry) { - EventBusAttributesExtractor eventBusAttributesExtractor = new EventBusAttributesExtractor(RECEIVE); - InstrumenterBuilder serverBuilder = Instrumenter.builder( openTelemetry, - INSTRUMENTATION_NAME, MessagingSpanNameExtractor.create(eventBusAttributesExtractor)); + INSTRUMENTATION_NAME, MessagingSpanNameExtractor.create(EventBusAttributesGetter.INSTANCE, RECEIVE)); return serverBuilder - .addAttributesExtractor(eventBusAttributesExtractor) + .addAttributesExtractor(MessagingAttributesExtractor.create(EventBusAttributesGetter.INSTANCE, RECEIVE)) .newConsumerInstrumenter(new TextMapGetter<>() { @Override public Iterable keys(final Message message) { @@ -75,14 +73,12 @@ public String get(final Message message, final String key) { } private static Instrumenter getProducerInstrumenter(final OpenTelemetry openTelemetry) { - EventBusAttributesExtractor eventBusAttributesExtractor = new EventBusAttributesExtractor(SEND); - InstrumenterBuilder serverBuilder = Instrumenter.builder( openTelemetry, - INSTRUMENTATION_NAME, MessagingSpanNameExtractor.create(eventBusAttributesExtractor)); + INSTRUMENTATION_NAME, MessagingSpanNameExtractor.create(EventBusAttributesGetter.INSTANCE, SEND)); return serverBuilder - .addAttributesExtractor(eventBusAttributesExtractor) + .addAttributesExtractor(MessagingAttributesExtractor.create(EventBusAttributesGetter.INSTANCE, SEND)) .newProducerInstrumenter((message, key, value) -> { if (message != null) { message.headers().set(key, value); @@ -90,70 +86,61 @@ private static Instrumenter getProducerInstrumenter(final Open }); } - private static class EventBusAttributesExtractor extends MessagingAttributesExtractor { - private final MessageOperation operation; - - public EventBusAttributesExtractor(final MessageOperation operation) { - this.operation = operation; - } - - @Override - public MessageOperation operation() { - return operation; - } + private enum EventBusAttributesGetter implements MessagingAttributesGetter { + INSTANCE; @Override - protected String system(final Message message) { + public String system(final Message message) { return "vert.x"; } @Override - protected String destinationKind(final Message message) { + public String destinationKind(final Message message) { return message.isSend() ? "queue" : "topic"; } @Override - protected String destination(final Message message) { + public String destination(final Message message) { return message.address(); } @Override - protected boolean temporaryDestination(final Message message) { + public boolean temporaryDestination(final Message message) { return false; } @Override - protected String protocol(final Message message) { + public String protocol(final Message message) { return null; } @Override - protected String protocolVersion(final Message message) { + public String protocolVersion(final Message message) { return "4.0"; } @Override - protected String url(final Message message) { + public String url(final Message message) { return null; } @Override - protected String conversationId(final Message message) { + public String conversationId(final Message message) { return message.replyAddress(); } @Override - protected Long messagePayloadSize(final Message message) { + public Long messagePayloadSize(final Message message) { return null; } @Override - protected Long messagePayloadCompressedSize(final Message message) { + public Long messagePayloadCompressedSize(final Message message) { return null; } @Override - protected String messageId(final Message message, final Message message2) { + public String messageId(final Message message, final Message message2) { return null; } } diff --git a/extensions/panache/hibernate-orm-panache-kotlin/runtime/pom.xml b/extensions/panache/hibernate-orm-panache-kotlin/runtime/pom.xml index dfbfcd17a8bc4..250be01d75f16 100644 --- a/extensions/panache/hibernate-orm-panache-kotlin/runtime/pom.xml +++ b/extensions/panache/hibernate-orm-panache-kotlin/runtime/pom.xml @@ -185,4 +185,28 @@ + + + + jakarta-rewrite + + + jakarta-rewrite + + + + + + org.openrewrite.maven + rewrite-maven-plugin + + + io.quarkus.hibernate-orm-narayana-switch + + + + + + + diff --git a/extensions/panache/hibernate-orm-panache/runtime/pom.xml b/extensions/panache/hibernate-orm-panache/runtime/pom.xml index ec1b11cef0922..26399e79713c5 100644 --- a/extensions/panache/hibernate-orm-panache/runtime/pom.xml +++ b/extensions/panache/hibernate-orm-panache/runtime/pom.xml @@ -109,4 +109,29 @@ + + + + jakarta-rewrite + + + jakarta-rewrite + + + + + + org.openrewrite.maven + rewrite-maven-plugin + + + io.quarkus.jakarta-jaxb-switch + io.quarkus.hibernate-orm-narayana-switch + + + + + + + diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/pom.xml b/extensions/panache/hibernate-orm-rest-data-panache/deployment/pom.xml index 0a23562bece62..7da5feefe7a9b 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/deployment/pom.xml +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/pom.xml @@ -40,6 +40,11 @@ quarkus-resteasy-jsonb-deployment test + + io.quarkus + quarkus-resteasy-links-deployment + test + io.rest-assured rest-assured diff --git a/extensions/panache/hibernate-reactive-panache/runtime/pom.xml b/extensions/panache/hibernate-reactive-panache/runtime/pom.xml index eda49d612646f..6afd3de237a7b 100644 --- a/extensions/panache/hibernate-reactive-panache/runtime/pom.xml +++ b/extensions/panache/hibernate-reactive-panache/runtime/pom.xml @@ -117,4 +117,29 @@ + + + + jakarta-rewrite + + + jakarta-rewrite + + + + + + org.openrewrite.maven + rewrite-maven-plugin + + + io.quarkus.jakarta-jaxb-switch + io.quarkus.hibernate-orm-narayana-switch + + + + + + + diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/pom.xml b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/pom.xml index 3c94e343ebd1e..e3a9b09d1a2eb 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/pom.xml +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/pom.xml @@ -14,7 +14,6 @@ - postgres:14.1 vertx-reactive:postgresql://localhost:5432/hibernate_orm_test diff --git a/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/PanacheMongoEntity.kt b/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/PanacheMongoEntity.kt old mode 100755 new mode 100644 diff --git a/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/PanacheMongoEntityBase.kt b/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/PanacheMongoEntityBase.kt old mode 100755 new mode 100644 diff --git a/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/PanacheMongoRepository.kt b/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/PanacheMongoRepository.kt old mode 100755 new mode 100644 diff --git a/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/PanacheMongoRepositoryBase.kt b/extensions/panache/mongodb-panache-kotlin/runtime/src/main/kotlin/io/quarkus/mongodb/panache/kotlin/PanacheMongoRepositoryBase.kt old mode 100755 new mode 100644 diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntity.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntity.java old mode 100755 new mode 100644 diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntityBase.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntityBase.java old mode 100755 new mode 100644 diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepository.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepository.java old mode 100755 new mode 100644 diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepositoryBase.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepositoryBase.java old mode 100755 new mode 100644 diff --git a/extensions/panache/rest-data-panache/deployment/pom.xml b/extensions/panache/rest-data-panache/deployment/pom.xml index 66c1b0e79bc43..65c66c9a3ad3b 100644 --- a/extensions/panache/rest-data-panache/deployment/pom.xml +++ b/extensions/panache/rest-data-panache/deployment/pom.xml @@ -29,14 +29,9 @@ io.quarkus quarkus-resteasy-common-spi - - - io.quarkus - quarkus-jackson-spi - io.quarkus - quarkus-jsonb-spi + quarkus-hal-deployment 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 2e46e264a0063..0691e13bec031 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 @@ -19,10 +19,7 @@ import io.quarkus.rest.data.panache.deployment.methods.ListMethodImplementor; import io.quarkus.rest.data.panache.deployment.methods.MethodImplementor; import io.quarkus.rest.data.panache.deployment.methods.UpdateMethodImplementor; -import io.quarkus.rest.data.panache.deployment.methods.hal.AddHalMethodImplementor; -import io.quarkus.rest.data.panache.deployment.methods.hal.GetHalMethodImplementor; import io.quarkus.rest.data.panache.deployment.methods.hal.ListHalMethodImplementor; -import io.quarkus.rest.data.panache.deployment.methods.hal.UpdateHalMethodImplementor; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; import io.quarkus.runtime.util.HashUtil; @@ -36,16 +33,14 @@ class JaxRsResourceImplementor { private final List methodImplementors; JaxRsResourceImplementor(boolean withValidation, boolean isResteasyClassic, boolean isReactivePanache) { - this.methodImplementors = Arrays.asList( - new GetMethodImplementor(isResteasyClassic, isReactivePanache), - new GetHalMethodImplementor(isResteasyClassic, isReactivePanache), + this.methodImplementors = Arrays.asList(new GetMethodImplementor(isResteasyClassic, isReactivePanache), new ListMethodImplementor(isResteasyClassic, isReactivePanache), - new ListHalMethodImplementor(isResteasyClassic, isReactivePanache), new AddMethodImplementor(withValidation, isResteasyClassic, isReactivePanache), - new AddHalMethodImplementor(withValidation, isResteasyClassic, isReactivePanache), new UpdateMethodImplementor(withValidation, isResteasyClassic, isReactivePanache), - new UpdateHalMethodImplementor(withValidation, isResteasyClassic, isReactivePanache), - new DeleteMethodImplementor(isResteasyClassic, isReactivePanache)); + new DeleteMethodImplementor(isResteasyClassic, isReactivePanache), + // The list hal endpoint needs to be added for both resteasy classic and resteasy reactive + // because the pagination links are programmatically added. + new ListHalMethodImplementor(isResteasyClassic, isReactivePanache)); } /** diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/RestDataProcessor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/RestDataProcessor.java index a9fe647b86743..c1843ab0e6a7f 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/RestDataProcessor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/RestDataProcessor.java @@ -1,10 +1,8 @@ package io.quarkus.rest.data.panache.deployment; -import java.util.Arrays; import java.util.Collections; import java.util.List; -import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.deployment.Capabilities; @@ -12,25 +10,11 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.gizmo.ClassOutput; -import io.quarkus.jackson.spi.JacksonModuleBuildItem; -import io.quarkus.jsonb.spi.JsonbSerializerBuildItem; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; import io.quarkus.rest.data.panache.deployment.properties.ResourcePropertiesBuildItem; import io.quarkus.rest.data.panache.deployment.properties.ResourcePropertiesProvider; -import io.quarkus.rest.data.panache.runtime.hal.HalCollectionWrapper; -import io.quarkus.rest.data.panache.runtime.hal.HalCollectionWrapperJacksonSerializer; -import io.quarkus.rest.data.panache.runtime.hal.HalCollectionWrapperJsonbSerializer; -import io.quarkus.rest.data.panache.runtime.hal.HalEntityWrapper; -import io.quarkus.rest.data.panache.runtime.hal.HalEntityWrapperJacksonSerializer; -import io.quarkus.rest.data.panache.runtime.hal.HalEntityWrapperJsonbSerializer; -import io.quarkus.rest.data.panache.runtime.hal.HalLink; -import io.quarkus.rest.data.panache.runtime.hal.HalLinkJacksonSerializer; -import io.quarkus.rest.data.panache.runtime.hal.HalLinkJsonbSerializer; -import io.quarkus.rest.data.panache.runtime.resource.RESTEasyClassicResourceLinksProvider; -import io.quarkus.rest.data.panache.runtime.resource.RESTEasyReactiveResourceLinksProvider; import io.quarkus.rest.data.panache.runtime.sort.SortQueryParamFilter; import io.quarkus.rest.data.panache.runtime.sort.SortQueryParamValidator; import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; @@ -40,14 +24,8 @@ public class RestDataProcessor { - @BuildStep - ReflectiveClassBuildItem registerReflection() { - return new ReflectiveClassBuildItem(true, true, HalLink.class); - } - @BuildStep void supportingBuildItems(Capabilities capabilities, - BuildProducer additionalBeanBuildItemBuildProducer, BuildProducer runtimeInitializedClassBuildItemBuildProducer, BuildProducer resteasyJaxrsProviderBuildItemBuildProducer, BuildProducer containerRequestFilterBuildItemBuildProducer) { @@ -56,14 +34,9 @@ void supportingBuildItems(Capabilities capabilities, if (!isResteasyClassicAvailable && !isResteasyReactiveAvailable) { throw new IllegalStateException( - "REST Data Panache can only work if 'quarkus-resteasy' or 'quarkus-resteasy-reactive-links' is present"); + "REST Data Panache can only work if 'quarkus-resteasy' or 'quarkus-resteasy-reactive' is present"); } - String className = isResteasyClassicAvailable ? RESTEasyClassicResourceLinksProvider.class.getName() - : RESTEasyReactiveResourceLinksProvider.class.getName(); - additionalBeanBuildItemBuildProducer - .produce(AdditionalBeanBuildItem.builder().addBeanClass(className).setUnremovable().build()); - if (isResteasyClassicAvailable) { runtimeInitializedClassBuildItemBuildProducer .produce(new RuntimeInitializedClassBuildItem("org.jboss.resteasy.links.impl.EL")); @@ -100,9 +73,15 @@ void implementResources(CombinedIndexBuildItem index, List resourcePropertiesBuildItems) { for (ResourcePropertiesBuildItem resourcePropertiesBuildItem : resourcePropertiesBuildItems) { @@ -143,10 +104,13 @@ private boolean hasValidatorCapability(Capabilities capabilities) { return capabilities.isPresent(Capability.HIBERNATE_VALIDATOR); } - private boolean hasHalCapability(Capabilities capabilities) { + private boolean hasAnyJsonCapabilityForResteasyClassic(Capabilities capabilities) { return capabilities.isPresent(Capability.RESTEASY_JSON_JSONB) - || capabilities.isPresent(Capability.RESTEASY_JSON_JACKSON) - || capabilities.isPresent(Capability.RESTEASY_REACTIVE_JSON_JSONB) + || capabilities.isPresent(Capability.RESTEASY_JSON_JACKSON); + } + + private boolean hasAnyJsonCapabilityForResteasyReactive(Capabilities capabilities) { + return capabilities.isPresent(Capability.RESTEASY_REACTIVE_JSON_JSONB) || capabilities.isPresent(Capability.RESTEASY_REACTIVE_JSON_JACKSON); } } diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/AddMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/AddMethodImplementor.java index 19836c02c87d6..8ffddc7a573dc 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/AddMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/AddMethodImplementor.java @@ -13,7 +13,6 @@ import io.quarkus.rest.data.panache.RestDataResource; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; import io.smallrye.mutiny.Uni; @@ -109,7 +108,7 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res addPathAnnotation(methodCreator, resourceProperties.getPath(RESOURCE_METHOD_NAME)); addPostAnnotation(methodCreator); addConsumesAnnotation(methodCreator, APPLICATION_JSON); - addProducesAnnotation(methodCreator, APPLICATION_JSON); + addProducesJsonAnnotation(methodCreator, resourceProperties); addLinksAnnotation(methodCreator, resourceMetadata.getEntityType(), REL); // Add parameter annotations if (withValidation) { @@ -124,7 +123,7 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res ResultHandle entity = tryBlock.invokeVirtualMethod( ofMethod(resourceMetadata.getResourceClass(), RESOURCE_METHOD_NAME, Object.class, Object.class), resource, entityToSave); - tryBlock.returnValue(ResponseImplementor.created(tryBlock, entity)); + tryBlock.returnValue(responseImplementor.created(tryBlock, entity)); tryBlock.close(); } else { ResultHandle uniEntity = methodCreator.invokeVirtualMethod( @@ -132,7 +131,7 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res resource, entityToSave); methodCreator.returnValue(UniImplementor.map(methodCreator, uniEntity, EXCEPTION_MESSAGE, - (body, item) -> body.returnValue(ResponseImplementor.created(body, item)))); + (body, item) -> body.returnValue(responseImplementor.created(body, item)))); } methodCreator.close(); diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/DeleteMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/DeleteMethodImplementor.java index 93265c9bec692..a312b8d7ece56 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/DeleteMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/DeleteMethodImplementor.java @@ -13,7 +13,6 @@ import io.quarkus.rest.data.panache.RestDataResource; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; import io.smallrye.mutiny.Uni; @@ -105,8 +104,8 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res // Return response BranchResult entityWasDeleted = tryBlock.ifNonZero(deleted); - entityWasDeleted.trueBranch().returnValue(ResponseImplementor.noContent(entityWasDeleted.trueBranch())); - entityWasDeleted.falseBranch().returnValue(ResponseImplementor.notFound(entityWasDeleted.falseBranch())); + entityWasDeleted.trueBranch().returnValue(responseImplementor.noContent(entityWasDeleted.trueBranch())); + entityWasDeleted.falseBranch().returnValue(responseImplementor.notFound(entityWasDeleted.falseBranch())); tryBlock.close(); } else { @@ -124,9 +123,10 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res ofMethod(Boolean.class, "compareTo", int.class, Boolean.class), deleted, falseDefault); BranchResult entityWasDeleted = body.ifNonZero(deletedAsInt); - entityWasDeleted.trueBranch().returnValue(ResponseImplementor.noContent(entityWasDeleted.trueBranch())); + entityWasDeleted.trueBranch() + .returnValue(responseImplementor.noContent(entityWasDeleted.trueBranch())); entityWasDeleted.falseBranch() - .returnValue(ResponseImplementor.notFound(entityWasDeleted.falseBranch())); + .returnValue(responseImplementor.notFound(entityWasDeleted.falseBranch())); })); } diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/GetMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/GetMethodImplementor.java index d495d467d9bcf..0d3b4a1a5121f 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/GetMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/GetMethodImplementor.java @@ -13,7 +13,6 @@ import io.quarkus.rest.data.panache.RestDataResource; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; import io.smallrye.mutiny.Uni; @@ -93,7 +92,8 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res // Add method annotations addPathAnnotation(methodCreator, appendToPath(resourceProperties.getPath(RESOURCE_METHOD_NAME), "{id}")); addGetAnnotation(methodCreator); - addProducesAnnotation(methodCreator, APPLICATION_JSON); + addProducesJsonAnnotation(methodCreator, resourceProperties); + addPathParamAnnotation(methodCreator.getParameterAnnotations(0), "id"); addLinksAnnotation(methodCreator, resourceMetadata.getEntityType(), REL); @@ -108,8 +108,8 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res // Return response BranchResult wasNotFound = tryBlock.ifNull(entity); - wasNotFound.trueBranch().returnValue(ResponseImplementor.notFound(wasNotFound.trueBranch())); - wasNotFound.falseBranch().returnValue(ResponseImplementor.ok(wasNotFound.falseBranch(), entity)); + wasNotFound.trueBranch().returnValue(responseImplementor.notFound(wasNotFound.trueBranch())); + wasNotFound.falseBranch().returnValue(responseImplementor.ok(wasNotFound.falseBranch(), entity)); tryBlock.close(); } else { @@ -121,9 +121,9 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res (body, entity) -> { BranchResult entityWasNotFound = body.ifNull(entity); entityWasNotFound.trueBranch() - .returnValue(ResponseImplementor.notFound(entityWasNotFound.trueBranch())); + .returnValue(responseImplementor.notFound(entityWasNotFound.trueBranch())); entityWasNotFound.falseBranch() - .returnValue(ResponseImplementor.ok(entityWasNotFound.falseBranch(), entity)); + .returnValue(responseImplementor.ok(entityWasNotFound.falseBranch(), entity)); })); } diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/ListMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/ListMethodImplementor.java index 6bd29edd5d6a3..bfd1544b8c988 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/ListMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/ListMethodImplementor.java @@ -21,7 +21,6 @@ import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; import io.quarkus.rest.data.panache.deployment.utils.PaginationImplementor; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; import io.quarkus.rest.data.panache.deployment.utils.SortImplementor; import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; import io.smallrye.mutiny.Uni; @@ -171,7 +170,7 @@ private void implementPaged(ClassCreator classCreator, ResourceMetadata resource resource, page, sort); // Return response - tryBlock.returnValue(ResponseImplementor.ok(tryBlock, entities, links)); + tryBlock.returnValue(responseImplementor.ok(tryBlock, entities, links)); tryBlock.close(); } else { ResultHandle uniPageCount = methodCreator.invokeVirtualMethod( @@ -188,7 +187,7 @@ private void implementPaged(ClassCreator classCreator, ResourceMetadata resource Sort.class), resource, page, sort); body.returnValue(UniImplementor.map(body, uniEntities, EXCEPTION_MESSAGE, - (listBody, list) -> listBody.returnValue(ResponseImplementor.ok(listBody, list, links)))); + (listBody, list) -> listBody.returnValue(responseImplementor.ok(listBody, list, links)))); })); } @@ -218,7 +217,7 @@ private void implementNotPaged(ClassCreator classCreator, ResourceMetadata resou ofMethod(resourceMetadata.getResourceClass(), RESOURCE_METHOD_NAME, List.class, Page.class, Sort.class), resource, tryBlock.loadNull(), sort); - tryBlock.returnValue(ResponseImplementor.ok(tryBlock, entities)); + tryBlock.returnValue(responseImplementor.ok(tryBlock, entities)); tryBlock.close(); } else { ResultHandle uniEntities = methodCreator.invokeVirtualMethod( @@ -227,7 +226,7 @@ private void implementNotPaged(ClassCreator classCreator, ResourceMetadata resou resource, methodCreator.loadNull(), sort); methodCreator.returnValue(UniImplementor.map(methodCreator, uniEntities, EXCEPTION_MESSAGE, - (body, entities) -> body.returnValue(ResponseImplementor.ok(body, entities)))); + (body, entities) -> body.returnValue(responseImplementor.ok(body, entities)))); } methodCreator.close(); diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/StandardMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/StandardMethodImplementor.java index cf90e6b3b0e35..1cb56a8484253 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/StandardMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/StandardMethodImplementor.java @@ -24,6 +24,7 @@ import io.quarkus.rest.data.panache.RestDataPanacheException; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; +import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; import io.quarkus.rest.data.panache.runtime.sort.SortQueryParamValidator; /** @@ -33,12 +34,14 @@ public abstract class StandardMethodImplementor implements MethodImplementor { private static final Logger LOGGER = Logger.getLogger(StandardMethodImplementor.class); + protected final ResponseImplementor responseImplementor; private final boolean isResteasyClassic; private final boolean isReactivePanache; protected StandardMethodImplementor(boolean isResteasyClassic, boolean isReactivePanache) { this.isResteasyClassic = isResteasyClassic; this.isReactivePanache = isReactivePanache; + this.responseImplementor = new ResponseImplementor(isResteasyClassic); } /** @@ -121,6 +124,14 @@ protected void addDefaultValueAnnotation(AnnotatedElement element, String value) element.addAnnotation(DefaultValue.class).addValue("value", value); } + protected void addProducesJsonAnnotation(AnnotatedElement element, ResourceProperties properties) { + if (properties.isHal()) { + addProducesAnnotation(element, APPLICATION_JSON, APPLICATION_HAL_JSON); + } else { + addProducesAnnotation(element, APPLICATION_JSON); + } + } + protected void addProducesAnnotation(AnnotatedElement element, String... mediaTypes) { element.addAnnotation(Produces.class).addValue("value", mediaTypes); } @@ -147,6 +158,10 @@ protected String appendToPath(String path, String suffix) { return String.join("/", path, suffix); } + protected boolean isResteasyClassic() { + return isResteasyClassic; + } + protected boolean isNotReactivePanache() { return !isReactivePanache; } diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/UpdateMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/UpdateMethodImplementor.java index c5b64ff2129b4..6924d7ab17bab 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/UpdateMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/UpdateMethodImplementor.java @@ -23,7 +23,6 @@ import io.quarkus.rest.data.panache.RestDataResource; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; import io.quarkus.rest.data.panache.runtime.UpdateExecutor; import io.smallrye.mutiny.Uni; @@ -142,7 +141,7 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res addPutAnnotation(methodCreator); addPathParamAnnotation(methodCreator.getParameterAnnotations(0), "id"); addConsumesAnnotation(methodCreator, APPLICATION_JSON); - addProducesAnnotation(methodCreator, APPLICATION_JSON); + addProducesJsonAnnotation(methodCreator, resourceProperties); addLinksAnnotation(methodCreator, resourceMetadata.getEntityType(), REL); // Add parameter annotations if (withValidation) { @@ -185,9 +184,9 @@ private void implementReactiveVersion(MethodCreator methodCreator, ResourceMetad (updateBody, itemUpdated) -> { BranchResult ifEntityIsNew = updateBody.ifNull(itemWasFound); ifEntityIsNew.trueBranch() - .returnValue(ResponseImplementor.created(ifEntityIsNew.trueBranch(), itemUpdated)); + .returnValue(responseImplementor.created(ifEntityIsNew.trueBranch(), itemUpdated)); ifEntityIsNew.falseBranch() - .returnValue(ResponseImplementor.noContent(ifEntityIsNew.falseBranch())); + .returnValue(responseImplementor.noContent(ifEntityIsNew.falseBranch())); })); })); } @@ -206,8 +205,8 @@ private void implementClassicVersion(MethodCreator methodCreator, ResourceMetada updateExecutor, updateFunction); BranchResult createdNewEntity = tryBlock.ifNotNull(newEntity); - createdNewEntity.trueBranch().returnValue(ResponseImplementor.created(createdNewEntity.trueBranch(), newEntity)); - createdNewEntity.falseBranch().returnValue(ResponseImplementor.noContent(createdNewEntity.falseBranch())); + createdNewEntity.trueBranch().returnValue(responseImplementor.created(createdNewEntity.trueBranch(), newEntity)); + createdNewEntity.falseBranch().returnValue(responseImplementor.noContent(createdNewEntity.falseBranch())); } private ResultHandle getUpdateFunction(BytecodeCreator creator, String resourceClass, ResultHandle resource, diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/AddHalMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/AddHalMethodImplementor.java deleted file mode 100644 index 36323f7824366..0000000000000 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/AddHalMethodImplementor.java +++ /dev/null @@ -1,143 +0,0 @@ -package io.quarkus.rest.data.panache.deployment.methods.hal; - -import static io.quarkus.gizmo.MethodDescriptor.ofMethod; - -import javax.validation.Valid; -import javax.ws.rs.core.Response; - -import io.quarkus.gizmo.ClassCreator; -import io.quarkus.gizmo.FieldDescriptor; -import io.quarkus.gizmo.MethodCreator; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.gizmo.TryBlock; -import io.quarkus.rest.data.panache.RestDataResource; -import io.quarkus.rest.data.panache.deployment.ResourceMetadata; -import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; -import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; -import io.smallrye.mutiny.Uni; - -public final class AddHalMethodImplementor extends HalMethodImplementor { - - private static final String METHOD_NAME = "addHal"; - - private static final String RESOURCE_METHOD_NAME = "add"; - - private static final String EXCEPTION_MESSAGE = "Failed to add an entity"; - - private final boolean withValidation; - - public AddHalMethodImplementor(boolean withValidation, boolean isResteasyClassic, boolean isReactivePanache) { - super(isResteasyClassic, isReactivePanache); - this.withValidation = withValidation; - } - - /** - * Generate HAL JAX-RS POST method. - * - * The RESTEasy Classic version exposes {@link RestDataResource#add(Object)} via HAL JAX-RS method. - * Generated code looks more or less like this: - * - *

-     * {@code
-     *     @POST
-     *     @Path("")
-     *     @Consumes({"application/json"})
-     *     @Produces({"application/hal+json"})
-     *     public Response addHal(Entity entityToSave) {
-     *         try {
-     *             Entity entity = resource.add(entityToSave);
-     *             HalEntityWrapper wrapper = new HalEntityWrapper(entity);
-     *             String location = new ResourceLinksProvider().getSelfLink(entity);
-     *             if (location != null) {
-     *                 ResponseBuilder responseBuilder = Response.status(201);
-     *                 responseBuilder.entity(wrapper);
-     *                 responseBuilder.location(URI.create(location));
-     *                 return responseBuilder.build();
-     *             } else {
-     *                 throw new RuntimeException("Could not extract a new entity URL");
-     *             }
-     *         } catch (Throwable t) {
-     *             throw new RestDataPanacheException(t);
-     *         }
-     *     }
-     * }
-     * 
- * - * The RESTEasy Reactive version exposes {@link io.quarkus.rest.data.panache.ReactiveRestDataResource#add(Object)} - * and the generated code looks more or less like this: - * - *
-     * {@code
-     *     @POST
-     *     @Path("")
-     *     @Consumes({"application/json"})
-     *     @Produces({"application/hal+json"})
-     *     public Uni addHal(Entity entityToSave) {
-     *
-     *         return resource.add(entityToSave).map(entity -> {
-     *             HalEntityWrapper wrapper = new HalEntityWrapper(entity);
-     *             String location = new ResourceLinksProvider().getSelfLink(entity);
-     *             if (location != null) {
-     *                 ResponseBuilder responseBuilder = Response.status(201);
-     *                 responseBuilder.entity(wrapper);
-     *                 responseBuilder.location(URI.create(location));
-     *                 return responseBuilder.build();
-     *             } else {
-     *                 throw new RuntimeException("Could not extract a new entity URL");
-     *             }
-     *         }).onFailure().invoke(t -> throw new RestDataPanacheException(t));
-     *     }
-     * }
-     * 
- * - */ - @Override - protected void implementInternal(ClassCreator classCreator, ResourceMetadata resourceMetadata, - ResourceProperties resourceProperties, FieldDescriptor resourceField) { - MethodCreator methodCreator = classCreator.getMethodCreator(METHOD_NAME, - isNotReactivePanache() ? Response.class : Uni.class, - resourceMetadata.getEntityType()); - - // Add method annotations - addPathAnnotation(methodCreator, resourceProperties.getPath(RESOURCE_METHOD_NAME)); - addPostAnnotation(methodCreator); - addConsumesAnnotation(methodCreator, APPLICATION_JSON); - addProducesAnnotation(methodCreator, APPLICATION_HAL_JSON); - // Add parameter annotations - if (withValidation) { - methodCreator.getParameterAnnotations(0).addAnnotation(Valid.class); - } - - ResultHandle resource = methodCreator.readInstanceField(resourceField, methodCreator.getThis()); - ResultHandle entityToSave = methodCreator.getMethodParam(0); - - if (isNotReactivePanache()) { - TryBlock tryBlock = implementTryBlock(methodCreator, EXCEPTION_MESSAGE); - ResultHandle entity = tryBlock.invokeVirtualMethod( - ofMethod(resourceMetadata.getResourceClass(), RESOURCE_METHOD_NAME, Object.class, Object.class), - resource, entityToSave); - - // Wrap and return response - tryBlock.returnValue(ResponseImplementor.created(tryBlock, wrapHalEntity(tryBlock, entity), - ResponseImplementor.getEntityUrl(tryBlock, entity))); - - tryBlock.close(); - } else { - ResultHandle uniEntity = methodCreator.invokeVirtualMethod( - ofMethod(resourceMetadata.getResourceClass(), RESOURCE_METHOD_NAME, Uni.class, Object.class), - resource, entityToSave); - - methodCreator.returnValue(UniImplementor.map(methodCreator, uniEntity, EXCEPTION_MESSAGE, - (body, item) -> body.returnValue(ResponseImplementor.created(body, wrapHalEntity(body, item), - ResponseImplementor.getEntityUrl(body, item))))); - } - - methodCreator.close(); - } - - @Override - protected String getResourceMethodName() { - return RESOURCE_METHOD_NAME; - } -} diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/GetHalMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/GetHalMethodImplementor.java deleted file mode 100644 index ba632e8f9063e..0000000000000 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/GetHalMethodImplementor.java +++ /dev/null @@ -1,128 +0,0 @@ -package io.quarkus.rest.data.panache.deployment.methods.hal; - -import static io.quarkus.gizmo.MethodDescriptor.ofMethod; - -import javax.ws.rs.core.Response; - -import io.quarkus.gizmo.BranchResult; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.ClassCreator; -import io.quarkus.gizmo.FieldDescriptor; -import io.quarkus.gizmo.MethodCreator; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.gizmo.TryBlock; -import io.quarkus.rest.data.panache.RestDataResource; -import io.quarkus.rest.data.panache.deployment.ResourceMetadata; -import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; -import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; -import io.smallrye.mutiny.Uni; - -public final class GetHalMethodImplementor extends HalMethodImplementor { - - private static final String METHOD_NAME = "getHal"; - - private static final String RESOURCE_METHOD_NAME = "get"; - - private static final String EXCEPTION_MESSAGE = "Failed to get an entity"; - - public GetHalMethodImplementor(boolean isResteasyClassic, boolean isReactivePanache) { - super(isResteasyClassic, isReactivePanache); - } - - /** - * Generate HAL JAX-RS GET method. - * - * The RESTEasy Classic version exposes {@link RestDataResource#get(Object)} via HAL JAX-RS method. - * Generated code looks more or less like this: - * - *
-     * {@code
-     *     @GET
-     *     @Produces({"application/hal+json"})
-     *     @Path("{id}")
-     *     public Response getHal(@PathParam("id") ID id) {
-     *         try {
-     *             Entity entity = resource.get(id);
-     *             if (entity != null) {
-     *                 return Response.ok(new HalEntityWrapper(entity)).build();
-     *             } else {
-     *                 return Response.status(404).build();
-     *             }
-     *         } catch (Throwable t) {
-     *             throw new RestDataPanacheException(t);
-     *         }
-     *     }
-     * }
-     * 
- * - * The RESTEasy Reactive version exposes {@link io.quarkus.rest.data.panache.ReactiveRestDataResource#get(Object)} - * and the generated code looks more or less like this: - * - *
-     * {@code
-     *     @GET
-     *     @Produces({"application/hal+json"})
-     *     @Path("{id}")
-     *     public Uni getHal(@PathParam("id") ID id) {
-     *         return resource.get(id).map(entity -> {
-     *             if (entity != null) {
-     *                 return Response.ok(new HalEntityWrapper(entity)).build();
-     *             } else {
-     *                 return Response.status(404).build();
-     *             }
-     *         }).onFailure().invoke(t -> throw new RestDataPanacheException(t));
-     *     }
-     * }
-     * 
- */ - @Override - protected void implementInternal(ClassCreator classCreator, ResourceMetadata resourceMetadata, - ResourceProperties resourceProperties, FieldDescriptor resourceField) { - MethodCreator methodCreator = classCreator.getMethodCreator(METHOD_NAME, - isNotReactivePanache() ? Response.class : Uni.class, - resourceMetadata.getIdType()); - - // Add method annotations - addPathAnnotation(methodCreator, appendToPath(resourceProperties.getPath(RESOURCE_METHOD_NAME), "{id}")); - addGetAnnotation(methodCreator); - addProducesAnnotation(methodCreator, APPLICATION_HAL_JSON); - addPathParamAnnotation(methodCreator.getParameterAnnotations(0), "id"); - - ResultHandle resource = methodCreator.readInstanceField(resourceField, methodCreator.getThis()); - ResultHandle id = methodCreator.getMethodParam(0); - - if (isNotReactivePanache()) { - TryBlock tryBlock = implementTryBlock(methodCreator, EXCEPTION_MESSAGE); - ResultHandle entity = tryBlock.invokeVirtualMethod( - ofMethod(resourceMetadata.getResourceClass(), RESOURCE_METHOD_NAME, Object.class, Object.class), - resource, id); - - // Wrap and return response - ifNullReturnNotFound(tryBlock, entity); - - tryBlock.close(); - } else { - ResultHandle uniEntity = methodCreator.invokeVirtualMethod( - ofMethod(resourceMetadata.getResourceClass(), RESOURCE_METHOD_NAME, Uni.class, Object.class), - resource, id); - - methodCreator.returnValue(UniImplementor.map(methodCreator, uniEntity, EXCEPTION_MESSAGE, - (body, entity) -> ifNullReturnNotFound(body, entity))); - } - - methodCreator.close(); - } - - @Override - protected String getResourceMethodName() { - return RESOURCE_METHOD_NAME; - } - - private void ifNullReturnNotFound(BytecodeCreator body, ResultHandle entity) { - BranchResult wasNotFound = body.ifNull(entity); - wasNotFound.trueBranch().returnValue(ResponseImplementor.notFound(wasNotFound.trueBranch())); - wasNotFound.falseBranch().returnValue( - ResponseImplementor.ok(wasNotFound.falseBranch(), wrapHalEntity(wasNotFound.falseBranch(), entity))); - } -} diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/HalMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/HalMethodImplementor.java index 73f27d5ec1496..4a880938bd89e 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/HalMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/HalMethodImplementor.java @@ -1,17 +1,25 @@ package io.quarkus.rest.data.panache.deployment.methods.hal; +import static io.quarkus.gizmo.MethodDescriptor.ofMethod; + +import java.lang.annotation.Annotation; import java.util.Collection; +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.InstanceHandle; import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; +import io.quarkus.hal.HalCollectionWrapper; +import io.quarkus.hal.HalService; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.methods.StandardMethodImplementor; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.runtime.hal.HalCollectionWrapper; -import io.quarkus.rest.data.panache.runtime.hal.HalEntityWrapper; +import io.quarkus.resteasy.links.runtime.hal.ResteasyHalService; +import io.quarkus.resteasy.reactive.links.runtime.hal.ResteasyReactiveHalService; /** * HAL JAX-RS method implementor. @@ -33,15 +41,19 @@ public void implement(ClassCreator classCreator, ResourceMetadata resourceMetada } } - protected ResultHandle wrapHalEntity(BytecodeCreator creator, ResultHandle entity) { - return creator.newInstance(MethodDescriptor.ofConstructor(HalEntityWrapper.class, Object.class), entity); - } - protected ResultHandle wrapHalEntities(BytecodeCreator creator, ResultHandle entities, String entityType, String collectionName) { - return creator.newInstance( - MethodDescriptor.ofConstructor(HalCollectionWrapper.class, Collection.class, Class.class, String.class), - entities, creator.loadClassFromTCCL(entityType), - creator.load(collectionName)); + ResultHandle arcContainer = creator.invokeStaticMethod(ofMethod(Arc.class, "container", ArcContainer.class)); + ResultHandle instanceHandle = creator.invokeInterfaceMethod( + ofMethod(ArcContainer.class, "instance", InstanceHandle.class, Class.class, Annotation[].class), + arcContainer, + creator.loadClassFromTCCL(isResteasyClassic() ? ResteasyHalService.class : ResteasyReactiveHalService.class), + creator.newArray(Annotation.class, 0)); + ResultHandle halService = creator.invokeInterfaceMethod( + ofMethod(InstanceHandle.class, "get", Object.class), instanceHandle); + + return creator.invokeVirtualMethod(MethodDescriptor.ofMethod(HalService.class, "toHalCollectionWrapper", + HalCollectionWrapper.class, Collection.class, String.class, Class.class), + halService, entities, creator.load(collectionName), creator.loadClassFromTCCL(entityType)); } } diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/ListHalMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/ListHalMethodImplementor.java index 2c963d25be481..cd5f899397b15 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/ListHalMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/ListHalMethodImplementor.java @@ -16,6 +16,7 @@ import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.ResultHandle; import io.quarkus.gizmo.TryBlock; +import io.quarkus.hal.HalCollectionWrapper; import io.quarkus.panache.common.Page; import io.quarkus.panache.common.Sort; import io.quarkus.rest.data.panache.RestDataResource; @@ -23,10 +24,8 @@ import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; import io.quarkus.rest.data.panache.deployment.utils.PaginationImplementor; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; import io.quarkus.rest.data.panache.deployment.utils.SortImplementor; import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; -import io.quarkus.rest.data.panache.runtime.hal.HalCollectionWrapper; import io.smallrye.mutiny.Uni; public final class ListHalMethodImplementor extends HalMethodImplementor { @@ -190,9 +189,10 @@ private void returnWrappedHalEntitiesWithLinks(BytecodeCreator body, ResourceMet ResultHandle wrapper = wrapHalEntities(body, entities, resourceMetadata.getEntityType(), resourceProperties.getHalCollectionName()); + body.invokeVirtualMethod( ofMethod(HalCollectionWrapper.class, "addLinks", void.class, Link[].class), wrapper, links); - body.returnValue(ResponseImplementor.ok(body, wrapper, links)); + body.returnValue(responseImplementor.ok(body, wrapper, links)); } private void implementNotPaged(ClassCreator classCreator, ResourceMetadata resourceMetadata, @@ -237,6 +237,6 @@ private void returnWrappedHalEntities(BytecodeCreator body, ResourceMetadata res ResultHandle entities) { ResultHandle wrapper = wrapHalEntities(body, entities, resourceMetadata.getEntityType(), resourceProperties.getHalCollectionName()); - body.returnValue(ResponseImplementor.ok(body, wrapper)); + body.returnValue(responseImplementor.ok(body, wrapper)); } } diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/UpdateHalMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/UpdateHalMethodImplementor.java deleted file mode 100644 index 457338aeb1017..0000000000000 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/UpdateHalMethodImplementor.java +++ /dev/null @@ -1,259 +0,0 @@ -package io.quarkus.rest.data.panache.deployment.methods.hal; - -import static io.quarkus.gizmo.MethodDescriptor.ofMethod; - -import java.lang.annotation.Annotation; -import java.util.function.Supplier; - -import javax.validation.Valid; -import javax.ws.rs.core.Response; - -import io.quarkus.arc.Arc; -import io.quarkus.arc.ArcContainer; -import io.quarkus.arc.InstanceHandle; -import io.quarkus.gizmo.AssignableResultHandle; -import io.quarkus.gizmo.BranchResult; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.ClassCreator; -import io.quarkus.gizmo.FieldDescriptor; -import io.quarkus.gizmo.FunctionCreator; -import io.quarkus.gizmo.MethodCreator; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.gizmo.TryBlock; -import io.quarkus.rest.data.panache.RestDataResource; -import io.quarkus.rest.data.panache.deployment.ResourceMetadata; -import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; -import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; -import io.quarkus.rest.data.panache.runtime.UpdateExecutor; -import io.smallrye.mutiny.Uni; - -public final class UpdateHalMethodImplementor extends HalMethodImplementor { - - private static final String METHOD_NAME = "updateHal"; - - private static final String RESOURCE_UPDATE_METHOD_NAME = "update"; - - private static final String RESOURCE_GET_METHOD_NAME = "get"; - - private static final String EXCEPTION_MESSAGE = "Failed to update an entity"; - - private final boolean withValidation; - - public UpdateHalMethodImplementor(boolean withValidation, boolean isResteasyClassic, boolean isReactivePanache) { - super(isResteasyClassic, isReactivePanache); - this.withValidation = withValidation; - } - - /** - * Generate HAL JAX-RS PUT method. - * - * The RESTEasy Classic version exposes {@link RestDataResource#update(Object, Object)} via HAL JAX-RS method. - * Generated code looks more or less like this: - * - *
-     * {@code
-     *     @PUT
-     *     @Path("{id}")
-     *     @Consumes({"application/json"})
-     *     @Produces({"application/hal+json"})
-     *     public Response updateHal(@PathParam("id") ID id, Entity entityToSave) {
-     *         try {
-     *             Object newEntity = updateExecutor.execute(() -> {
-     *                 if (resource.get(id) == null) {
-     *                     return resource.update(id, entityToSave);
-     *                 } else {
-     *                     resource.update(id, entityToSave);
-     *                     return null;
-     *                 }
-     *             });
-     *
-     *             if (newEntity == null) {
-     *                 return Response.status(204).build();
-     *             } else {
-     *                 String location = new ResourceLinksProvider().getSelfLink(newEntity);
-     *                 if (location != null) {
-     *                     ResponseBuilder responseBuilder = Response.status(201);
-     *                     responseBuilder.entity(new HalEntityWrapper(newEntity));
-     *                     responseBuilder.location(URI.create(location));
-     *                     return responseBuilder.build();
-     *                 } else {
-     *                     throw new RuntimeException("Could not extract a new entity URL")
-     *                 }
-     *             }
-     *         } catch (Throwable t) {
-     *             throw new RestDataPanacheException(t);
-     *         }
-     *     }
-     * }
-     * 
- * - * The RESTEasy Reactive version exposes - * {@link io.quarkus.rest.data.panache.ReactiveRestDataResource#update(Object, Object)} - * and the generated code looks more or less like this: - * - *
-     * {@code
-     *     @PUT
-     *     @Path("{id}")
-     *     @Consumes({"application/json"})
-     *     @Produces({"application/json"})
-     *     @LinkResource(
-     *         rel = "update",
-     *         entityClassName = "com.example.Entity"
-     *     )
-     *     public Uni update(@PathParam("id") ID id, Entity entityToSave) {
-     *         return resource.get(id).flatMap(entity -> {
-     *             if (entity == null) {
-     *                 return Uni.createFrom().item(Response.status(204).build());
-     *             } else {
-     *                 return resource.update(id, entityToSave).map(savedEntity -> {
-     *                     String location = new ResourceLinksProvider().getSelfLink(savedEntity);
-     *                     if (location != null) {
-     *                         ResponseBuilder responseBuilder = Response.status(201);
-     *                         responseBuilder.entity(new HalEntityWrapper(savedEntity));
-     *                         responseBuilder.location(URI.create(location));
-     *                         return responseBuilder.build();
-     *                     } else {
-     *                         throw new RuntimeException("Could not extract a new entity URL")
-     *                     }
-     *                 });
-     *             }
-     *         }).onFailure().invoke(t -> throw new RestDataPanacheException(t));
-     *     }
-     * }
-     * 
- */ - @Override - protected void implementInternal(ClassCreator classCreator, ResourceMetadata resourceMetadata, - ResourceProperties resourceProperties, FieldDescriptor resourceField) { - MethodCreator methodCreator = classCreator.getMethodCreator(METHOD_NAME, - isNotReactivePanache() ? Response.class : Uni.class, - resourceMetadata.getIdType(), resourceMetadata.getEntityType()); - - // Add method annotations - addPathAnnotation(methodCreator, - appendToPath(resourceProperties.getPath(RESOURCE_UPDATE_METHOD_NAME), "{id}")); - addPutAnnotation(methodCreator); - addPathParamAnnotation(methodCreator.getParameterAnnotations(0), "id"); - addConsumesAnnotation(methodCreator, APPLICATION_JSON); - addProducesAnnotation(methodCreator, APPLICATION_HAL_JSON); - // Add parameter annotations - if (withValidation) { - methodCreator.getParameterAnnotations(1).addAnnotation(Valid.class); - } - - ResultHandle resource = methodCreator.readInstanceField(resourceField, methodCreator.getThis()); - ResultHandle id = methodCreator.getMethodParam(0); - ResultHandle entityToSave = methodCreator.getMethodParam(1); - - if (isNotReactivePanache()) { - // Invoke resource methods inside a supplier function which will be given to an update executor. - // For ORM, this update executor will have the @Transactional annotation to make - // sure that all database operations are executed in a single transaction. - TryBlock tryBlock = implementTryBlock(methodCreator, EXCEPTION_MESSAGE); - ResultHandle updateExecutor = getUpdateExecutor(tryBlock); - ResultHandle updateFunction = getUpdateFunction(tryBlock, resourceMetadata.getResourceClass(), resource, id, - entityToSave); - ResultHandle newEntity = tryBlock.invokeInterfaceMethod( - ofMethod(UpdateExecutor.class, "execute", Object.class, Supplier.class), - updateExecutor, updateFunction); - - BranchResult createdNewEntity = tryBlock.ifNotNull(newEntity); - ResultHandle wrappedNewEntity = wrapHalEntity(createdNewEntity.trueBranch(), newEntity); - ResultHandle newEntityUrl = ResponseImplementor.getEntityUrl(createdNewEntity.trueBranch(), newEntity); - createdNewEntity.trueBranch().returnValue( - ResponseImplementor.created(createdNewEntity.trueBranch(), wrappedNewEntity, newEntityUrl)); - createdNewEntity.falseBranch().returnValue(ResponseImplementor.noContent(createdNewEntity.falseBranch())); - } else { - ResultHandle uniResponse = methodCreator.invokeVirtualMethod( - ofMethod(resourceMetadata.getResourceClass(), RESOURCE_GET_METHOD_NAME, Uni.class, Object.class), - resource, id); - - methodCreator - .returnValue( - UniImplementor.flatMap(methodCreator, uniResponse, EXCEPTION_MESSAGE, (getBody, itemWasFound) -> { - ResultHandle uniUpdateEntity = getBody.invokeVirtualMethod( - ofMethod(resourceMetadata.getResourceClass(), RESOURCE_UPDATE_METHOD_NAME, Uni.class, - Object.class, - Object.class), - resource, id, entityToSave); - - getBody.returnValue(UniImplementor.map(getBody, uniUpdateEntity, EXCEPTION_MESSAGE, - (updateBody, itemUpdated) -> { - ResultHandle wrappedNewEntity = wrapHalEntity(updateBody, itemUpdated); - ResultHandle newEntityUrl = ResponseImplementor.getEntityUrl(updateBody, - itemUpdated); - - BranchResult ifEntityIsNew = updateBody.ifNull(itemWasFound); - ifEntityIsNew.trueBranch().returnValue(ResponseImplementor - .created(ifEntityIsNew.trueBranch(), wrappedNewEntity, newEntityUrl)); - ifEntityIsNew.falseBranch() - .returnValue(ResponseImplementor.noContent(ifEntityIsNew.falseBranch())); - })); - })); - } - - methodCreator.close(); - } - - @Override - protected String getResourceMethodName() { - return RESOURCE_UPDATE_METHOD_NAME; - } - - private ResultHandle getUpdateFunction(BytecodeCreator creator, String resourceClass, ResultHandle resource, - ResultHandle id, ResultHandle entity) { - FunctionCreator functionCreator = creator.createFunction(Supplier.class); - BytecodeCreator functionBytecodeCreator = functionCreator.getBytecode(); - - AssignableResultHandle entityToSave = functionBytecodeCreator.createVariable(Object.class); - functionBytecodeCreator.assign(entityToSave, entity); - - BranchResult shouldUpdate = entityExists(functionBytecodeCreator, resourceClass, resource, id); - // Update and return null - updateAndReturn(shouldUpdate.trueBranch(), resourceClass, resource, id, entityToSave); - // Update and return new entity - createAndReturn(shouldUpdate.falseBranch(), resourceClass, resource, id, entityToSave); - - return functionCreator.getInstance(); - } - - private BranchResult entityExists(BytecodeCreator creator, String resourceClass, ResultHandle resource, - ResultHandle id) { - return creator.ifNotNull(creator.invokeVirtualMethod( - ofMethod(resourceClass, RESOURCE_GET_METHOD_NAME, Object.class, Object.class), resource, id)); - } - - private void createAndReturn(BytecodeCreator creator, String resourceClass, ResultHandle resource, - ResultHandle id, ResultHandle entityToSave) { - ResultHandle newEntity = creator.invokeVirtualMethod( - ofMethod(resourceClass, RESOURCE_UPDATE_METHOD_NAME, Object.class, Object.class, Object.class), - resource, id, entityToSave); - creator.returnValue(newEntity); - } - - private void updateAndReturn(BytecodeCreator creator, String resourceClass, ResultHandle resource, - ResultHandle id, ResultHandle entityToSave) { - creator.invokeVirtualMethod( - ofMethod(resourceClass, RESOURCE_UPDATE_METHOD_NAME, Object.class, Object.class, Object.class), - resource, id, entityToSave); - creator.returnValue(creator.loadNull()); - } - - private ResultHandle getUpdateExecutor(BytecodeCreator creator) { - ResultHandle arcContainer = creator.invokeStaticMethod(ofMethod(Arc.class, "container", ArcContainer.class)); - ResultHandle instanceHandle = creator.invokeInterfaceMethod( - ofMethod(ArcContainer.class, "instance", InstanceHandle.class, Class.class, Annotation[].class), - arcContainer, creator.loadClassFromTCCL(UpdateExecutor.class), creator.newArray(Annotation.class, 0)); - ResultHandle instance = creator.invokeInterfaceMethod( - ofMethod(InstanceHandle.class, "get", Object.class), instanceHandle); - - creator.ifNull(instance) - .trueBranch() - .throwException(RuntimeException.class, - UpdateExecutor.class.getSimpleName() + " instance was not found"); - - return instance; - } -} diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/utils/ResponseImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/utils/ResponseImplementor.java index 71543ab82934d..ab14843701de0 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/utils/ResponseImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/utils/ResponseImplementor.java @@ -16,17 +16,25 @@ import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; -import io.quarkus.rest.data.panache.runtime.resource.ResourceLinksProvider; +import io.quarkus.hal.HalService; +import io.quarkus.resteasy.links.runtime.hal.ResteasyHalService; +import io.quarkus.resteasy.reactive.links.runtime.hal.ResteasyReactiveHalService; public final class ResponseImplementor { - public static ResultHandle ok(BytecodeCreator creator, ResultHandle entity) { + private final boolean isResteasyClassic; + + public ResponseImplementor(boolean isResteasyClassic) { + this.isResteasyClassic = isResteasyClassic; + } + + public ResultHandle ok(BytecodeCreator creator, ResultHandle entity) { ResultHandle builder = creator.invokeStaticMethod( ofMethod(Response.class, "ok", ResponseBuilder.class, Object.class), entity); return creator.invokeVirtualMethod(ofMethod(ResponseBuilder.class, "build", Response.class), builder); } - public static ResultHandle ok(BytecodeCreator creator, ResultHandle entity, ResultHandle links) { + public ResultHandle ok(BytecodeCreator creator, ResultHandle entity, ResultHandle links) { ResultHandle builder = creator.invokeStaticMethod( ofMethod(Response.class, "ok", ResponseBuilder.class, Object.class), entity); creator.invokeVirtualMethod( @@ -34,11 +42,11 @@ public static ResultHandle ok(BytecodeCreator creator, ResultHandle entity, Resu return creator.invokeVirtualMethod(ofMethod(ResponseBuilder.class, "build", Response.class), builder); } - public static ResultHandle created(BytecodeCreator creator, ResultHandle entity) { + public ResultHandle created(BytecodeCreator creator, ResultHandle entity) { return created(creator, entity, getEntityUrl(creator, entity)); } - public static ResultHandle created(BytecodeCreator creator, ResultHandle entity, ResultHandle location) { + public ResultHandle created(BytecodeCreator creator, ResultHandle entity, ResultHandle location) { ResultHandle builder = getResponseBuilder(creator, Response.Status.CREATED.getStatusCode()); creator.invokeVirtualMethod( ofMethod(ResponseBuilder.class, "entity", ResponseBuilder.class, Object.class), builder, entity); @@ -47,42 +55,44 @@ public static ResultHandle created(BytecodeCreator creator, ResultHandle entity, return creator.invokeVirtualMethod(ofMethod(ResponseBuilder.class, "build", Response.class), builder); } - public static ResultHandle getEntityUrl(BytecodeCreator creator, ResultHandle entity) { + public ResultHandle getEntityUrl(BytecodeCreator creator, ResultHandle entity) { ResultHandle arcContainer = creator .invokeStaticMethod(MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class)); ResultHandle instance = creator.invokeInterfaceMethod( MethodDescriptor.ofMethod(ArcContainer.class, "instance", InstanceHandle.class, Class.class, Annotation[].class), - arcContainer, creator.loadClassFromTCCL(ResourceLinksProvider.class), creator.loadNull()); - ResultHandle linksProvider = creator.invokeInterfaceMethod( + arcContainer, + creator.loadClassFromTCCL(isResteasyClassic ? ResteasyHalService.class : ResteasyReactiveHalService.class), + creator.loadNull()); + ResultHandle halService = creator.invokeInterfaceMethod( MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class), instance); - ResultHandle link = creator.invokeInterfaceMethod( - ofMethod(ResourceLinksProvider.class, "getSelfLink", String.class, Object.class), linksProvider, + ResultHandle link = creator.invokeVirtualMethod( + ofMethod(HalService.class, "getSelfLink", String.class, Object.class), halService, entity); creator.ifNull(link).trueBranch().throwException(RuntimeException.class, "Could not extract a new entity URL"); return creator.invokeStaticMethod(ofMethod(URI.class, "create", URI.class, String.class), link); } - public static ResultHandle noContent(BytecodeCreator creator) { + public ResultHandle noContent(BytecodeCreator creator) { return status(creator, Response.Status.NO_CONTENT.getStatusCode()); } - public static ResultHandle notFound(BytecodeCreator creator) { + public ResultHandle notFound(BytecodeCreator creator) { return status(creator, Response.Status.NOT_FOUND.getStatusCode()); } - public static ResultHandle notFoundException(BytecodeCreator creator) { + public ResultHandle notFoundException(BytecodeCreator creator) { return creator.newInstance(MethodDescriptor.ofConstructor(WebApplicationException.class, int.class), creator.load(Response.Status.NOT_FOUND.getStatusCode())); } - private static ResultHandle status(BytecodeCreator creator, int status) { + private ResultHandle status(BytecodeCreator creator, int status) { ResultHandle builder = getResponseBuilder(creator, status); return creator.invokeVirtualMethod(ofMethod(ResponseBuilder.class, "build", Response.class), builder); } - private static ResultHandle getResponseBuilder(BytecodeCreator creator, int status) { + private ResultHandle getResponseBuilder(BytecodeCreator creator, int status) { return creator.invokeStaticMethod( ofMethod(Response.class, "status", ResponseBuilder.class, int.class), creator.load(status)); } diff --git a/extensions/panache/rest-data-panache/runtime/pom.xml b/extensions/panache/rest-data-panache/runtime/pom.xml index f4a956c2875f2..7a0e30f04692d 100644 --- a/extensions/panache/rest-data-panache/runtime/pom.xml +++ b/extensions/panache/rest-data-panache/runtime/pom.xml @@ -17,6 +17,10 @@ io.quarkus quarkus-panache-common + + io.quarkus + quarkus-hal + org.jboss.spec.javax.ws.rs jboss-jaxrs-api_2.1_spec @@ -82,4 +86,28 @@
+ + + + jakarta-rewrite + + + jakarta-rewrite + + + + + + org.openrewrite.maven + rewrite-maven-plugin + + + io.quarkus.jakarta-jaxrs-switch + + + + + + + diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapper.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapper.java deleted file mode 100644 index 9862d03e99426..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapper.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import javax.ws.rs.core.Link; - -public class HalCollectionWrapper { - - private final Collection collection; - - private final Class elementType; - - private final String collectionName; - - private final Map links = new HashMap<>(); - - public HalCollectionWrapper(Collection collection, Class elementType, String collectionName) { - this.collection = collection; - this.elementType = elementType; - this.collectionName = collectionName; - } - - public Collection getCollection() { - return collection; - } - - public Class getElementType() { - return elementType; - } - - public String getCollectionName() { - return collectionName; - } - - public Map getLinks() { - return links; - } - - public void addLinks(Link... links) { - for (Link link : links) { - this.links.put(link.getRel(), new HalLink(link.getUri().toString())); - } - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapper.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapper.java deleted file mode 100644 index 8edcee713e639..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapper.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -public class HalEntityWrapper { - - private final Object entity; - - public HalEntityWrapper(Object entity) { - this.entity = entity; - } - - public Object getEntity() { - return entity; - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinksProvider.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinksProvider.java deleted file mode 100644 index 6ab614175e68a..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinksProvider.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import java.util.Map; - -public interface HalLinksProvider { - - Map getLinks(Class entityClass); - - Map getLinks(Object entity); -} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/RestEasyHalLinksProvider.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/RestEasyHalLinksProvider.java deleted file mode 100644 index ae87166774682..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/RestEasyHalLinksProvider.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import java.util.HashMap; -import java.util.Map; - -import io.quarkus.arc.Arc; -import io.quarkus.arc.InstanceHandle; -import io.quarkus.rest.data.panache.runtime.resource.ResourceLinksProvider; - -final class RestEasyHalLinksProvider implements HalLinksProvider { - - @Override - public Map getLinks(Class entityClass) { - return toHalLinkMap(restLinksProvider().getClassLinks(entityClass)); - } - - @Override - public Map getLinks(Object entity) { - return toHalLinkMap(restLinksProvider().getInstanceLinks(entity)); - } - - private Map toHalLinkMap(Map links) { - Map halLinks = new HashMap<>(links.size()); - for (Map.Entry entry : links.entrySet()) { - halLinks.put(entry.getKey(), new HalLink(entry.getValue())); - } - return halLinks; - } - - private ResourceLinksProvider restLinksProvider() { - InstanceHandle instance = Arc.container().instance(ResourceLinksProvider.class); - if (instance.isAvailable()) { - return instance.get(); - } - throw new IllegalStateException("No bean of type '" + ResourceLinksProvider.class.getName() + "' found."); - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/RESTEasyClassicResourceLinksProvider.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/RESTEasyClassicResourceLinksProvider.java deleted file mode 100644 index a47204b88c291..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/RESTEasyClassicResourceLinksProvider.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.resource; - -import java.util.HashMap; -import java.util.Map; - -import org.jboss.resteasy.links.LinksProvider; -import org.jboss.resteasy.links.RESTServiceDiscovery; - -public final class RESTEasyClassicResourceLinksProvider implements ResourceLinksProvider { - - private static final String SELF_REF = "self"; - - public Map getClassLinks(Class className) { - RESTServiceDiscovery links = LinksProvider - .getClassLinksProvider() - .getLinks(className, Thread.currentThread().getContextClassLoader()); - return linksToMap(links); - } - - public Map getInstanceLinks(Object instance) { - RESTServiceDiscovery links = LinksProvider - .getObjectLinksProvider() - .getLinks(instance, Thread.currentThread().getContextClassLoader()); - return linksToMap(links); - } - - public String getSelfLink(Object instance) { - RESTServiceDiscovery.AtomLink link = LinksProvider.getObjectLinksProvider() - .getLinks(instance, Thread.currentThread().getContextClassLoader()) - .getLinkForRel(SELF_REF); - return link == null ? null : link.getHref(); - } - - private Map linksToMap(RESTServiceDiscovery serviceDiscovery) { - Map links = new HashMap<>(serviceDiscovery.size()); - for (RESTServiceDiscovery.AtomLink atomLink : serviceDiscovery) { - links.put(atomLink.getRel(), atomLink.getHref()); - } - return links; - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/RESTEasyReactiveResourceLinksProvider.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/RESTEasyReactiveResourceLinksProvider.java deleted file mode 100644 index 0186bf1fb1e0f..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/RESTEasyReactiveResourceLinksProvider.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.resource; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import javax.ws.rs.core.Link; - -import io.quarkus.arc.Arc; -import io.quarkus.arc.InstanceHandle; -import io.quarkus.resteasy.reactive.links.RestLinksProvider; - -public class RESTEasyReactiveResourceLinksProvider implements ResourceLinksProvider { - - private static final String SELF_REF = "self"; - - public Map getClassLinks(Class className) { - return linksToMap(restLinksProvider().getTypeLinks(className)); - } - - public Map getInstanceLinks(Object instance) { - return linksToMap(restLinksProvider().getInstanceLinks(instance)); - } - - public String getSelfLink(Object instance) { - Collection links = restLinksProvider().getInstanceLinks(instance); - for (Link link : links) { - if (SELF_REF.equals(link.getRel())) { - return link.getUri().toString(); - } - } - return null; - } - - private RestLinksProvider restLinksProvider() { - InstanceHandle instance = Arc.container().instance(RestLinksProvider.class); - if (instance.isAvailable()) { - return instance.get(); - } - throw new IllegalStateException("Invalid use of '" + this.getClass().getName() - + "'. No request scope bean found for type '" + RESTEasyReactiveResourceLinksProvider.class.getName() + "'"); - } - - private Map linksToMap(Collection links) { - Map result = new HashMap<>(); - for (Link link : links) { - result.put(link.getRel(), link.getUri().toString()); - } - return result; - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/ResourceLinksProvider.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/ResourceLinksProvider.java deleted file mode 100644 index 8683c24cfef78..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/ResourceLinksProvider.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.resource; - -import java.util.Map; - -public interface ResourceLinksProvider { - - Map getClassLinks(Class className); - - Map getInstanceLinks(Object instance); - - @SuppressWarnings("unused") - String getSelfLink(Object instance); -} diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/AbstractSerializersTest.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/AbstractSerializersTest.java deleted file mode 100644 index 88ae8efea84d2..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/AbstractSerializersTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.io.StringReader; -import java.util.Collections; - -import javax.json.Json; -import javax.json.JsonObject; -import javax.json.JsonReader; - -import org.junit.jupiter.api.Test; - -abstract class AbstractSerializersTest { - - abstract String toJson(Object object); - - @Test - void shouldSerializeOneBook() { - int id = 1; - String title = "Black Swan"; - Book book = usePublishedBook() ? new PublishedBook(id, title) : new Book(id, title); - JsonReader jsonReader = Json.createReader(new StringReader(toJson(new HalEntityWrapper(book)))); - - assertBook(book, jsonReader.readObject()); - } - - @Test - void shouldSerializeOneBookWithNullName() { - int id = 1; - String title = null; - Book book = usePublishedBook() ? new PublishedBook(id, title) : new Book(id, title); - JsonReader jsonReader = Json.createReader(new StringReader(toJson(new HalEntityWrapper(book)))); - - assertBook(book, jsonReader.readObject()); - } - - @Test - void shouldSerializeCollectionOfBooks() { - int id = 1; - String title = "Black Swan"; - Book book = usePublishedBook() ? new PublishedBook(id, title) : new Book(id, title); - HalCollectionWrapper wrapper = new HalCollectionWrapper(Collections.singleton(book), Book.class, "books"); - JsonReader jsonReader = Json.createReader(new StringReader(toJson(wrapper))); - JsonObject collectionJson = jsonReader.readObject(); - - assertBook(book, collectionJson.getJsonObject("_embedded").getJsonArray("books").getJsonObject(0)); - - JsonObject collectionLinksJson = collectionJson.getJsonObject("_links"); - assertThat(collectionLinksJson.getJsonObject("list").getString("href")).isEqualTo("/books"); - assertThat(collectionLinksJson.getJsonObject("add").getString("href")).isEqualTo("/books"); - } - - private void assertBook(Book book, JsonObject bookJson) { - assertThat(bookJson.getInt("id")).isEqualTo(book.id); - if (bookJson.isNull("book-name")) { - assertThat(book.getName()).isNull(); - } else { - assertThat(bookJson.getString("book-name")).isEqualTo(book.getName()); - } - assertThat(bookJson.containsKey("ignored")).isFalse(); - - JsonObject bookLinksJson = bookJson.getJsonObject("_links"); - assertThat(bookLinksJson.getJsonObject("self").getString("href")).isEqualTo("/books/" + book.id); - assertThat(bookLinksJson.getJsonObject("update").getString("href")).isEqualTo("/books/" + book.id); - assertThat(bookLinksJson.getJsonObject("list").getString("href")).isEqualTo("/books"); - assertThat(bookLinksJson.getJsonObject("add").getString("href")).isEqualTo("/books"); - } - - protected abstract boolean usePublishedBook(); -} diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/Book.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/Book.java deleted file mode 100644 index fbf8190b2fd76..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/Book.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import javax.json.bind.annotation.JsonbProperty; -import javax.json.bind.annotation.JsonbTransient; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class Book { - - public final long id; - - @JsonProperty("book-name") - @JsonbProperty("book-name") - private final String name; - - @JsonIgnore - @JsonbTransient - public final String ignored = "ignore me"; - - public Book(long id, String name) { - this.id = id; - this.name = name; - } - - public String getName() { - return name; - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/BookHalLinksProvider.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/BookHalLinksProvider.java deleted file mode 100644 index d6a0c9d613d8b..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/BookHalLinksProvider.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import java.util.HashMap; -import java.util.Map; - -public class BookHalLinksProvider implements HalLinksProvider { - - @Override - public Map getLinks(Class entityClass) { - Map links = new HashMap<>(2); - links.put("list", new HalLink("/books")); - links.put("add", new HalLink("/books")); - - return links; - } - - @Override - public Map getLinks(Object entity) { - Book book = (Book) entity; - Map links = new HashMap<>(4); - links.put("list", new HalLink("/books")); - links.put("add", new HalLink("/books")); - links.put("self", new HalLink("/books/" + book.id)); - links.put("update", new HalLink("/books/" + book.id)); - - return links; - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JacksonSerializersTest.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JacksonSerializersTest.java deleted file mode 100644 index 2b444463d25a5..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JacksonSerializersTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import org.junit.jupiter.api.BeforeEach; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - -class JacksonSerializersTest extends AbstractSerializersTest { - - private ObjectMapper objectMapper; - - @BeforeEach - void setup() { - objectMapper = new ObjectMapper(); - SimpleModule module = new SimpleModule(); - module.addSerializer(HalEntityWrapper.class, new HalEntityWrapperJacksonSerializer(new BookHalLinksProvider())); - module.addSerializer(HalCollectionWrapper.class, - new HalCollectionWrapperJacksonSerializer(new BookHalLinksProvider())); - objectMapper.registerModule(module); - objectMapper.registerModule(new JavaTimeModule()); - } - - @Override - String toJson(Object object) { - try { - return objectMapper.writeValueAsString(object); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - @Override - protected boolean usePublishedBook() { - return true; - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JsonbSerializersTest.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JsonbSerializersTest.java deleted file mode 100644 index 5884f3b5894c7..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JsonbSerializersTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import javax.json.bind.Jsonb; -import javax.json.bind.JsonbBuilder; -import javax.json.bind.JsonbConfig; - -import org.junit.jupiter.api.BeforeEach; - -class JsonbSerializersTest extends AbstractSerializersTest { - - private Jsonb jsonb; - - @BeforeEach - void setup() { - JsonbConfig config = new JsonbConfig(); - config.withSerializers(new HalEntityWrapperJsonbSerializer(new BookHalLinksProvider())); - config.withSerializers(new HalCollectionWrapperJsonbSerializer(new BookHalLinksProvider())); - jsonb = JsonbBuilder.create(config); - } - - @Override - String toJson(Object object) { - return jsonb.toJson(object); - } - - @Override - protected boolean usePublishedBook() { - return false; - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/PublishedBook.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/PublishedBook.java deleted file mode 100644 index 38ff69b5463d1..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/PublishedBook.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import java.time.LocalDate; -import java.time.Month; - -public class PublishedBook extends Book { - - public LocalDate publicationDate = LocalDate.of(2021, Month.AUGUST, 31); - - public PublishedBook(long id, String name) { - super(id, name); - } -} diff --git a/extensions/pom.xml b/extensions/pom.xml index 5d4e1047343d4..6618b4c5aaf11 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -31,6 +31,7 @@ jaxb jsonp jsonb + hal vertx-http @@ -101,7 +102,7 @@ kafka-streams mongodb-client avro - apicurio-registry-avro + schema-registry devservices diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/OverdueCronExecutionTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/OverdueCronExecutionTest.java new file mode 100644 index 0000000000000..d06f384f4a6b9 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/OverdueCronExecutionTest.java @@ -0,0 +1,74 @@ +package io.quarkus.quartz.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +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.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.Trigger; +import io.quarkus.test.QuarkusUnitTest; + +public class OverdueCronExecutionTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> root.addClasses(Jobs.class) + .addAsResource(new StringAsset("quarkus.scheduler.overdue-grace-period=2H\njob.gracePeriod=2H"), + "application.properties")); + + @Inject + Scheduler scheduler; + + @Test + public void testExecution() { + try { + Trigger overdueJob = scheduler.getScheduledJob("overdueJob"); + Trigger tolerantJob = scheduler.getScheduledJob("tolerantJob"); + Trigger gracePeriodFromConfigJob = scheduler.getScheduledJob("gracePeriodFromConfigJob"); + Trigger defaultGracePeriodJob = scheduler.getScheduledJob("defaultGracePeriodJob"); + assertTrue(Jobs.LATCH.await(5, TimeUnit.SECONDS)); + scheduler.pause(); + Thread.sleep(1250); + assertTrue(overdueJob.isOverdue()); + assertFalse(tolerantJob.isOverdue()); + assertFalse(gracePeriodFromConfigJob.isOverdue()); + assertFalse(defaultGracePeriodJob.isOverdue()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + } + + static class Jobs { + + static final String CRON = "0/1 * * * * ?"; + + static final CountDownLatch LATCH = new CountDownLatch(1); + + @Scheduled(identity = "overdueJob", cron = CRON, overdueGracePeriod = "0.1s") + void overdueJob() { + LATCH.countDown(); + } + + @Scheduled(identity = "tolerantJob", cron = CRON, overdueGracePeriod = "2H") + void tolerantJob() { + } + + @Scheduled(identity = "gracePeriodFromConfigJob", cron = CRON, overdueGracePeriod = "{job.gracePeriod}") + void gracePeriodFromConfigJob() { + } + + @Scheduled(identity = "defaultGracePeriodJob", cron = CRON) + void defaultGracePeriodJob() { + } + } +} diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java index d04ae8ab00a34..8b31854cd1ab5 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java @@ -73,7 +73,9 @@ import io.quarkus.scheduler.common.runtime.StatusEmitterInvoker; import io.quarkus.scheduler.common.runtime.util.SchedulerUtils; import io.quarkus.scheduler.runtime.SchedulerRuntimeConfig; +import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle; import io.smallrye.common.vertx.VertxContext; +import io.vertx.core.Context; import io.vertx.core.Handler; import io.vertx.core.Vertx; @@ -164,6 +166,7 @@ public QuartzScheduler(SchedulerContext context, QuartzSupport quartzSupport, Sc String cron = SchedulerUtils.lookUpPropertyValue(scheduled.cron()); if (!cron.isEmpty()) { if (SchedulerUtils.isOff(cron)) { + this.pause(identity); continue; } if (!CronType.QUARTZ.equals(cronType)) { @@ -212,6 +215,7 @@ public QuartzScheduler(SchedulerContext context, QuartzSupport quartzSupport, Sc } else if (!scheduled.every().isEmpty()) { OptionalLong everyMillis = SchedulerUtils.parseEveryAsMillis(scheduled); if (!everyMillis.isPresent()) { + this.pause(identity); continue; } SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule() @@ -275,8 +279,8 @@ public QuartzScheduler(SchedulerContext context, QuartzSupport quartzSupport, Sc org.quartz.Trigger trigger = triggerBuilder.build(); org.quartz.Trigger oldTrigger = scheduler.getTrigger(trigger.getKey()); if (oldTrigger != null) { - scheduler.rescheduleJob(trigger.getKey(), - triggerBuilder.startAt(oldTrigger.getNextFireTime()).build()); + trigger = triggerBuilder.startAt(oldTrigger.getNextFireTime()).build(); + scheduler.rescheduleJob(trigger.getKey(), trigger); LOGGER.debugf("Rescheduled business method %s with config %s", method.getMethodDescription(), scheduled); } else if (!scheduler.checkExists(jobDetail.getKey())) { @@ -290,7 +294,8 @@ public QuartzScheduler(SchedulerContext context, QuartzSupport quartzSupport, Sc oldTrigger = scheduler.getTrigger(new TriggerKey(identity + "_trigger", Scheduler.class.getName())); if (oldTrigger != null) { scheduler.deleteJob(jobDetail.getKey()); - scheduler.scheduleJob(jobDetail, triggerBuilder.startAt(oldTrigger.getNextFireTime()).build()); + trigger = triggerBuilder.startAt(oldTrigger.getNextFireTime()).build(); + scheduler.scheduleJob(jobDetail, trigger); LOGGER.debugf( "Rescheduled business method %s with config %s due to Trigger '%s' record being renamed after removal of '_trigger' suffix", method.getMethodDescription(), @@ -555,26 +560,33 @@ static class InvokerJob implements Job { } @Override - public void execute(JobExecutionContext context) throws JobExecutionException { - if (trigger.invoker != null) { // could be null from previous runs + public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { + if (trigger != null && trigger.invoker != null) { // could be null from previous runs if (trigger.invoker.isBlocking()) { try { - trigger.invoker.invoke(new QuartzScheduledExecution(trigger, context)); + trigger.invoker.invoke(new QuartzScheduledExecution(trigger, jobExecutionContext)); } catch (Exception e) { throw new JobExecutionException(e); } } else { - VertxContext.getOrCreateDuplicatedContext(vertx).runOnContext(new Handler() { + Context context = VertxContext.getOrCreateDuplicatedContext(vertx); + VertxContextSafetyToggle.setContextSafe(context, true); + context.runOnContext(new Handler() { @Override public void handle(Void event) { try { - trigger.invoker.invoke(new QuartzScheduledExecution(trigger, context)); + trigger.invoker.invoke(new QuartzScheduledExecution(trigger, jobExecutionContext)); } catch (Exception e) { // already logged by the StatusEmitterInvoker } } }); } + } else { + String jobName = jobExecutionContext.getJobDetail().getKey().getName(); + LOGGER.warnf("Unable to find corresponding Quartz trigger for job %s. " + + "Update your Quartz table by removing all phantom jobs or make sure that there is a " + + "Scheduled method with the identity matching the job's name", jobName); } } } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleBuildItem.java index 786e723a9aa6a..805c2d2677aca 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleBuildItem.java @@ -13,13 +13,18 @@ public final class MessageBundleBuildItem extends MultiBuildItem { private final ClassInfo defaultBundleInterface; private final Map localizedInterfaces; private final Map localizedFiles; + private final Map mergeCandidates; + private final String defaultLocale; - public MessageBundleBuildItem(String name, ClassInfo defaultBundleInterface, Map localizedInterfaces, - Map localizedFiles) { + public MessageBundleBuildItem(String name, ClassInfo defaultBundleInterface, + Map localizedInterfaces, Map localizedFiles, + Map mergeCandidates, String defaultLocale) { this.name = name; this.defaultBundleInterface = defaultBundleInterface; this.localizedInterfaces = localizedInterfaces; this.localizedFiles = localizedFiles; + this.mergeCandidates = mergeCandidates; + this.defaultLocale = defaultLocale; } public String getName() { @@ -38,4 +43,17 @@ public Map getLocalizedFiles() { return localizedFiles; } + /** + * Merge candidates are localized files used as a supplementary source of message templates + * not specified by localized interfaces. + * + * @return locale -> localized file {@link Path} + */ + public Map getMergeCandidates() { + return mergeCandidates; + } + + public String getDefaultLocale() { + return defaultLocale; + } } 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 b5e6f2f687f2b..21a68167cce90 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 @@ -86,6 +86,7 @@ import io.quarkus.qute.deployment.QuteProcessor.LookupConfig; import io.quarkus.qute.deployment.QuteProcessor.Match; import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; +import io.quarkus.qute.deployment.Types.AssignableInfo; import io.quarkus.qute.generator.Descriptors; import io.quarkus.qute.generator.ValueResolverGenerator; import io.quarkus.qute.i18n.Localized; @@ -176,29 +177,31 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv // Find localized files Map localeToFile = new HashMap<>(); + // Message templates not specified by a localized interface are looked up in a localized file (merge candidate) + Map localeToMergeCandidate = new HashMap<>(); for (Path messageFile : messageFiles) { String fileName = messageFile.getFileName().toString(); if (fileName.startsWith(name)) { // msg_en.txt -> en String locale = fileName.substring(fileName.indexOf('_') + 1, fileName.indexOf('.')); - if (defaultLocale.equals(locale)) { - throw new MessageBundleException( - String.format( - "Locale of [%s] conflicts with the locale [%s] of the default message bundle [%s]", - fileName, locale, bundleClass)); - } ClassInfo localizedInterface = localeToInterface.get(locale); - if (localizedInterface != null) { - throw new MessageBundleException( - String.format( - "Cannot register [%s] - a localized message bundle interface exists for locale [%s]: %s", - fileName, locale, localizedInterface)); + if (defaultLocale.equals(locale) || localizedInterface != null) { + // both file and interface exist for one locale, therefore we need to merge them + Path previous = localeToMergeCandidate.put(locale, messageFile); + if (previous != null) { + throw new MessageBundleException( + String.format( + "Cannot register [%s] - a localized file already exists for locale [%s]: [%s]", + fileName, locale, previous.getFileName().toString())); + } + } else { + localeToFile.put(locale, messageFile); } - localeToFile.put(locale, messageFile); } } - bundles.add(new MessageBundleBuildItem(name, bundleClass, localeToInterface, localeToFile)); + bundles.add(new MessageBundleBuildItem(name, bundleClass, localeToInterface, + localeToFile, localeToMergeCandidate, defaultLocale)); } else { throw new MessageBundleException("@MessageBundle must be declared on an interface: " + bundleClass); } @@ -421,6 +424,7 @@ public String apply(String id) { .filter(Predicate.not(TemplateExtensionMethodBuildItem::hasNamespace)).collect(Collectors.toUnmodifiableList()); LookupConfig lookupConfig = new QuteProcessor.FixedLookupConfig(index, QuteProcessor.initDefaultMembersFilter(), false); + Map assignableCache = new HashMap<>(); // bundle name -> (key -> method) Map> bundleMethodsMap = new HashMap<>(); @@ -525,10 +529,10 @@ public String apply(String id) { results, excludes, incorrectExpressions, expression, index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, - regularExtensionMethods, namespaceExtensionMethods); + regularExtensionMethods, namespaceExtensionMethods, assignableCache); Match match = results.get(param.toOriginalString()); if (match != null && !match.isEmpty() && !Types.isAssignableFrom(match.type(), - methodParams.get(idx), index)) { + methodParams.get(idx), index, assignableCache)) { incorrectExpressions .produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), "Message bundle method " + method.declaringClass().name() + "#" + @@ -587,8 +591,8 @@ void generateExamplePropertiesFiles(List messageBu for (Entry> entry : bundles.entrySet()) { List messages = entry.getValue(); messages.sort(Comparator.comparing(MessageBundleMethodBuildItem::getKey)); - Path exampleProperfies = generatedExamplesDir.resolve(entry.getKey() + ".properties"); - Files.write(exampleProperfies, + Path exampleProperties = generatedExamplesDir.resolve(entry.getKey() + ".properties"); + Files.write(exampleProperties, messages.stream().map(m -> m.getMethod().name() + "=" + m.getTemplate()).collect(Collectors.toList())); } } @@ -603,46 +607,32 @@ private Map generateImplementations(List for (MessageBundleBuildItem bundle : bundles) { ClassInfo bundleInterface = bundle.getDefaultBundleInterface(); - String bundleImpl = generateImplementation(null, null, bundleInterface, defaultClassOutput, messageTemplateMethods, - Collections.emptyMap(), null); + + // take message templates not specified by Message#value from corresponding localized file + Map defaultKeyToMap = getLocalizedFileKeyToTemplate(bundle, bundleInterface, + bundle.getDefaultLocale(), bundleInterface.methods(), null); + MergeClassInfoWrapper bundleInterfaceWrapper = new MergeClassInfoWrapper(bundleInterface, null, null); + + String bundleImpl = generateImplementation(null, null, bundleInterfaceWrapper, + defaultClassOutput, messageTemplateMethods, defaultKeyToMap, null); generatedTypes.put(bundleInterface.name().toString(), bundleImpl); - for (ClassInfo localizedInterface : bundle.getLocalizedInterfaces().values()) { - generatedTypes.put(localizedInterface.name().toString(), - generateImplementation(bundle.getDefaultBundleInterface(), bundleImpl, localizedInterface, - defaultClassOutput, - messageTemplateMethods, Collections.emptyMap(), null)); + for (Entry entry : bundle.getLocalizedInterfaces().entrySet()) { + ClassInfo localizedInterface = entry.getValue(); + + // take message templates not specified by Message#value from corresponding localized file + Map keyToMap = getLocalizedFileKeyToTemplate(bundle, bundleInterface, entry.getKey(), + localizedInterface.methods(), localizedInterface); + MergeClassInfoWrapper localizedInterfaceWrapper = new MergeClassInfoWrapper(localizedInterface, bundleInterface, + keyToMap); + + generatedTypes.put(entry.getValue().name().toString(), + generateImplementation(bundleInterface, bundleImpl, localizedInterfaceWrapper, + defaultClassOutput, messageTemplateMethods, keyToMap, null)); } for (Entry entry : bundle.getLocalizedFiles().entrySet()) { Path localizedFile = entry.getValue(); - Map keyToTemplate = new HashMap<>(); - for (ListIterator it = Files.readAllLines(localizedFile).listIterator(); it.hasNext();) { - String line = it.next(); - if (line.startsWith("#") || line.isBlank()) { - // Comments and blank lines are skipped - continue; - } - int eqIdx = line.indexOf('='); - if (eqIdx == -1) { - throw new MessageBundleException( - "Missing key/value separator\n\t- file: " + localizedFile + "\n\t- line " + it.previousIndex()); - } - String key = line.substring(0, eqIdx).strip(); - if (!hasMessageBundleMethod(bundleInterface, key)) { - throw new MessageBundleException( - "Message bundle method " + key + "() not found on: " + bundleInterface + "\n\t- file: " - + localizedFile + "\n\t- line " + it.previousIndex()); - } - String value = adaptLine(line.substring(eqIdx + 1, line.length())); - if (value.endsWith("\\")) { - // The logical line is spread out across several normal lines - StringBuilder builder = new StringBuilder(value.substring(0, value.length() - 1)); - constructLine(builder, it); - keyToTemplate.put(key, builder.toString()); - } else { - keyToTemplate.put(key, value); - } - } + var keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile); String locale = entry.getKey(); ClassOutput localeAwareGizmoAdaptor = new GeneratedClassGizmoAdaptor(generatedClasses, @@ -657,14 +647,81 @@ public String apply(String className) { } })); generatedTypes.put(localizedFile.toString(), - generateImplementation(bundle.getDefaultBundleInterface(), bundleImpl, bundleInterface, - localeAwareGizmoAdaptor, - messageTemplateMethods, keyToTemplate, locale)); + generateImplementation(bundleInterface, bundleImpl, new SimpleClassInfoWrapper(bundleInterface), + localeAwareGizmoAdaptor, messageTemplateMethods, keyToTemplate, locale)); } } return generatedTypes; } + private Map getLocalizedFileKeyToTemplate(MessageBundleBuildItem bundle, + ClassInfo bundleInterface, String locale, List methods, ClassInfo localizedInterface) + throws IOException { + + var localizedFile = bundle.getMergeCandidates().get(locale); + if (localizedFile != null) { + Map keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile); + if (!keyToTemplate.isEmpty()) { + + // keep message templates if value wasn't provided by Message#value + methods + .stream() + .filter(method -> keyToTemplate.containsKey(method.name())) + .filter(method -> { + AnnotationInstance messageAnnotation; + if (localizedInterface != null) { + MethodInfo defaultBundleMethod = localizedInterface.method(method.name(), + method.parameters().toArray(new Type[] {})); + if (defaultBundleMethod == null) { + return true; + } + messageAnnotation = defaultBundleMethod.annotation(Names.MESSAGE); + } else { + messageAnnotation = method.annotation(Names.MESSAGE); + } + return getMessageAnnotationValue(messageAnnotation) != null; + }) + .map(MethodInfo::name) + .forEach(keyToTemplate::remove); + return keyToTemplate; + } + } + return Collections.emptyMap(); + } + + private Map parseKeyToTemplateFromLocalizedFile(ClassInfo bundleInterface, + Path localizedFile) throws IOException { + Map keyToTemplate = new HashMap<>(); + for (ListIterator it = Files.readAllLines(localizedFile).listIterator(); it.hasNext();) { + String line = it.next(); + if (line.startsWith("#") || line.isBlank()) { + // Comments and blank lines are skipped + continue; + } + int eqIdx = line.indexOf('='); + if (eqIdx == -1) { + throw new MessageBundleException( + "Missing key/value separator\n\t- file: " + localizedFile + "\n\t- line " + it.previousIndex()); + } + String key = line.substring(0, eqIdx).strip(); + if (!hasMessageBundleMethod(bundleInterface, key)) { + throw new MessageBundleException( + "Message bundle method " + key + "() not found on: " + bundleInterface + "\n\t- file: " + + localizedFile + "\n\t- line " + it.previousIndex()); + } + String value = adaptLine(line.substring(eqIdx + 1, line.length())); + if (value.endsWith("\\")) { + // The logical line is spread out across several normal lines + StringBuilder builder = new StringBuilder(value.substring(0, value.length() - 1)); + constructLine(builder, it); + keyToTemplate.put(key, builder.toString()); + } else { + keyToTemplate.put(key, value); + } + } + return keyToTemplate; + } + private void constructLine(StringBuilder builder, Iterator it) { if (it.hasNext()) { String nextLine = adaptLine(it.next()); @@ -690,10 +747,12 @@ private boolean hasMessageBundleMethod(ClassInfo bundleInterface, String name) { return false; } - private String generateImplementation(ClassInfo defaultBundleInterface, String defaultBundleImpl, ClassInfo bundleInterface, - ClassOutput classOutput, BuildProducer messageTemplateMethods, + private String generateImplementation(ClassInfo defaultBundleInterface, String defaultBundleImpl, + ClassInfoWrapper bundleInterfaceWrapper, ClassOutput classOutput, + BuildProducer messageTemplateMethods, Map messageTemplates, String locale) { + ClassInfo bundleInterface = bundleInterfaceWrapper.getClassInfo(); LOGGER.debugf("Generate bundle implementation for %s", bundleInterface); AnnotationInstance bundleAnnotation = defaultBundleInterface != null ? defaultBundleInterface.classAnnotation(Names.BUNDLE) @@ -726,7 +785,7 @@ private String generateImplementation(ClassInfo defaultBundleInterface, String d // key -> method Map keyMap = new LinkedHashMap<>(); - List methods = new ArrayList<>(bundleInterface.methods()); + List methods = new ArrayList<>(bundleInterfaceWrapper.methods()); // Sort methods methods.sort(Comparator.comparing(MethodInfo::name).thenComparing(Comparator.comparing(MethodInfo::toString))); @@ -742,7 +801,7 @@ private String generateImplementation(ClassInfo defaultBundleInterface, String d AnnotationInstance messageAnnotation; if (defaultBundleInterface != null) { - MethodInfo defaultBundleMethod = bundleInterface.method(method.name(), + MethodInfo defaultBundleMethod = bundleInterfaceWrapper.method(method.name(), method.parameters().toArray(new Type[] {})); if (defaultBundleMethod == null) { throw new MessageBundleException( @@ -772,7 +831,18 @@ private String generateImplementation(ClassInfo defaultBundleInterface, String d String messageTemplate = messageTemplates.get(method.name()); if (messageTemplate == null) { - messageTemplate = messageAnnotation.value().asString(); + messageTemplate = getMessageAnnotationValue(messageAnnotation); + } + + if (messageTemplate == null && defaultBundleInterface != null) { + // method is annotated with @Message without value() -> fallback to default locale + messageTemplate = getMessageAnnotationValue((defaultBundleInterface.method(method.name(), + method.parameters().toArray(new Type[] {}))).annotation(Names.MESSAGE)); + } + + if (messageTemplate == null) { + throw new MessageBundleException( + String.format("Message template for key [%s] is missing for default locale", key)); } String templateId = null; @@ -831,6 +901,18 @@ private String generateImplementation(ClassInfo defaultBundleInterface, String d return generatedName.replace('/', '.'); } + /** + * @return {@link Message#value()} if value was provided + */ + private String getMessageAnnotationValue(AnnotationInstance messageAnnotation) { + var messageValue = messageAnnotation.value(); + if (messageValue == null || messageValue.asString().equals(Message.DEFAULT_VALUE)) { + // no value was provided in annotation + return null; + } + return messageValue.asString(); + } + static String getParameterName(MethodInfo method, int position) { String name = method.parameterName(position); AnnotationInstance paramAnnotation = Annotations @@ -1152,4 +1234,90 @@ public boolean test(String name) { return GeneratedClassGizmoAdaptor.isApplicationClass(className); } } + + private interface ClassInfoWrapper { + + ClassInfo getClassInfo(); + + List methods(); + + MethodInfo method(String name, Type... parameters); + } + + private static class SimpleClassInfoWrapper implements ClassInfoWrapper { + + private final ClassInfo classInfo; + + SimpleClassInfoWrapper(ClassInfo classInfo) { + this.classInfo = classInfo; + } + + @Override + public ClassInfo getClassInfo() { + return classInfo; + } + + @Override + public final List methods() { + return classInfo.methods(); + } + + @Override + public final MethodInfo method(String name, Type... parameters) { + return classInfo.method(name, parameters); + } + + } + + private static class MergeClassInfoWrapper implements ClassInfoWrapper { + + private final ClassInfo classInfo; + + private final ClassInfo interfaceClassInfo; + + private final Map interfaceKeyToMethodInfo; + + MergeClassInfoWrapper(ClassInfo classInfo, ClassInfo interfaceClassInfo, + Map localizedFileKeyToTemplate) { + this.classInfo = classInfo; + this.interfaceClassInfo = interfaceClassInfo; + + // take methods missing in class info so each message template provided in file has its method + if (interfaceClassInfo != null && localizedFileKeyToTemplate != null) { + List classInfoMethods = classInfo.methods(); + interfaceKeyToMethodInfo = interfaceClassInfo + .methods() + .stream() + // keep method with message template in localized file + .filter(method -> localizedFileKeyToTemplate.containsKey(method.name())) + // if method is overridden, prefer implementation + .filter(method -> classInfoMethods.stream() + .noneMatch(m -> m.name().equals(method.name()))) + .collect(toMap(MethodInfo::name, Function.identity())); + } else { + interfaceKeyToMethodInfo = Collections.emptyMap(); + } + } + + @Override + public ClassInfo getClassInfo() { + return classInfo; + } + + @Override + public final List methods() { + return Stream.concat( + interfaceKeyToMethodInfo.values().stream(), + classInfo.methods().stream()).collect(Collectors.toCollection(ArrayList::new)); + } + + @Override + public final MethodInfo method(String name, Type... parameters) { + + if (interfaceKeyToMethodInfo.containsKey(name)) { + return interfaceClassInfo.method(name, parameters); + } + return classInfo.method(name, parameters); + } + } } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index 2459c7ab7c289..10368db18c7a5 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -90,9 +90,11 @@ import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.Engine; import io.quarkus.qute.EngineBuilder; +import io.quarkus.qute.ErrorCode; import io.quarkus.qute.Expression; import io.quarkus.qute.Expression.VirtualMethodPart; import io.quarkus.qute.LoopSectionHelper; +import io.quarkus.qute.ParameterDeclaration; import io.quarkus.qute.ParserHelper; import io.quarkus.qute.ParserHook; import io.quarkus.qute.ResultNode; @@ -112,6 +114,7 @@ import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; import io.quarkus.qute.deployment.TypeCheckExcludeBuildItem.TypeCheck; import io.quarkus.qute.deployment.TypeInfos.Info; +import io.quarkus.qute.deployment.Types.AssignableInfo; import io.quarkus.qute.generator.ExtensionMethodGenerator; import io.quarkus.qute.generator.ExtensionMethodGenerator.NamespaceResolverCreator; import io.quarkus.qute.generator.ExtensionMethodGenerator.NamespaceResolverCreator.ResolveCreator; @@ -171,23 +174,33 @@ void processTemplateErrors(TemplatesAnalysisBuildItem analysis, List set of used members Map> implicitClassToMembersUsed = new HashMap<>(); - Map namespaceTemplateData = templateData.stream() - .filter(TemplateDataBuildItem::hasNamespace) - .collect(Collectors.toMap(TemplateDataBuildItem::getNamespace, Function.identity())); + Map namespaceTemplateData = new HashMap<>(); + for (TemplateDataBuildItem td : templateData) { + if (td.hasNamespace()) { + namespaceTemplateData.put(td.getNamespace(), td); + } + } Map> namespaceExtensionMethods = templateExtensionMethods.stream() .filter(TemplateExtensionMethodBuildItem::hasNamespace) .sorted(Comparator.comparingInt(TemplateExtensionMethodBuildItem::getPriority).reversed()) .collect(Collectors.groupingBy(TemplateExtensionMethodBuildItem::getNamespace)); - List regularExtensionMethods = templateExtensionMethods.stream() - .filter(Predicate.not(TemplateExtensionMethodBuildItem::hasNamespace)).collect(Collectors.toUnmodifiableList()); + List regularExtensionMethods = new ArrayList<>(); + for (TemplateExtensionMethodBuildItem extensionMethod : templateExtensionMethods) { + if (!extensionMethod.hasNamespace()) { + regularExtensionMethods.add(extensionMethod); + } + } LookupConfig lookupConfig = new FixedLookupConfig(index, initDefaultMembersFilter(), false); + Map assignableCache = new HashMap<>(); + int expressionsValidated = 0; for (TemplateAnalysis templateAnalysis : templatesAnalysis.getAnalysis()) { // The relevant checked template, may be null @@ -540,16 +563,55 @@ public String apply(String id) { continue; } Match match = validateNestedExpressions(config, templateAnalysis, null, new HashMap<>(), excludes, - incorrectExpressions, - expression, index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, + incorrectExpressions, expression, index, implicitClassToMembersUsed, templateIdToPathFun, + generatedIdsToMatches, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, regularExtensionMethods, - namespaceExtensionMethods); + namespaceExtensionMethods, assignableCache); generatedIdsToMatches.put(expression.getGeneratedId(), match); } + + // Validate default values of parameter declarations + for (ParameterDeclaration parameterDeclaration : templateAnalysis.parameterDeclarations) { + Expression defaultValue = parameterDeclaration.getDefaultValue(); + if (defaultValue != null) { + Match match; + if (defaultValue.isLiteral()) { + match = new Match(index, assignableCache); + setMatchValues(match, defaultValue, generatedIdsToMatches, index); + } else { + match = generatedIdsToMatches.get(defaultValue.getGeneratedId()); + if (match == null) { + LOGGER.debugf( + "No type info available - unable to validate the default value of a parameter declaration [" + + parameterDeclaration.getKey() + "] in " + defaultValue.getOrigin()); + continue; + } + } + Info info = TypeInfos.create(parameterDeclaration.getTypeInfo(), null, index, templateIdToPathFun, + parameterDeclaration.getDefaultValue().getOrigin()); + if (!info.isTypeInfo()) { + throw new IllegalStateException("Invalid type info [" + info + "] of parameter declaration [" + + parameterDeclaration.getKey() + "] in " + + defaultValue.getOrigin().toString()); + } + if (!Types.isAssignableFrom(info.asTypeInfo().resolvedType, match.type(), index, assignableCache)) { + incorrectExpressions.produce(new IncorrectExpressionBuildItem(defaultValue.toOriginalString(), + "The type of the default value [" + match.type() + + "] does not match the type of the parameter declaration [" + + info.asTypeInfo().resolvedType + "]", + defaultValue.getOrigin())); + } + } + } + expressionMatches .produce(new TemplateExpressionMatchesBuildItem(templateAnalysis.generatedId, generatedIdsToMatches)); + + expressionsValidated += generatedIdsToMatches.size(); } + LOGGER.debugf("Validated %s expressions", expressionsValidated); + // Register an implicit value resolver for the classes collected during validation for (Entry> entry : implicitClassToMembersUsed.entrySet()) { if (entry.getValue().isEmpty()) { @@ -617,7 +679,8 @@ static Match validateNestedExpressions(QuteConfig config, TemplateAnalysis templ LookupConfig lookupConfig, Map namedBeans, Map namespaceTemplateData, List regularExtensionMethods, - Map> namespaceExtensionMethods) { + Map> namespaceExtensionMethods, + Map assignableCache) { LOGGER.debugf("Validate %s from %s", expression, expression.getOrigin()); @@ -633,13 +696,13 @@ static Match validateNestedExpressions(QuteConfig config, TemplateAnalysis templ validateNestedExpressions(config, templateAnalysis, null, results, excludes, incorrectExpressions, param, index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, - regularExtensionMethods, namespaceExtensionMethods); + regularExtensionMethods, namespaceExtensionMethods, assignableCache); } } } } - Match match = new Match(index); + Match match = new Match(index, assignableCache); String namespace = expression.getNamespace(); TemplateDataBuildItem templateData = null; @@ -723,7 +786,7 @@ static Match validateNestedExpressions(QuteConfig config, TemplateAnalysis templ if (extensionMethods != null) { // Namespace is used and at least one namespace extension method exists for the given namespace TemplateExtensionMethodBuildItem extensionMethod = findTemplateExtensionMethod(root, null, extensionMethods, - expression, index, templateIdToPathFun, results); + expression, index, templateIdToPathFun, results, assignableCache); if (extensionMethod != null) { MethodInfo method = extensionMethod.getMethod(); ClassInfo returnType = index.getClassByName(method.returnType().name()); @@ -840,7 +903,7 @@ static Match validateNestedExpressions(QuteConfig config, TemplateAnalysis templ if (match.clazz() != null) { if (info.isVirtualMethod()) { member = findMethod(info.part.asVirtualMethod(), match.clazz(), expression, index, - templateIdToPathFun, results, lookupConfig); + templateIdToPathFun, results, lookupConfig, assignableCache); if (member != null) { membersUsed.add(member.asMethod().name()); } @@ -857,8 +920,7 @@ static Match validateNestedExpressions(QuteConfig config, TemplateAnalysis templ if (member == null) { // Then try to find an etension method extensionMethod = findTemplateExtensionMethod(info, match.type(), regularExtensionMethods, expression, - index, - templateIdToPathFun, results); + index, templateIdToPathFun, results, assignableCache); if (extensionMethod != null) { member = extensionMethod.getMethod(); } @@ -1662,25 +1724,19 @@ static void processLoopElementHint(Match match, IndexView index, Expression expr } else if (match.isClass() || match.isParameterizedType()) { Set closure = Types.getTypeClosure(match.clazz, Types.buildResolvedMap( match.getParameterizedTypeArguments(), match.getTypeParameters(), new HashMap<>(), index), index); - Function firstParamType = t -> t.asParameterizedType().arguments().get(0); // Iterable => Item - matchType = extractMatchType(closure, Names.ITERABLE, firstParamType); + matchType = extractMatchType(closure, Names.ITERABLE, FIRST_PARAM_TYPE_EXTRACT_FUN); if (matchType == null) { // Stream => Long - matchType = extractMatchType(closure, Names.STREAM, firstParamType); + matchType = extractMatchType(closure, Names.STREAM, FIRST_PARAM_TYPE_EXTRACT_FUN); } if (matchType == null) { // Entry => Entry - matchType = extractMatchType(closure, Names.MAP, t -> { - Type[] args = new Type[2]; - args[0] = t.asParameterizedType().arguments().get(0); - args[1] = t.asParameterizedType().arguments().get(1); - return ParameterizedType.create(Names.MAP_ENTRY, args, null); - }); + matchType = extractMatchType(closure, Names.MAP, MAP_ENTRY_EXTRACT_FUN); } if (matchType == null) { // Iterator => Item - matchType = extractMatchType(closure, Names.ITERATOR, firstParamType); + matchType = extractMatchType(closure, Names.ITERATOR, FIRST_PARAM_TYPE_EXTRACT_FUN); } } @@ -1693,19 +1749,48 @@ static void processLoopElementHint(Match match, IndexView index, Expression expr } } + static final Function FIRST_PARAM_TYPE_EXTRACT_FUN = new Function() { + + @Override + public Type apply(Type type) { + return type.asParameterizedType().arguments().get(0); + } + + }; + + static final Function MAP_ENTRY_EXTRACT_FUN = new Function() { + + @Override + public Type apply(Type type) { + Type[] args = new Type[2]; + args[0] = type.asParameterizedType().arguments().get(0); + args[1] = type.asParameterizedType().arguments().get(1); + return ParameterizedType.create(Names.MAP_ENTRY, args, null); + } + + }; + static Type extractMatchType(Set closure, DotName matchName, Function extractFun) { - Type type = closure.stream().filter(t -> t.name().equals(matchName)).findFirst().orElse(null); + Type type = null; + for (Type t : closure) { + if (t.name().equals(matchName)) { + type = t; + } + } return type != null ? extractFun.apply(type) : null; } static class Match { private final IndexView index; + private final Map assignableCache; + private ClassInfo clazz; private Type type; - Match(IndexView index) { + Match(IndexView index, Map assignableCache) { this.index = index; + this.assignableCache = assignableCache; } List getParameterizedTypeArguments() { @@ -1758,30 +1843,35 @@ boolean isEmpty() { } void autoExtractType() { - boolean hasCompletionStage = ValueResolverGenerator.hasCompletionStageInTypeClosure(clazz, index); - boolean hasUni = hasCompletionStage ? false - : ValueResolverGenerator.hasClassInTypeClosure(clazz, Names.UNI, index); - if (hasCompletionStage || hasUni) { - Set closure = Types.getTypeClosure(clazz, Types.buildResolvedMap( - getParameterizedTypeArguments(), getTypeParameters(), new HashMap<>(), index), index); - Function firstParamType = t -> t.asParameterizedType().arguments().get(0); - // CompletionStage> => List - // Uni> => List - this.type = extractMatchType(closure, hasCompletionStage ? Names.COMPLETION_STAGE : Names.UNI, firstParamType); - this.clazz = index.getClassByName(type.name()); + if (clazz != null) { + boolean hasCompletionStage = Types.isAssignableFrom(Names.COMPLETION_STAGE, clazz.name(), index, + assignableCache); + boolean hasUni = hasCompletionStage ? false + : Types.isAssignableFrom(Names.UNI, clazz.name(), index, assignableCache); + if (hasCompletionStage || hasUni) { + Set closure = Types.getTypeClosure(clazz, Types.buildResolvedMap( + getParameterizedTypeArguments(), getTypeParameters(), new HashMap<>(), index), index); + // CompletionStage> => List + // Uni> => List + this.type = extractMatchType(closure, hasCompletionStage ? Names.COMPLETION_STAGE : Names.UNI, + FIRST_PARAM_TYPE_EXTRACT_FUN); + this.clazz = index.getClassByName(type.name()); + } } } } private static TemplateExtensionMethodBuildItem findTemplateExtensionMethod(Info info, Type matchType, List templateExtensionMethods, Expression expression, IndexView index, - Function templateIdToPathFun, Map results) { + Function templateIdToPathFun, Map results, + Map assignableCache) { if (!info.isProperty() && !info.isVirtualMethod()) { return null; } String name = info.isProperty() ? info.asProperty().name : info.asVirtualMethod().name; for (TemplateExtensionMethodBuildItem extensionMethod : templateExtensionMethods) { - if (matchType != null && !Types.isAssignableFrom(extensionMethod.getMatchType(), matchType, index)) { + if (matchType != null + && !Types.isAssignableFrom(extensionMethod.getMatchType(), matchType, index, assignableCache)) { // If "Bar extends Foo" then Bar should be matched for the extension method "int get(Foo)" continue; } @@ -1831,7 +1921,7 @@ private static TemplateExtensionMethodBuildItem findTemplateExtensionMethod(Info paramType = evaluatedParams.get(idx).type; } if (!Types.isAssignableFrom(paramType, - result.type, index)) { + result.type, index, assignableCache)) { matches = false; break; } @@ -1925,7 +2015,7 @@ private static void addInterfaces(ClassInfo clazz, IndexView index, Set private static AnnotationTarget findMethod(VirtualMethodPart virtualMethod, ClassInfo clazz, Expression expression, IndexView index, Function templateIdToPathFun, Map results, - LookupConfig config) { + LookupConfig config, Map assignableCache) { // Find a method with the given name, matching number of params and assignable parameter types Set interfaceNames = config.declaredMembersOnly() ? null : new HashSet<>(); while (clazz != null) { @@ -1934,7 +2024,8 @@ private static AnnotationTarget findMethod(VirtualMethodPart virtualMethod, Clas } for (MethodInfo method : clazz.methods()) { if (config.filter().test(method) - && methodMatches(method, virtualMethod, expression, index, templateIdToPathFun, results)) { + && methodMatches(method, virtualMethod, expression, index, templateIdToPathFun, results, + assignableCache)) { return method; } } @@ -1952,7 +2043,8 @@ && methodMatches(method, virtualMethod, expression, index, templateIdToPathFun, if (interfaceClassInfo != null) { for (MethodInfo method : interfaceClassInfo.methods()) { if (config.filter().test(method) - && methodMatches(method, virtualMethod, expression, index, templateIdToPathFun, results)) { + && methodMatches(method, virtualMethod, expression, index, templateIdToPathFun, results, + assignableCache)) { return method; } } @@ -1964,7 +2056,8 @@ && methodMatches(method, virtualMethod, expression, index, templateIdToPathFun, } private static boolean methodMatches(MethodInfo method, VirtualMethodPart virtualMethod, Expression expression, - IndexView index, Function templateIdToPathFun, Map results) { + IndexView index, Function templateIdToPathFun, Map results, + Map assignableCache) { if (!method.name().equals(virtualMethod.getName())) { return false; @@ -2002,7 +2095,7 @@ private static boolean methodMatches(MethodInfo method, VirtualMethodPart virtua paramType = parameters.get(idx); } if (!Types.isAssignableFrom(paramType, - result.type, index)) { + result.type, index, assignableCache)) { matches = false; break; } @@ -2452,4 +2545,16 @@ public void nextPart() { } + enum Code implements ErrorCode { + + INCORRECT_EXPRESSION, + ; + + @Override + public String getName() { + return "BUILD_" + name(); + } + + } + } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java index 2ed8bce352807..e22433a435f34 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplatesAnalysisBuildItem.java @@ -5,6 +5,7 @@ import io.quarkus.builder.item.SimpleBuildItem; import io.quarkus.qute.Expression; +import io.quarkus.qute.ParameterDeclaration; /** * Represents the result of analysis of all templates. @@ -34,13 +35,17 @@ public static final class TemplateAnalysis { public final List expressions; + public final List parameterDeclarations; + // File path, e.g. hello.html or ItemResource/items.html public final String path; - public TemplateAnalysis(String id, String generatedId, List expressions, String path) { + public TemplateAnalysis(String id, String generatedId, List expressions, + List parameterDeclarations, String path) { this.id = id; this.generatedId = generatedId; this.expressions = expressions; + this.parameterDeclarations = parameterDeclarations; this.path = path; } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeInfos.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeInfos.java index 452782b3fc5f0..a9da5ba7179d9 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeInfos.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeInfos.java @@ -18,6 +18,7 @@ import io.quarkus.qute.Expression; import io.quarkus.qute.Expressions; +import io.quarkus.qute.Expressions.SplitConfig; import io.quarkus.qute.TemplateException; import io.quarkus.qute.TemplateNode.Origin; @@ -169,16 +170,35 @@ private static Type resolveType(String value) { return Type.create(DotName.createSimple(value), Kind.CLASS); } else { String name = value.substring(0, angleIdx); + String params = value.substring(angleIdx + 1, value.length() - 1); DotName rawName = DotName.createSimple(name); - String[] parts = value.substring(angleIdx + 1, value.length() - 1).split(","); - Type[] arguments = new Type[parts.length]; + List parts = Expressions.splitParts(params, PARAMETERIZED_TYPE_SPLIT_CONFIG); + Type[] arguments = new Type[parts.size()]; for (int i = 0; i < arguments.length; i++) { - arguments[i] = resolveType(parts[i].trim()); + arguments[i] = resolveType(parts.get(i).trim()); } return ParameterizedType.create(rawName, arguments, null); } } + static final SplitConfig PARAMETERIZED_TYPE_SPLIT_CONFIG = new SplitConfig() { + + @Override + public boolean isSeparator(char candidate) { + return ',' == candidate; + } + + public boolean isInfixNotationSupported() { + return false; + } + + @Override + public boolean isLiteralSeparator(char candidate) { + return candidate == '<' || candidate == '>'; + } + + }; + private TypeInfos() { } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java index 917b9cd4bdaab..d99ac6bab319e 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java @@ -29,7 +29,7 @@ static Set getTypeClosure(ClassInfo classInfo, Map res Set types = new HashSet<>(); List typeParameters = classInfo.typeParameters(); - if (typeParameters.isEmpty() || !typeParameters.stream().allMatch(resolvedTypeParameters::containsKey)) { + if (typeParameters.isEmpty() || !resolvedTypeParameters.keySet().containsAll(typeParameters)) { // Not a parameterized type or a raw type types.add(Type.create(classInfo.name(), Kind.CLASS)); } else { @@ -128,15 +128,41 @@ static boolean containsTypeVariable(Type type) { return false; } - static boolean isAssignableFrom(Type type1, Type type2, IndexView index) { + static boolean isAssignableFrom(Type type1, Type type2, IndexView index, Map assignableCache) { // TODO consider type params in assignability rules if (type1.kind() == Kind.ARRAY && type2.kind() == Kind.ARRAY) { - return isAssignableFrom(type1.asArrayType().component(), type2.asArrayType().component(), index); + return isAssignableFrom(type1.asArrayType().component(), type2.asArrayType().component(), index, assignableCache); } - return Types.isAssignableFrom(box(type1).name(), box(type2).name(), index); + return Types.isAssignableFrom(box(type1).name(), box(type2).name(), index, assignableCache); } - static boolean isAssignableFrom(DotName class1, DotName class2, IndexView index) { + static class AssignableInfo { + + final Set subclasses; + final Set implementors; + final Set extendingInterfaces; + + public AssignableInfo(Collection subclasses, Collection implementors, + Set extendingInterfaces) { + this.subclasses = new HashSet<>(); + for (ClassInfo subclass : subclasses) { + this.subclasses.add(subclass.name()); + } + this.implementors = new HashSet<>(); + for (ClassInfo implementor : implementors) { + this.implementors.add(implementor.name()); + } + this.extendingInterfaces = extendingInterfaces; + } + + boolean isAssignableFrom(DotName clazz) { + return subclasses.contains(clazz) || implementors.contains(clazz) || extendingInterfaces.contains(clazz); + } + + } + + static boolean isAssignableFrom(DotName class1, DotName class2, IndexView index, + Map assignableCache) { // java.lang.Object is assignable from any type if (class1.equals(DotNames.OBJECT)) { return true; @@ -145,18 +171,13 @@ static boolean isAssignableFrom(DotName class1, DotName class2, IndexView index) if (class1.equals(class2)) { return true; } - // type1 is a superclass - Set assignables = new HashSet<>(); - Collection subclasses = index.getAllKnownSubclasses(class1); - for (ClassInfo subclass : subclasses) { - assignables.add(subclass.name()); - } - Collection implementors = index.getAllKnownImplementors(class1); - for (ClassInfo implementor : implementors) { - assignables.add(implementor.name()); + AssignableInfo assignableInfo = assignableCache.get(class1); + if (assignableInfo == null) { + assignableInfo = new AssignableInfo(index.getAllKnownSubclasses(class1), index.getAllKnownImplementors(class1), + getAllInterfacesExtending(class1, index)); + assignableCache.put(class1, assignableInfo); } - assignables.addAll(getAllInterfacesExtending(class1, index)); - return assignables.contains(class2); + return assignableInfo.isAssignableFrom(class2); } static Type box(Type type) { @@ -192,7 +213,9 @@ static Type box(Primitive primitive) { private static Set getAllInterfacesExtending(DotName target, IndexView index) { Set ret = new HashSet<>(); for (ClassInfo clazz : index.getKnownClasses()) { - if (!Modifier.isInterface(clazz.flags())) { + if (!Modifier.isInterface(clazz.flags()) + || clazz.isAnnotation() + || clazz.isEnum()) { continue; } if (clazz.interfaceNames().contains(target)) { diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java index 7f5fcbe56d0c7..4c55e93394d45 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java @@ -26,10 +26,11 @@ public class PropertyNotFoundDevModeTest { @Test public void testExceptionIsThrown() { - assertEquals("Entry \"foo\" not found in the data map in expression {foo.surname} in template foo.html on line 1", + assertEquals( + "Rendering error in template [foo.html] line 1: Entry \"foo\" not found in the data map in expression {foo.surname}", RestAssured.get("test-foo").then().statusCode(200).extract().body().asString()); assertEquals( - "Property \"name\" not found on the base object \"java.lang.String\" in expression {bar.name} in template bar.html on line 1", + "Rendering error in template [bar.html] line 1: Property \"name\" not found on the base object \"java.lang.String\" in expression {bar.name}", RestAssured.get("test-bar").then().statusCode(200).extract().body().asString()); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeInfosTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeInfosTest.java index bd8e811324853..10f86f00999e2 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeInfosTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypeInfosTest.java @@ -8,16 +8,21 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.regex.Matcher; +import org.jboss.jandex.DotName; import org.jboss.jandex.Index; import org.jboss.jandex.IndexView; import org.jboss.jandex.Indexer; +import org.jboss.jandex.ParameterizedType; import org.junit.jupiter.api.Test; import io.quarkus.qute.Engine; import io.quarkus.qute.Expression; import io.quarkus.qute.deployment.TypeInfos.Info; +import io.quarkus.qute.deployment.TypeInfos.TypeInfo; public class TypeInfosTest { @@ -32,10 +37,9 @@ public void testHintPattern() { @Test public void testCreate() throws IOException { List expressions = Engine.builder().build() - .parse("{@io.quarkus.qute.deployment.TypeInfosTest$Foo foo}{config:['foo.bar.baz']}{foo.name}") + .parse("{@io.quarkus.qute.deployment.TypeInfosTest$Foo foo}{@java.util.Map list}{config:['foo.bar.baz']}{foo.name}{list.size}") .getExpressions(); - ; - IndexView index = index(Foo.class); + IndexView index = index(Foo.class, Map.class); List infos = TypeInfos.create(expressions.get(0), index, id -> "dummy"); assertEquals(1, infos.size()); @@ -48,6 +52,34 @@ public void testCreate() throws IOException { assertEquals("io.quarkus.qute.deployment.TypeInfosTest$Foo", infos.get(0).asTypeInfo().rawClass.name().toString()); assertTrue(infos.get(1).isProperty()); assertEquals("name", infos.get(1).value); + + infos = TypeInfos.create(expressions.get(2), index, id -> "dummy"); + assertEquals(2, infos.size()); + assertTrue(infos.get(0).isTypeInfo()); + assertEquals("java.util.Map", infos.get(0).asTypeInfo().rawClass.name().toString()); + ParameterizedType parameterizedType = infos.get(0).asTypeInfo().resolvedType.asParameterizedType(); + assertEquals("org.acme.Foo", parameterizedType.arguments().get(0).toString()); + assertEquals("String", parameterizedType.arguments().get(1).toString()); + assertTrue(infos.get(1).isProperty()); + assertEquals("size", infos.get(1).value); + } + + @Test + public void testNestedGenerics() throws IOException { + List expressions = Engine.builder().build() + .parse("{@java.util.List> list}{list.size}") + .getExpressions(); + IndexView index = index(Foo.class, List.class, Entry.class); + List infos = TypeInfos.create(expressions.get(0), index, id -> "dummy"); + assertEquals(2, infos.size()); + assertTrue(infos.get(0).isTypeInfo()); + TypeInfo info = infos.get(0).asTypeInfo(); + assertEquals(DotName.createSimple(List.class.getName()), info.rawClass.name()); + ParameterizedType entryType = info.resolvedType.asParameterizedType().arguments().get(0).asParameterizedType(); + assertEquals(DotName.createSimple(Entry.class.getName()), entryType.name()); + assertEquals(DotName.createSimple("String"), entryType.arguments().get(0).name()); + assertEquals(DotName.createSimple("Integer"), entryType.arguments().get(1).name()); + assertTrue(infos.get(1).isProperty()); } private void assertHints(String hintStr, String... expectedHints) { diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumIgnoredTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumIgnoredTest.java index 765737622afc8..7b582d1267580 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumIgnoredTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumIgnoredTest.java @@ -31,7 +31,7 @@ public void testTemplateData() { assertThatExceptionOfType(TemplateException.class) .isThrownBy(() -> engine.parse("{TransactionType:FOO}", null, "bar").render()) .withMessage( - "No namespace resolver found for [TransactionType] in expression {TransactionType:FOO} in template bar on line 1"); + "Rendering error in template [bar] line 1: No namespace resolver found for [TransactionType] in expression {TransactionType:FOO}"); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumInvalidTargetTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumInvalidTargetTest.java index c0ac7eefd33ab..1e11e9b3c9598 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumInvalidTargetTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumInvalidTargetTest.java @@ -27,7 +27,7 @@ public void testTemplateEnum() { assertThatExceptionOfType(TemplateException.class) .isThrownBy(() -> engine.parse("{Transactions:VAL}", null, "bar").render()) .withMessage( - "No namespace resolver found for [Transactions] in expression {Transactions:VAL} in template bar on line 1"); + "Rendering error in template [bar] line 1: No namespace resolver found for [Transactions] in expression {Transactions:VAL}"); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TimeTemplateExtensionsTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TimeTemplateExtensionsTest.java index 073d509cdf07b..e4f758fd094d9 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TimeTemplateExtensionsTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TimeTemplateExtensionsTest.java @@ -59,7 +59,8 @@ public void testInvalidParameter() { engine.parse("{time:format(input.birthday, 'uuuu')}").data("input", Map.of("name", "Quarkus Qute")).render(); fail(); } catch (TemplateException expected) { - assertTrue(expected.getMessage().startsWith("Property \"birthday\" not found on the base object")); + assertTrue(expected.getMessage().startsWith("Rendering error: Property \"birthday\" not found on the base object"), + expected.getMessage()); } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/LocalizedFileBundleLocaleConflictTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/DefaultLocaleMissingMessageTemplateTest.java similarity index 61% rename from extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/LocalizedFileBundleLocaleConflictTest.java rename to extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/DefaultLocaleMissingMessageTemplateTest.java index e49fff4798deb..62cb84248b79d 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/LocalizedFileBundleLocaleConflictTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/DefaultLocaleMissingMessageTemplateTest.java @@ -1,6 +1,6 @@ package io.quarkus.qute.deployment.i18n; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import org.jboss.shrinkwrap.api.asset.StringAsset; @@ -14,28 +14,24 @@ import io.quarkus.runtime.util.ExceptionUtil; import io.quarkus.test.QuarkusUnitTest; -public class LocalizedFileBundleLocaleConflictTest { +public class DefaultLocaleMissingMessageTemplateTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar .addClasses(Messages.class, EnMessages.class) - // This localized file conflicts with the default locale - .addAsResource(new StringAsset( - "hello=Hello!"), - "messages/msg_en.properties")) + .addAsResource(new StringAsset("goodbye=auf Wiedersehen"), "messages/msg_de.properties")) .overrideConfigKey("quarkus.default-locale", "cs") .assertException(t -> { Throwable rootCause = ExceptionUtil.getRootCause(t); if (rootCause instanceof MessageBundleException) { - assertEquals( - "Cannot register [msg_en.properties] - a localized message bundle interface exists for locale [en]: io.quarkus.qute.deployment.i18n.LocalizedFileBundleLocaleConflictTest$EnMessages", - rootCause.getMessage()); + assertTrue( + rootCause.getMessage() + .contains("Message template for key [goodbye] is missing for default locale")); } else { fail("No message bundle exception thrown: " + t); } - - });; + }); @Test public void testValidation() { @@ -45,16 +41,15 @@ public void testValidation() { @MessageBundle public interface Messages { - @Message("Ahoj svete!") - String helloWorld(); - + @Message + String goodbye(); } @Localized("en") public interface EnMessages extends Messages { - @Message("Hello world!") - String helloWorld(); + @Message("Goodbye") + String goodbye(); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/LocalizedFileBundleLocaleMergeTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/LocalizedFileBundleLocaleMergeTest.java new file mode 100644 index 0000000000000..967872f7d1868 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/LocalizedFileBundleLocaleMergeTest.java @@ -0,0 +1,97 @@ +package io.quarkus.qute.deployment.i18n; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.i18n.Localized; +import io.quarkus.qute.i18n.Message; +import io.quarkus.qute.i18n.MessageBundle; +import io.quarkus.test.QuarkusUnitTest; + +public class LocalizedFileBundleLocaleMergeTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Messages.class, EnMessages.class, DeMessages.class) + .addAsResource(new StringAsset("hello_world=Hi!\ngoodbye=Bye"), "messages/msg_en.properties") + .addAsResource(new StringAsset("farewell=Abschied"), "messages/msg_de.properties") + .addAsResource(new StringAsset("goodbye=Mej se!\nfarewell=Sbohem!"), "messages/msg_cs.properties")) + .overrideConfigKey("quarkus.default-locale", "cs"); + + @Localized("en") + Messages enMessages; + + @Localized("de") + Messages deMessages; + + /** + * Default message template method is not overridden. + */ + @Test + public void testDefaultIsUsedAsFallback() { + assertEquals("Nazdar!", enMessages.hello()); + } + + /** + * Localized message template method is provided without {@link Message#value()} + */ + @Test + public void testDefaultIsUsedAsFallback2() { + assertEquals("Greetings!", enMessages.greetings()); + } + + @Test + public void testLocalizedFileIsMerged() { + assertEquals("Bye", enMessages.goodbye()); + } + + @Test + public void testLocalizedInterfaceHasPriority() { + assertEquals("Hello world!", enMessages.hello_world()); + } + + @Test + public void testBothDefaultAndLocalizedFromFile() { + assertEquals("Abschied", deMessages.farewell()); + } + + @MessageBundle + public interface Messages { + + @Message("Ahoj svete!") + String hello_world(); + + @Message("Nazdar!") + String hello(); + + @Message + String goodbye(); + + @Message("Greetings!") + String greetings(); + + @Message + String farewell(); + } + + @Localized("en") + public interface EnMessages extends Messages { + + @Message("Hello world!") + String hello_world(); + + @Message + String greetings(); + + } + + @Localized("de") + public interface DeMessages extends Messages { + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/LocalizedFileDefaultLocaleConflictTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/LocalizedFileDefaultLocaleConflictTest.java deleted file mode 100644 index b5012f059d2ff..0000000000000 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/LocalizedFileDefaultLocaleConflictTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.quarkus.qute.deployment.i18n; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; - -import org.jboss.shrinkwrap.api.asset.StringAsset; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import io.quarkus.qute.deployment.MessageBundleException; -import io.quarkus.qute.i18n.Message; -import io.quarkus.qute.i18n.MessageBundle; -import io.quarkus.runtime.util.ExceptionUtil; -import io.quarkus.test.QuarkusUnitTest; - -public class LocalizedFileDefaultLocaleConflictTest { - - @RegisterExtension - static final QuarkusUnitTest config = new QuarkusUnitTest() - .withApplicationRoot((jar) -> jar - .addClasses(Messages.class) - // This localized file conflicts with the default locale - .addAsResource(new StringAsset( - "hello=Hello!"), - "messages/msg_en.properties")) - .overrideConfigKey("quarkus.default-locale", "en") - .assertException(t -> { - Throwable rootCause = ExceptionUtil.getRootCause(t); - if (rootCause instanceof MessageBundleException) { - assertEquals( - "Locale of [msg_en.properties] conflicts with the locale [en] of the default message bundle [io.quarkus.qute.deployment.i18n.LocalizedFileDefaultLocaleConflictTest$Messages]", - rootCause.getMessage()); - } else { - fail("No message bundle exception thrown: " + t); - } - }); - - @Test - public void testValidation() { - fail(); - } - - @MessageBundle - public interface Messages { - - @Message("Hello world!") - String helloWorld(); - - } - -} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/LocalizedFileDefaultLocaleMergeTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/LocalizedFileDefaultLocaleMergeTest.java new file mode 100644 index 0000000000000..3695ca59fed44 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/LocalizedFileDefaultLocaleMergeTest.java @@ -0,0 +1,55 @@ +package io.quarkus.qute.deployment.i18n; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.i18n.Localized; +import io.quarkus.qute.i18n.Message; +import io.quarkus.qute.i18n.MessageBundle; +import io.quarkus.test.QuarkusUnitTest; + +public class LocalizedFileDefaultLocaleMergeTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(Messages.class) + .addAsResource(new StringAsset("hello=Hi!\ngoodbye=Bye"), "messages/msg_en.properties")) + .overrideConfigKey("quarkus.default-locale", "en"); + + @Localized("en") + Messages messages; + + @Test + public void testInterfaceHasPriority() { + assertEquals("Hello", messages.hello()); + } + + @Test + public void testLocalizedFileIsMerged() { + assertEquals("Bye", messages.goodbye()); + } + + @Test + public void testInterfaceIsMerged() { + assertEquals("Hello world!", messages.helloWorld()); + } + + @MessageBundle + public interface Messages { + + @Message("Hello world!") + String helloWorld(); + + @Message("Hello") + String hello(); + + @Message + String goodbye(); + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/include/InsertTagConflictTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/include/InsertTagConflictTest.java index 608ea80b98f0f..8d39f5a7c4b9f 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/include/InsertTagConflictTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/include/InsertTagConflictTest.java @@ -31,7 +31,7 @@ public class InsertTagConflictTest { } assertNotNull(te); assertTrue(te.getMessage().contains( - "An {#insert} section defined in the {#include} section in template [base.html] on line 1 conflicts with an existing section/tag: row"), + "Parser error in template [base.html] line 1: {#insert} defined in the {#include} conflicts with an existing section/tag: row"), te.getMessage()); });; diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/propertynotfound/PropertyNotFoundThrowExceptionTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/propertynotfound/PropertyNotFoundThrowExceptionTest.java index ef6354e0abfa0..812bb1ffee26b 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/propertynotfound/PropertyNotFoundThrowExceptionTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/propertynotfound/PropertyNotFoundThrowExceptionTest.java @@ -36,7 +36,7 @@ public void testException() { } catch (Exception expected) { Throwable rootCause = ExceptionUtil.getRootCause(expected); assertEquals(TemplateException.class, rootCause.getClass()); - assertTrue(rootCause.getMessage().contains("{foos}")); + assertTrue(rootCause.getMessage().contains("Entry \"foos\" not found in the data map"), rootCause.getMessage()); } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ParamDeclarationDefaultValueTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ParamDeclarationDefaultValueTest.java new file mode 100644 index 0000000000000..15f225d81feed --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ParamDeclarationDefaultValueTest.java @@ -0,0 +1,38 @@ +package io.quarkus.qute.deployment.typesafe; + +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 io.quarkus.qute.Template; +import io.quarkus.qute.TemplateEnum; +import io.quarkus.test.QuarkusUnitTest; + +public class ParamDeclarationDefaultValueTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(MyEnum.class) + .addAsResource(new StringAsset( + "{@io.quarkus.qute.deployment.typesafe.ParamDeclarationDefaultValueTest$MyEnum myEnum=MyEnum:BAR}{myEnum}"), + "templates/myEnum.html")); + + @Inject + Template myEnum; + + @Test + public void testDefaultValue() { + assertEquals("BAR", myEnum.render()); + } + + @TemplateEnum + enum MyEnum { + FOO, + BAR + } +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ParamDeclarationDefaultValueValidationFailureTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ParamDeclarationDefaultValueValidationFailureTest.java new file mode 100644 index 0000000000000..01e276d389ce0 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ParamDeclarationDefaultValueValidationFailureTest.java @@ -0,0 +1,43 @@ +package io.quarkus.qute.deployment.typesafe; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.TemplateException; +import io.quarkus.test.QuarkusUnitTest; + +public class ParamDeclarationDefaultValueValidationFailureTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addAsResource(new StringAsset( + "{@java.lang.String myName=1}{myName}"), + "templates/myName.html")) + .assertException(t -> { + Throwable e = t; + TemplateException te = null; + while (e != null) { + if (e instanceof TemplateException) { + te = (TemplateException) e; + break; + } + e = e.getCause(); + } + assertNotNull(te); + assertTrue(te.getMessage().contains( + " The type of the default value [java.lang.Integer] does not match the type of the parameter declaration [java.lang.String]"), + te.getMessage()); + + }); + + @Test + public void testValidation() { + fail(); + } +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLetTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLetTest.java index 8dced899e9a82..a616a95a6933d 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLetTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/TypeSafeLetTest.java @@ -18,8 +18,9 @@ public class TypeSafeLetTest { .withApplicationRoot((jar) -> jar .addClasses(Movie.class) .addAsResource(new StringAsset("{@io.quarkus.qute.deployment.typesafe.Movie movie}" - + "{#let service=movie.findService('foo')}" + + "{#let service=movie.findService('foo') name?=movie.name}" + "{service.shortValue}" + + "::{name.length}" + "{/let}"), "templates/foo.html")); @Inject @@ -27,7 +28,7 @@ public class TypeSafeLetTest { @Test public void testValidation() { - assertEquals("10", + assertEquals("10::5", foo.data("movie", new Movie()).render()); } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java index 2b11a569a3008..a8b9472036721 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java @@ -28,6 +28,12 @@ */ String ELEMENT_NAME = "<>"; + /** + * Constant value for {@link #value()} indicating that message template value specified in a localized file + * should be used. If localized file fails to provide value, an exception is thrown and the build fails. + */ + String DEFAULT_VALUE = "<>"; + /** * Constant value for {@link #key()} indicating that the annotated element's name should be de-camel-cased and * hyphenated, and then used. @@ -47,9 +53,12 @@ String key() default DEFAULT_NAME; /** + * This value has higher priority over a message template specified in a localized file, and it's + * considered a good practice to specify it. In case the value is not provided and there is no + * match in the localized file too, the build fails. * * @return the message template */ - String value(); + String value() default DEFAULT_VALUE; } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/PropertyNotFoundThrowException.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/PropertyNotFoundThrowException.java index 01ca4636a417c..d4794fc881711 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/PropertyNotFoundThrowException.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/PropertyNotFoundThrowException.java @@ -27,9 +27,9 @@ public String map(Object result, Expression expression) { } else { propertyMessage = "Property not found"; } - throw new TemplateException(expression.getOrigin(), - String.format("%s in expression {%s} in template %s on line %s", propertyMessage, expression.toOriginalString(), - expression.getOrigin().getTemplateId(), expression.getOrigin().getLine())); + throw TemplateException.builder().origin(expression.getOrigin()) + .message("{}{#if origin.hasNonGeneratedTemplateId??} in{origin}{/if}: expression \\{{}}") + .arguments(propertyMessage, expression.toOriginalString()).build(); } } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java index 54fdfcbc19c14..100d872678692 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java @@ -11,6 +11,7 @@ import java.util.Optional; import java.util.concurrent.CompletionStage; import java.util.function.Consumer; +import java.util.function.Predicate; import javax.enterprise.inject.Produces; import javax.enterprise.inject.spi.AnnotatedParameter; @@ -22,6 +23,7 @@ import io.quarkus.qute.Engine; import io.quarkus.qute.Expression; import io.quarkus.qute.Location; +import io.quarkus.qute.ParameterDeclaration; import io.quarkus.qute.Template; import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.TemplateInstanceBase; @@ -117,6 +119,16 @@ public List getExpressions() { throw new UnsupportedOperationException("Injected templates do not support getExpressions()"); } + @Override + public Expression findExpression(Predicate predicate) { + throw new UnsupportedOperationException("Injected templates do not support findExpression()"); + } + + @Override + public List getParameterDeclarations() { + throw new UnsupportedOperationException("Injected templates do not support getParameterDeclarations()"); + } + @Override public String getGeneratedId() { throw new UnsupportedOperationException("Injected templates do not support getGeneratedId()"); diff --git a/extensions/reactive-mysql-client/deployment/pom.xml b/extensions/reactive-mysql-client/deployment/pom.xml index 72ed890af1b81..caef8a588fe1e 100644 --- a/extensions/reactive-mysql-client/deployment/pom.xml +++ b/extensions/reactive-mysql-client/deployment/pom.xml @@ -183,7 +183,7 @@ - ${project.basedir}/custom-mariadbconfig:/etc/mysql/conf.d/:Z + ${project.basedir}/custom-mariadbconfig:/etc/mysql/conf.d:Z diff --git a/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java index 7c9ceba7a0646..1f6f658403d62 100644 --- a/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java +++ b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java @@ -7,6 +7,7 @@ import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpCompression; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.vertx.core.Handler; @@ -18,9 +19,12 @@ public class VertxWebRecorder { final RuntimeValue httpConfiguration; + final HttpBuildTimeConfig httpBuildTimeConfig; - public VertxWebRecorder(RuntimeValue httpConfiguration) { + public VertxWebRecorder(RuntimeValue httpConfiguration, + HttpBuildTimeConfig httpBuildTimeConfig) { this.httpConfiguration = httpConfiguration; + this.httpBuildTimeConfig = httpBuildTimeConfig; } @SuppressWarnings("unchecked") @@ -41,10 +45,10 @@ public Handler createHandler(String handlerClassName) { } public Handler compressRouteHandler(Handler routeHandler, HttpCompression compression) { - if (httpConfiguration.getValue().enableCompression) { + if (httpBuildTimeConfig.enableCompression) { return new HttpCompressionHandler(routeHandler, compression, compression == HttpCompression.UNDEFINED - ? Set.copyOf(httpConfiguration.getValue().compressMediaTypes.orElse(List.of())) + ? Set.copyOf(httpBuildTimeConfig.compressMediaTypes.orElse(List.of())) : Set.of()); } else { return routeHandler; diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesRedisProcessor.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesRedisProcessor.java index dad2345df6b83..1ed1202a9e677 100644 --- a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesRedisProcessor.java +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesRedisProcessor.java @@ -20,13 +20,13 @@ import org.testcontainers.utility.DockerImageName; import io.quarkus.deployment.Feature; -import io.quarkus.deployment.IsDockerWorking.IsDockerRunningSilent; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.console.ConsoleInstalledBuildItem; import io.quarkus.deployment.console.StartupLogCompressor; @@ -59,10 +59,10 @@ public class DevServicesRedisProcessor { private static volatile List devServices; private static volatile Map capturedDevServicesConfiguration; private static volatile boolean first = true; - private static volatile Boolean dockerRunning = null; @BuildStep(onlyIfNot = IsNormal.class, onlyIf = { GlobalDevServicesConfig.Enabled.class }) public List startRedisContainers(LaunchModeBuildItem launchMode, + DockerStatusBuildItem dockerStatusBuildItem, List devServicesSharedNetworkBuildItem, RedisBuildTimeConfig config, Optional consoleInstalledBuildItem, @@ -100,7 +100,8 @@ public List startRedisContainers(LaunchModeBuildItem try { for (Entry entry : currentDevServicesConfiguration.entrySet()) { String connectionName = entry.getKey(); - RunningDevService devService = startContainer(connectionName, entry.getValue().devservices, + RunningDevService devService = startContainer(dockerStatusBuildItem, connectionName, + entry.getValue().devservices, launchMode.getLaunchMode(), !devServicesSharedNetworkBuildItem.isEmpty(), devServicesConfig.timeout); if (devService == null) { @@ -111,7 +112,11 @@ public List startRedisContainers(LaunchModeBuildItem log.infof("The %s redis server is ready to accept connections on %s", connectionName, devService.getConfig().get(configKey)); } - compressor.close(); + if (newDevServices.isEmpty()) { + compressor.closeAndDumpCaptured(); + } else { + compressor.close(); + } } catch (Throwable t) { compressor.closeAndDumpCaptured(); throw new RuntimeException(t); @@ -122,7 +127,6 @@ public List startRedisContainers(LaunchModeBuildItem if (first) { first = false; Runnable closeTask = () -> { - dockerRunning = null; if (devServices != null) { for (Closeable closeable : devServices) { try { @@ -141,7 +145,8 @@ public List startRedisContainers(LaunchModeBuildItem return devServices.stream().map(RunningDevService::toBuildItem).collect(Collectors.toList()); } - private RunningDevService startContainer(String connectionName, DevServicesConfig devServicesConfig, LaunchMode launchMode, + private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuildItem, String connectionName, + DevServicesConfig devServicesConfig, LaunchMode launchMode, boolean useSharedNetwork, Optional timeout) { if (!devServicesConfig.enabled) { // explicitly disabled @@ -159,11 +164,7 @@ private RunningDevService startContainer(String connectionName, DevServicesConfi return null; } - if (dockerRunning == null) { - dockerRunning = new IsDockerRunningSilent().getAsBoolean(); - } - - if (!dockerRunning) { + if (!dockerStatusBuildItem.isDockerAvailable()) { log.warn("Please configure quarkus.redis.hosts for " + (isDefault(connectionName) ? "default redis client" : connectionName) + " or get a working docker instance"); 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 f0f87c0be37f8..4d833152a4c9f 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 @@ -44,6 +44,7 @@ public class RestClientConfig { EMPTY.headers = Collections.emptyMap(); EMPTY.shared = Optional.empty(); EMPTY.name = Optional.empty(); + EMPTY.userAgent = Optional.empty(); } /** @@ -220,6 +221,14 @@ public class RestClientConfig { @ConfigItem public Optional name; + /** + * Configure the HTTP user-agent header to use. + * + * This property is applicable to reactive REST clients only. + */ + @ConfigItem + public Optional userAgent; + public static RestClientConfig load(String configKey) { final RestClientConfig instance = new RestClientConfig(); @@ -248,6 +257,7 @@ public static RestClientConfig load(String configKey) { instance.headers = getConfigValues(configKey, "headers", String.class, String.class); instance.shared = getConfigValue(configKey, "shared", Boolean.class); instance.name = getConfigValue(configKey, "name", String.class); + instance.userAgent = getConfigValue(configKey, "user-agent", String.class); return instance; } @@ -280,6 +290,7 @@ public static RestClientConfig load(Class interfaceClass) { instance.headers = getConfigValues(interfaceClass, "headers", String.class, String.class); instance.shared = getConfigValue(interfaceClass, "shared", Boolean.class); instance.name = getConfigValue(interfaceClass, "name", String.class); + instance.userAgent = getConfigValue(interfaceClass, "user-agent", String.class); return instance; } 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 69919795a33a0..1e4a88c51db38 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 @@ -116,6 +116,14 @@ public class RestClientsConfig { @ConfigItem(defaultValue = "false") public boolean disableContextualErrorMessages; + /** + * Configure the HTTP user-agent header to use. + * + * This property is applicable to reactive REST clients only. + */ + @ConfigItem + public Optional userAgent; + public RestClientConfig getClientConfig(String configKey) { if (configKey == null) { return RestClientConfig.EMPTY; diff --git a/extensions/resteasy-classic/rest-client-jaxb/deployment/src/main/java/io/quarkus/restclient/jaxb/deployment/RestClientJaxbProcessor.java b/extensions/resteasy-classic/rest-client-jaxb/deployment/src/main/java/io/quarkus/restclient/jaxb/deployment/RestClientJaxbProcessor.java old mode 100755 new mode 100644 diff --git a/extensions/resteasy-classic/rest-client-jsonb/deployment/src/main/java/io/quarkus/restclient/jsonb/deployment/RestClientJsonbProcessor.java b/extensions/resteasy-classic/rest-client-jsonb/deployment/src/main/java/io/quarkus/restclient/jsonb/deployment/RestClientJsonbProcessor.java old mode 100755 new mode 100644 diff --git a/extensions/resteasy-classic/rest-client-jsonb/runtime/pom.xml b/extensions/resteasy-classic/rest-client-jsonb/runtime/pom.xml index cb66c255f9fdf..58a91bea2bc3b 100644 --- a/extensions/resteasy-classic/rest-client-jsonb/runtime/pom.xml +++ b/extensions/resteasy-classic/rest-client-jsonb/runtime/pom.xml @@ -25,6 +25,13 @@ org.jboss.resteasy resteasy-json-binding-provider + + + + org.glassfish + jakarta.json + + org.jboss.resteasy diff --git a/extensions/resteasy-classic/resteasy-common/runtime/pom.xml b/extensions/resteasy-classic/resteasy-common/runtime/pom.xml index 7224aff534492..b2b019c2e7e10 100644 --- a/extensions/resteasy-classic/resteasy-common/runtime/pom.xml +++ b/extensions/resteasy-classic/resteasy-common/runtime/pom.xml @@ -77,6 +77,13 @@ org.jboss.resteasy resteasy-json-binding-provider true + + + + org.glassfish + jakarta.json + + diff --git a/extensions/resteasy-classic/resteasy-jaxb/deployment/src/main/java/io/quarkus/resteasy/jaxb/deployment/ResteasyJaxbProcessor.java b/extensions/resteasy-classic/resteasy-jaxb/deployment/src/main/java/io/quarkus/resteasy/jaxb/deployment/ResteasyJaxbProcessor.java old mode 100755 new mode 100644 diff --git a/extensions/resteasy-classic/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java b/extensions/resteasy-classic/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java old mode 100755 new mode 100644 diff --git a/extensions/resteasy-classic/resteasy-jsonb/runtime/pom.xml b/extensions/resteasy-classic/resteasy-jsonb/runtime/pom.xml index 4236a8df917ab..4cc30b2a5dc8d 100644 --- a/extensions/resteasy-classic/resteasy-jsonb/runtime/pom.xml +++ b/extensions/resteasy-classic/resteasy-jsonb/runtime/pom.xml @@ -25,6 +25,13 @@ org.jboss.resteasy resteasy-json-binding-provider + + + + org.glassfish + jakarta.json + + org.jboss.resteasy diff --git a/extensions/resteasy-classic/resteasy-links/deployment/pom.xml b/extensions/resteasy-classic/resteasy-links/deployment/pom.xml index 4c55f33c1fe16..6287cd5765d9f 100644 --- a/extensions/resteasy-classic/resteasy-links/deployment/pom.xml +++ b/extensions/resteasy-classic/resteasy-links/deployment/pom.xml @@ -22,6 +22,26 @@ io.quarkus quarkus-resteasy-links + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + org.assertj + assertj-core + test + + + org.glassfish + jakarta.el + test + @@ -40,6 +60,4 @@ - - diff --git a/extensions/resteasy-classic/resteasy-links/deployment/src/main/java/io/quarkus/resteasy/links/deployment/LinksProcessor.java b/extensions/resteasy-classic/resteasy-links/deployment/src/main/java/io/quarkus/resteasy/links/deployment/LinksProcessor.java new file mode 100644 index 0000000000000..f972525e283e3 --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/deployment/src/main/java/io/quarkus/resteasy/links/deployment/LinksProcessor.java @@ -0,0 +1,31 @@ +package io.quarkus.resteasy.links.deployment; + +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.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.links.runtime.hal.HalServerResponseFilter; +import io.quarkus.resteasy.links.runtime.hal.ResteasyHalService; + +final class LinksProcessor { + @BuildStep + void addHalSupport(Capabilities capabilities, BuildProducer jaxRsProviders, + BuildProducer additionalBeans) { + boolean isHalSupported = capabilities.isPresent(Capability.HAL); + if (isHalSupported) { + if (!capabilities.isPresent(Capability.RESTEASY_JSON_JSONB) + && !capabilities.isPresent(Capability.RESTEASY_JSON_JACKSON)) { + throw new IllegalStateException("Cannot generate HAL endpoints without " + + "either 'quarkus-resteasy-jsonb' or 'quarkus-resteasy-jackson'"); + } + + jaxRsProviders.produce( + new ResteasyJaxrsProviderBuildItem(HalServerResponseFilter.class.getName())); + + additionalBeans.produce(AdditionalBeanBuildItem.builder() + .addBeanClass(ResteasyHalService.class).setUnremovable().build()); + } + } +} diff --git a/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/AbstractEntity.java b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/AbstractEntity.java new file mode 100644 index 0000000000000..f06f27dc4f8ed --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/AbstractEntity.java @@ -0,0 +1,32 @@ +package io.quarkus.resteasy.links.deployment; + +public abstract class AbstractEntity { + + private int id; + + private String slug; + + public AbstractEntity() { + } + + protected AbstractEntity(int id, String slug) { + this.id = id; + this.slug = slug; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } +} diff --git a/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/AbstractHalLinksTest.java b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/AbstractHalLinksTest.java new file mode 100644 index 0000000000000..0ccca18469c63 --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/AbstractHalLinksTest.java @@ -0,0 +1,32 @@ +package io.quarkus.resteasy.links.deployment; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.restassured.response.Response; + +public abstract class AbstractHalLinksTest { + + private static final String APPLICATION_HAL_JSON = "application/hal+json"; + + @Test + void shouldGetHalLinksForCollections() { + Response response = given().accept(APPLICATION_HAL_JSON) + .get("/records") + .thenReturn(); + + assertThat(response.body().jsonPath().getList("_embedded.items.id")).containsOnly(1, 2); + assertThat(response.body().jsonPath().getString("_links.list.href")).endsWith("/records"); + } + + @Test + void shouldGetHalLinksForInstance() { + Response response = given().accept(APPLICATION_HAL_JSON) + .get("/records/first") + .thenReturn(); + + assertThat(response.body().jsonPath().getString("_links.list.href")).endsWith("/records"); + } +} diff --git a/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/HalLinksWithJacksonTest.java b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/HalLinksWithJacksonTest.java new file mode 100644 index 0000000000000..4cce2ea8b3588 --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/HalLinksWithJacksonTest.java @@ -0,0 +1,22 @@ +package io.quarkus.resteasy.links.deployment; + +import java.util.Arrays; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.builder.Version; +import io.quarkus.test.QuarkusProdModeTest; + +public class HalLinksWithJacksonTest extends AbstractHalLinksTest { + @RegisterExtension + static final QuarkusProdModeTest TEST = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(AbstractEntity.class, TestRecord.class, TestResource.class)) + .setForcedDependencies( + Arrays.asList( + new AppArtifact("io.quarkus", "quarkus-resteasy-jackson", Version.getVersion()), + new AppArtifact("io.quarkus", "quarkus-hal", Version.getVersion()))) + .setLogFileName("app.log") + .setRun(true); +} diff --git a/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/HalLinksWithJsonbTest.java b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/HalLinksWithJsonbTest.java new file mode 100644 index 0000000000000..2fc962a0bb5e8 --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/HalLinksWithJsonbTest.java @@ -0,0 +1,22 @@ +package io.quarkus.resteasy.links.deployment; + +import java.util.Arrays; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.builder.Version; +import io.quarkus.test.QuarkusProdModeTest; + +public class HalLinksWithJsonbTest extends AbstractHalLinksTest { + @RegisterExtension + static final QuarkusProdModeTest TEST = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(AbstractEntity.class, TestRecord.class, TestResource.class)) + .setForcedDependencies( + Arrays.asList( + new AppArtifact("io.quarkus", "quarkus-resteasy-jsonb", Version.getVersion()), + new AppArtifact("io.quarkus", "quarkus-hal", Version.getVersion()))) + .setRun(true); + +} diff --git a/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/TestRecord.java b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/TestRecord.java new file mode 100644 index 0000000000000..f449fea6407e2 --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/TestRecord.java @@ -0,0 +1,22 @@ +package io.quarkus.resteasy.links.deployment; + +public class TestRecord extends AbstractEntity { + + private String value; + + public TestRecord() { + } + + public TestRecord(int id, String slug, String value) { + super(id, slug); + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/TestResource.java b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/TestResource.java new file mode 100644 index 0000000000000..a2b417099ef07 --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/TestResource.java @@ -0,0 +1,38 @@ +package io.quarkus.resteasy.links.deployment; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.links.LinkResource; + +@Path("/records") +public class TestResource { + + private static final AtomicInteger ID_COUNTER = new AtomicInteger(0); + + private static final List RECORDS = new LinkedList<>(Arrays.asList( + new TestRecord(ID_COUNTER.incrementAndGet(), "first", "First value"), + new TestRecord(ID_COUNTER.incrementAndGet(), "second", "Second value"))); + + @GET + @Produces({ MediaType.APPLICATION_JSON, "application/hal+json" }) + @LinkResource(entityClassName = "io.quarkus.resteasy.links.deployment.TestRecord", rel = "list") + public List getAll() { + return RECORDS; + } + + @GET + @Path("/first") + @Produces({ MediaType.APPLICATION_JSON, "application/hal+json" }) + @LinkResource(rel = "first") + public TestRecord getFirst() { + return RECORDS.get(0); + } +} diff --git a/extensions/resteasy-classic/resteasy-links/runtime/pom.xml b/extensions/resteasy-classic/resteasy-links/runtime/pom.xml index e6b18a4191bf4..4440152caf48e 100644 --- a/extensions/resteasy-classic/resteasy-links/runtime/pom.xml +++ b/extensions/resteasy-classic/resteasy-links/runtime/pom.xml @@ -33,6 +33,11 @@ + + io.quarkus + quarkus-hal + true + diff --git a/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/HalServerResponseFilter.java b/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/HalServerResponseFilter.java new file mode 100644 index 0000000000000..b2711330b5da7 --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/HalServerResponseFilter.java @@ -0,0 +1,66 @@ +package io.quarkus.resteasy.links.runtime.hal; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +import io.quarkus.arc.Arc; +import io.quarkus.hal.HalCollectionWrapper; +import io.quarkus.hal.HalEntityWrapper; + +@Provider +public class HalServerResponseFilter implements ContainerResponseFilter { + + private static final String APPLICATION_HAL_JSON = "application/hal+json"; + private static final String COLLECTION_NAME = "items"; + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { + Object entity = responseContext.getEntity(); + if (isHttpStatusSuccessful(responseContext.getStatusInfo()) + && acceptsHalMediaType(requestContext) + && canEntityBeProcessed(entity)) { + ResteasyHalService service = Arc.container().instance(ResteasyHalService.class).get(); + if (entity instanceof Collection) { + responseContext.setEntity(service.toHalCollectionWrapper((Collection) entity, + COLLECTION_NAME, findEntityClass(requestContext, responseContext.getEntityType()))); + } else { + responseContext.setEntity(service.toHalWrapper(entity)); + } + } + } + + private boolean canEntityBeProcessed(Object entity) { + return entity != null + && !(entity instanceof String) + && !(entity instanceof HalEntityWrapper || entity instanceof HalCollectionWrapper); + } + + private boolean isHttpStatusSuccessful(Response.StatusType statusInfo) { + return Response.Status.Family.SUCCESSFUL.equals(statusInfo.getFamily()); + } + + private boolean acceptsHalMediaType(ContainerRequestContext requestContext) { + List acceptMediaType = requestContext.getAcceptableMediaTypes().stream().map(MediaType::toString).collect( + Collectors.toList()); + return acceptMediaType.contains(APPLICATION_HAL_JSON); + } + + private Class findEntityClass(ContainerRequestContext requestContext, Type entityType) { + if (entityType instanceof ParameterizedType) { + // we can resolve the entity class from the param type + return (Class) ((ParameterizedType) entityType).getActualTypeArguments()[0]; + } + + return null; + } +} diff --git a/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java b/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java new file mode 100644 index 0000000000000..6b5161282cb2f --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java @@ -0,0 +1,36 @@ +package io.quarkus.resteasy.links.runtime.hal; + +import java.util.HashMap; +import java.util.Map; + +import javax.enterprise.context.RequestScoped; + +import org.jboss.resteasy.links.LinksProvider; +import org.jboss.resteasy.links.RESTServiceDiscovery; + +import io.quarkus.hal.HalLink; +import io.quarkus.hal.HalService; + +@RequestScoped +public class ResteasyHalService extends HalService { + + @Override + protected Map getClassLinks(Class entityClass) { + return linksToMap(LinksProvider.getClassLinksProvider().getLinks(entityClass, + Thread.currentThread().getContextClassLoader())); + } + + @Override + protected Map getInstanceLinks(Object entity) { + return linksToMap(LinksProvider.getObjectLinksProvider().getLinks(entity, + Thread.currentThread().getContextClassLoader())); + } + + private Map linksToMap(RESTServiceDiscovery serviceDiscovery) { + Map links = new HashMap<>(serviceDiscovery.size()); + for (RESTServiceDiscovery.AtomLink atomLink : serviceDiscovery) { + links.put(atomLink.getRel(), new HalLink(atomLink.getHref())); + } + return links; + } +} diff --git a/extensions/resteasy-classic/resteasy-links/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/resteasy-classic/resteasy-links/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 7e1bebda26e85..c391cb8763b03 100644 --- a/extensions/resteasy-classic/resteasy-links/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/resteasy-classic/resteasy-links/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -11,7 +11,6 @@ metadata: - "web" - "serialization" status: "stable" - unlisted: true codestart: name: "resteasy" languages: diff --git a/extensions/resteasy-classic/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java b/extensions/resteasy-classic/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java old mode 100755 new mode 100644 index 96da8f5c108fa..fe3f2df8ce4ad --- a/extensions/resteasy-classic/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java +++ b/extensions/resteasy-classic/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java @@ -56,7 +56,6 @@ import io.quarkus.arc.processor.BuiltinScope; import io.quarkus.arc.processor.DotNames; import io.quarkus.arc.processor.Transformation; -import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -184,10 +183,8 @@ static final class ResteasyConfig { @BuildStep NativeImageResourceBundleBuildItem optionalResourceBundle() { - for (ClassPathElement cpe : QuarkusClassLoader.getElements(MESSAGES_RESOURCE_BUNDLE, false)) { - if (cpe.isRuntime()) { - return new NativeImageResourceBundleBuildItem(MESSAGES_RESOURCE_BUNDLE); - } + if (QuarkusClassLoader.isResourcePresentAtRuntime(MESSAGES_RESOURCE_BUNDLE)) { + return new NativeImageResourceBundleBuildItem(MESSAGES_RESOURCE_BUNDLE); } return null; diff --git a/extensions/resteasy-classic/resteasy/deployment/pom.xml b/extensions/resteasy-classic/resteasy/deployment/pom.xml index 9a0c93589b7df..ab503764f1bdd 100644 --- a/extensions/resteasy-classic/resteasy/deployment/pom.xml +++ b/extensions/resteasy-classic/resteasy/deployment/pom.xml @@ -53,12 +53,6 @@ awaitility test - - - org.jboss.resteasy - resteasy-json-binding-provider - test - commons-io commons-io 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 b037afb10decc..d198c210afc5c 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 @@ -116,6 +116,7 @@ import io.quarkus.arc.processor.DotNames; import io.quarkus.arc.processor.MethodDescriptors; import io.quarkus.arc.processor.Types; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; @@ -743,6 +744,10 @@ A more full example of generated client (with sub-resource) can is at the bottom MethodDescriptor.ofMethod(WebTarget.class, "path", WebTarget.class, String.class), classContext.constructor.getMethodParam(0), classContext.constructor.load(restClientInterface.getPath()))); + FieldDescriptor baseTargetField = classContext.classCreator + .getFieldCreator("baseTarget", WebTargetImpl.class.getName()) + .getFieldDescriptor(); + classContext.constructor.writeInstanceField(baseTargetField, classContext.constructor.getThis(), baseTarget); for (JaxrsClientReactiveEnricherBuildItem enricher : enrichers) { enricher.getEnricher().forClass(classContext.constructor, baseTarget, interfaceClass, index); @@ -752,7 +757,6 @@ A more full example of generated client (with sub-resource) can is at the bottom // go through all the methods of the jaxrs interface. Create specific WebTargets (in the constructor) and methods // int methodIndex = 0; - List webTargets = new ArrayList<>(); for (ResourceMethod method : restClientInterface.getMethods()) { methodIndex++; @@ -775,10 +779,8 @@ A more full example of generated client (with sub-resource) can is at the bottom if (method.getHttpMethod() == null) { handleSubResourceMethod(enrichers, generatedClasses, interfaceClass, index, defaultMediaType, - httpAnnotationToMethod, - name, classContext, classContext.constructor, - baseTarget, methodIndex, webTargets, method, javaMethodParameters, jandexMethod, - multipartResponseTypes); + httpAnnotationToMethod, name, classContext, baseTarget, methodIndex, method, + javaMethodParameters, jandexMethod, multipartResponseTypes, Collections.emptyList()); } else { FieldDescriptor methodField = classContext.createJavaMethodField(interfaceClass, jandexMethod, methodIndex); @@ -793,7 +795,6 @@ A more full example of generated client (with sub-resource) can is at the bottom // constructor: initializing the immutable part of the method-specific web target FieldDescriptor webTargetForMethod = FieldDescriptor.of(name, "target" + methodIndex, WebTargetImpl.class); classContext.classCreator.getFieldCreator(webTargetForMethod).setModifiers(Modifier.FINAL); - webTargets.add(webTargetForMethod); AssignableResultHandle constructorTarget = createWebTargetForMethod(classContext.constructor, baseTarget, method); @@ -970,15 +971,14 @@ A more full example of generated client (with sub-resource) can is at the bottom classContext.clinit.returnValue(null); // create `void close()` method: + // we only close the RestClient of the base target - all targets share the same one MethodCreator closeCreator = classContext.classCreator .getMethodCreator(MethodDescriptor.ofMethod(Closeable.class, "close", void.class)); - for (FieldDescriptor target : webTargets) { - ResultHandle webTarget = closeCreator.readInstanceField(target, closeCreator.getThis()); - ResultHandle webTargetImpl = closeCreator.checkCast(webTarget, WebTargetImpl.class); - ResultHandle restClient = closeCreator.invokeVirtualMethod( - MethodDescriptor.ofMethod(WebTargetImpl.class, "getRestClient", ClientImpl.class), webTargetImpl); - closeCreator.invokeVirtualMethod(MethodDescriptor.ofMethod(ClientImpl.class, "close", void.class), restClient); - } + ResultHandle webTarget = closeCreator.readInstanceField(baseTargetField, closeCreator.getThis()); + ResultHandle webTargetImpl = closeCreator.checkCast(webTarget, WebTargetImpl.class); + ResultHandle restClient = closeCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(WebTargetImpl.class, "getRestClient", ClientImpl.class), webTargetImpl); + closeCreator.invokeVirtualMethod(MethodDescriptor.ofMethod(ClientImpl.class, "close", void.class), restClient); closeCreator.returnValue(null); } @@ -1031,17 +1031,16 @@ private ClassInfo returnTypeAsClass(MethodInfo jandexMethod, IndexView index) { private void handleSubResourceMethod(List enrichers, BuildProducer generatedClasses, ClassInfo interfaceClass, IndexView index, String defaultMediaType, Map httpAnnotationToMethod, String name, - ClassRestClientContext classContext, MethodCreator constructor, AssignableResultHandle baseTarget, int methodIndex, - List webTargets, + ClassRestClientContext ownerContext, ResultHandle ownerTarget, int methodIndex, ResourceMethod method, String[] javaMethodParameters, MethodInfo jandexMethod, - Set multipartResponseTypes) { + Set multipartResponseTypes, List ownerSubResourceParameters) { Type returnType = jandexMethod.returnType(); if (returnType.kind() != CLASS) { // sort of sub-resource method that returns a thing that isn't a class throw new IllegalArgumentException("Sub resource type is not a class: " + returnType.name().toString()); } - ClassInfo subResourceInterface = index.getClassByName(returnType.name()); - if (!Modifier.isInterface(subResourceInterface.flags())) { + ClassInfo subInterface = index.getClassByName(returnType.name()); + if (!Modifier.isInterface(subInterface.flags())) { throw new IllegalArgumentException( "Client interface method: " + jandexMethod.declaringClass().name() + "#" + jandexMethod + " has no HTTP method annotation (@GET, @POST, etc) and it's return type: " @@ -1050,321 +1049,333 @@ private void handleSubResourceMethod(List + "If it's not, it has to have one of the HTTP method annotations."); } - classContext.createJavaMethodField(interfaceClass, jandexMethod, methodIndex); - Supplier methodParamAnnotationsField = classContext.getLazyJavaMethodParamAnnotationsField( - methodIndex); - Supplier methodGenericParametersField = classContext.getLazyJavaMethodGenericParametersField( - methodIndex); + ownerContext.createJavaMethodField(interfaceClass, jandexMethod, methodIndex); - // generate implementation for a method from the jaxrs interface: - MethodCreator methodCreator = classContext.classCreator.getMethodCreator(method.getName(), method.getSimpleReturnType(), + // generate implementation for a method that returns the sub-client: + MethodCreator ownerMethod = ownerContext.classCreator.getMethodCreator(method.getName(), method.getSimpleReturnType(), javaMethodParameters); - String subName = subResourceInterface.name().toString() + HashUtil.sha1(name) + methodIndex; - try (ClassRestClientContext subClassContext = new ClassRestClientContext(subName, generatedClasses, - subResourceInterface.name().toString())) { + String subName = subInterface.name().toString() + HashUtil.sha1(name) + methodIndex; + MethodDescriptor subConstructorDescriptor = MethodDescriptor.ofConstructor(subName, WebTargetImpl.class.getName()); + try (ClassRestClientContext subContext = new ClassRestClientContext(subName, subConstructorDescriptor, + generatedClasses, Object.class, subInterface.name().toString())) { + + subContext.constructor.invokeSpecialMethod(MethodDescriptor.ofConstructor(Object.class), + subContext.constructor.getThis()); + + AssignableResultHandle constructorTarget = createWebTargetForMethod(ownerContext.constructor, ownerTarget, + method); - subClassContext.constructor.invokeSpecialMethod(MethodDescriptor.ofConstructor(Object.class), - subClassContext.constructor.getThis()); + FieldDescriptor forMethodTargetDesc = ownerContext.classCreator + .getFieldCreator("targetInOwner" + methodIndex, WebTargetImpl.class).getFieldDescriptor(); + ownerContext.constructor.writeInstanceField(forMethodTargetDesc, ownerContext.constructor.getThis(), + constructorTarget); - ResultHandle subInstance = methodCreator.newInstance(MethodDescriptor.ofConstructor(subName)); + ResultHandle subInstance = ownerMethod.newInstance(subConstructorDescriptor, + ownerMethod.readInstanceField(forMethodTargetDesc, ownerMethod.getThis())); - Map paramFields = new HashMap<>(); + List subParamFields = new ArrayList<>(); + + for (SubResourceParameter ownerParameter : ownerSubResourceParameters) { + FieldDescriptor paramField = subContext.classCreator.getFieldCreator(ownerParameter.field.getName() + "$_", + ownerParameter.typeName) + .setModifiers(Modifier.PUBLIC) + .getFieldDescriptor(); + ownerMethod.writeInstanceField(paramField, subInstance, + ownerMethod.readInstanceField(ownerParameter.field, ownerMethod.getThis())); + subParamFields.add(new SubResourceParameter(ownerParameter.methodParameter, ownerParameter.typeName, + ownerParameter.type, paramField, ownerParameter.paramAnnotationsField, + ownerParameter.genericsParametersField, + ownerParameter.paramIndex)); + } - FieldDescriptor clientField = createRestClientField(name, classContext.classCreator, methodCreator, - subClassContext.classCreator, subInstance); + 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): for (int i = 0; i < method.getParameters().length; i++) { - FieldDescriptor paramField = subClassContext.classCreator.getFieldCreator("param" + i, + FieldDescriptor paramField = subContext.classCreator.getFieldCreator("param" + i, method.getParameters()[i].type) .setModifiers(Modifier.PUBLIC) .getFieldDescriptor(); - methodCreator.writeInstanceField(paramField, subInstance, methodCreator.getMethodParam(i)); - paramFields.put(i, paramField); + ownerMethod.writeInstanceField(paramField, subInstance, ownerMethod.getMethodParam(i)); + subParamFields.add(new SubResourceParameter(method.getParameters()[i], method.getParameters()[i].type, + jandexMethod.parameters().get(i), paramField, methodParamAnnotationsField, methodGenericParametersField, + i)); } ResultHandle multipartForm = null; int subMethodIndex = 0; for (ResourceMethod subMethod : method.getSubResourceMethods()) { - MethodInfo jandexSubMethod = getJavaMethod(subResourceInterface, subMethod, + MethodInfo jandexSubMethod = getJavaMethod(subInterface, subMethod, subMethod.getParameters(), index) .orElseThrow(() -> new RuntimeException( "Failed to find matching java method for " + subMethod + " on " - + subResourceInterface + + subInterface + ". It may have unresolved parameter types (generics)")); subMethodIndex++; - // WebTarget field in the root stub implementation (not to recreate it on each call): - FieldDescriptor subMethodField = subClassContext.createJavaMethodField(subResourceInterface, jandexSubMethod, - subMethodIndex); - Supplier subMethodParamAnnotationsField = subClassContext - .getLazyJavaMethodParamAnnotationsField(subMethodIndex); - Supplier subMethodGenericParametersField = subClassContext - .getLazyJavaMethodGenericParametersField(subMethodIndex); - - // initializing the web target in the root stub constructor: - FieldDescriptor webTargetForSubMethod = FieldDescriptor.of(name, "target" + methodIndex + "_" + subMethodIndex, - WebTarget.class); - classContext.classCreator.getFieldCreator(webTargetForSubMethod).setModifiers(Modifier.FINAL); - webTargets.add(webTargetForSubMethod); - - AssignableResultHandle constructorTarget = createWebTargetForMethod(constructor, baseTarget, - method); - if (subMethod.getPath() != null) { - appendPath(constructor, subMethod.getPath(), constructorTarget); - } - constructor.writeInstanceField(webTargetForSubMethod, constructor.getThis(), constructorTarget); - - // set the sub stub target field value to the target created above: - FieldDescriptor subWebTarget = subClassContext.classCreator.getFieldCreator("target" + subMethodIndex, - WebTarget.class) - .setModifiers(Modifier.PUBLIC) - .getFieldDescriptor(); - methodCreator.writeInstanceField(subWebTarget, subInstance, - methodCreator.readInstanceField(webTargetForSubMethod, methodCreator.getThis())); - - MethodCreator subMethodCreator = subClassContext.classCreator.getMethodCreator(subMethod.getName(), - jandexSubMethod.returnType().name().toString(), - parametersAsStringArray(jandexSubMethod)); - - AssignableResultHandle methodTarget = subMethodCreator.createVariable(WebTarget.class); - subMethodCreator.assign(methodTarget, - subMethodCreator.readInstanceField(subWebTarget, subMethodCreator.getThis())); - - ResultHandle bodyParameterValue = null; - AssignableResultHandle formParams = null; - Map invocationBuilderEnrichers = new HashMap<>(); - - // first go through parameters of the root stub method, we have them copied to fields in the sub stub - for (int paramIdx = 0; paramIdx < method.getParameters().length; ++paramIdx) { - MethodParameter param = method.getParameters()[paramIdx]; - ResultHandle paramValue = subMethodCreator.readInstanceField(paramFields.get(paramIdx), - subMethodCreator.getThis()); - if (param.parameterType == ParameterType.QUERY) { - //TODO: converters - - // query params have to be set on a method-level web target (they vary between invocations) - subMethodCreator.assign(methodTarget, - addQueryParam(jandexMethod, subMethodCreator, methodTarget, param.name, - paramValue, jandexMethod.parameters().get(paramIdx), index, - subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - subMethodCreator.readStaticField(methodGenericParametersField.get()), - subMethodCreator.readStaticField(methodParamAnnotationsField.get()), - paramIdx)); - } else if (param.parameterType == ParameterType.BEAN) { - // bean params require both, web-target and Invocation.Builder, modifications - // The web target changes have to be done on the method level. - // Invocation.Builder changes are offloaded to a separate method - // so that we can generate bytecode for both, web target and invocation builder modifications - // at once - ClientBeanParamInfo beanParam = (ClientBeanParamInfo) param; - MethodDescriptor handleBeanParamDescriptor = MethodDescriptor.ofMethod(subName, - subMethod.getName() + "$$" + methodIndex + "$$handleBeanParam$$" + paramIdx, - Invocation.Builder.class, - Invocation.Builder.class, param.type); - MethodCreator handleBeanParamMethod = subClassContext.classCreator.getMethodCreator( - handleBeanParamDescriptor); - - AssignableResultHandle invocationBuilderRef = handleBeanParamMethod - .createVariable(Invocation.Builder.class); - handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); - formParams = addBeanParamData(jandexMethod, subMethodCreator, handleBeanParamMethod, - invocationBuilderRef, beanParam.getItems(), - paramValue, methodTarget, index, - interfaceClass.name().toString(), - subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - handleBeanParamMethod.readInstanceField(clientField, handleBeanParamMethod.getThis()), - formParams, - methodGenericParametersField, methodParamAnnotationsField, paramIdx); - - handleBeanParamMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleBeanParamDescriptor, paramValue); - } else if (param.parameterType == ParameterType.PATH) { - // methodTarget = methodTarget.resolveTemplate(paramname, paramvalue); - addPathParam(subMethodCreator, methodTarget, param.name, paramValue, - param.type, - subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - subMethodCreator.readStaticField(methodGenericParametersField.get()), - subMethodCreator.readStaticField(methodParamAnnotationsField.get()), - paramIdx); - } else if (param.parameterType == ParameterType.BODY) { - // just store the index of parameter used to create the body, we'll use it later - bodyParameterValue = paramValue; - } else if (param.parameterType == ParameterType.HEADER) { - // headers are added at the invocation builder level - MethodDescriptor handleHeaderDescriptor = MethodDescriptor.ofMethod(subName, - subMethod.getName() + "$$" + subMethodIndex + "$$handleHeader$$param" + paramIdx, - Invocation.Builder.class, - Invocation.Builder.class, param.type); - MethodCreator handleHeaderMethod = subClassContext.classCreator.getMethodCreator( - handleHeaderDescriptor); - - AssignableResultHandle invocationBuilderRef = handleHeaderMethod - .createVariable(Invocation.Builder.class); - handleHeaderMethod.assign(invocationBuilderRef, handleHeaderMethod.getMethodParam(0)); - addHeaderParam(handleHeaderMethod, invocationBuilderRef, param.name, - handleHeaderMethod.getMethodParam(1), - param.type, - handleHeaderMethod.readInstanceField(clientField, handleHeaderMethod.getThis()), - methodGenericParametersField.get(), methodParamAnnotationsField.get(), paramIdx); - handleHeaderMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleHeaderDescriptor, paramValue); - } else if (param.parameterType == ParameterType.COOKIE) { - // cookies are added at the invocation builder level - MethodDescriptor handleCookieDescriptor = MethodDescriptor.ofMethod(subName, - subMethod.getName() + "$$" + subMethodIndex + "$$handleCookie$$param" + paramIdx, - Invocation.Builder.class, - Invocation.Builder.class, param.type); - MethodCreator handleCookieMethod = subClassContext.classCreator.getMethodCreator( - handleCookieDescriptor); - - AssignableResultHandle invocationBuilderRef = handleCookieMethod - .createVariable(Invocation.Builder.class); - handleCookieMethod.assign(invocationBuilderRef, handleCookieMethod.getMethodParam(0)); - addCookieParam(handleCookieMethod, invocationBuilderRef, param.name, - handleCookieMethod.getMethodParam(1), - param.type, - handleCookieMethod.readInstanceField(clientField, handleCookieMethod.getThis()), - methodGenericParametersField.get(), methodParamAnnotationsField.get(), paramIdx); - handleCookieMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleCookieDescriptor, paramValue); - } else if (param.parameterType == ParameterType.FORM) { - formParams = createIfAbsent(subMethodCreator, formParams); - subMethodCreator.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, - subMethodCreator.load(param.name), paramValue); - } else if (param.parameterType == ParameterType.MULTI_PART_FORM) { - if (multipartForm != null) { - throw new IllegalArgumentException("MultipartForm data set twice for method " - + jandexSubMethod.declaringClass().name() + "#" + jandexSubMethod.name()); - } - multipartForm = createMultipartForm(subMethodCreator, paramValue, - jandexMethod.parameters().get(paramIdx).asClassType(), index); + boolean isSubResourceMethod = subMethod.getHttpMethod() == null; + if (!isSubResourceMethod) { + // java method data: + FieldDescriptor subMethodField = subContext.createJavaMethodField(subInterface, jandexSubMethod, + subMethodIndex); + Supplier subMethodParamAnnotationsField = subContext + .getLazyJavaMethodParamAnnotationsField(subMethodIndex); + Supplier subMethodGenericParametersField = subContext + .getLazyJavaMethodGenericParametersField(subMethodIndex); + + MethodCreator subMethodCreator = subContext.classCreator.getMethodCreator(subMethod.getName(), + jandexSubMethod.returnType().name().toString(), + parametersAsStringArray(jandexSubMethod)); + // initializing the web target in the sub constructor: + FieldDescriptor subMethodTarget = FieldDescriptor.of(subName, "target" + subMethodIndex, + WebTarget.class); + subContext.classCreator.getFieldCreator(subMethodTarget).setModifiers(Modifier.FINAL); + + AssignableResultHandle subMethodTargetV = subContext.constructor.createVariable(WebTargetImpl.class); + subContext.constructor.assign(subMethodTargetV, subContext.constructor.getMethodParam(0)); + if (subMethod.getPath() != null) { + appendPath(subContext.constructor, subMethod.getPath(), subMethodTargetV); } - } - // handle sub-method parameters: - for (int paramIdx = 0; paramIdx < subMethod.getParameters().length; ++paramIdx) { - MethodParameter param = subMethod.getParameters()[paramIdx]; - if (param.parameterType == ParameterType.QUERY) { - //TODO: converters - - // query params have to be set on a method-level web target (they vary between invocations) - subMethodCreator.assign(methodTarget, - addQueryParam(jandexMethod, subMethodCreator, methodTarget, param.name, - subMethodCreator.getMethodParam(paramIdx), - jandexSubMethod.parameters().get(paramIdx), index, - subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - subMethodCreator.readStaticField(subMethodGenericParametersField.get()), - subMethodCreator.readStaticField(subMethodParamAnnotationsField.get()), - paramIdx)); - } else if (param.parameterType == ParameterType.BEAN) { - // bean params require both, web-target and Invocation.Builder, modifications - // The web target changes have to be done on the method level. - // Invocation.Builder changes are offloaded to a separate method - // so that we can generate bytecode for both, web target and invocation builder modifications - // at once - ClientBeanParamInfo beanParam = (ClientBeanParamInfo) param; - MethodDescriptor handleBeanParamDescriptor = MethodDescriptor.ofMethod(subName, - subMethod.getName() + "$$" + subMethodIndex + "$$handleBeanParam$$" + paramIdx, - Invocation.Builder.class, - Invocation.Builder.class, param.type); - MethodCreator handleBeanParamMethod = classContext.classCreator.getMethodCreator( - handleBeanParamDescriptor); - - AssignableResultHandle invocationBuilderRef = handleBeanParamMethod - .createVariable(Invocation.Builder.class); - handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); - formParams = addBeanParamData(jandexMethod, subMethodCreator, handleBeanParamMethod, - invocationBuilderRef, beanParam.getItems(), - subMethodCreator.getMethodParam(paramIdx), methodTarget, index, - interfaceClass.name().toString(), - subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - handleBeanParamMethod.readInstanceField(clientField, handleBeanParamMethod.getThis()), - formParams, - subMethodGenericParametersField, subMethodParamAnnotationsField, paramIdx); - - handleBeanParamMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleBeanParamDescriptor, - subMethodCreator.getMethodParam(paramIdx)); - } else if (param.parameterType == ParameterType.PATH) { - addPathParam(subMethodCreator, methodTarget, param.name, - subMethodCreator.getMethodParam(paramIdx), param.type, - subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - subMethodCreator.readStaticField(subMethodGenericParametersField.get()), - subMethodCreator.readStaticField(subMethodParamAnnotationsField.get()), - paramIdx); - } else if (param.parameterType == ParameterType.BODY) { - // just store the index of parameter used to create the body, we'll use it later - bodyParameterValue = subMethodCreator.getMethodParam(paramIdx); - } else if (param.parameterType == ParameterType.HEADER) { - // headers are added at the invocation builder level - MethodDescriptor handleHeaderDescriptor = MethodDescriptor.ofMethod(subName, - subMethod.getName() + "$$" + subMethodIndex + "$$handleHeader$$" + paramIdx, - Invocation.Builder.class, - Invocation.Builder.class, param.type); - MethodCreator handleHeaderMethod = subClassContext.classCreator.getMethodCreator( - handleHeaderDescriptor); - - AssignableResultHandle invocationBuilderRef = handleHeaderMethod - .createVariable(Invocation.Builder.class); - handleHeaderMethod.assign(invocationBuilderRef, handleHeaderMethod.getMethodParam(0)); - addHeaderParam(handleHeaderMethod, invocationBuilderRef, param.name, - handleHeaderMethod.getMethodParam(1), param.type, - handleHeaderMethod.readInstanceField(clientField, handleHeaderMethod.getThis()), - subMethodGenericParametersField.get(), subMethodParamAnnotationsField.get(), paramIdx); - handleHeaderMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleHeaderDescriptor, subMethodCreator.getMethodParam(paramIdx)); - } else if (param.parameterType == ParameterType.COOKIE) { - // cookies are added at the invocation builder level - MethodDescriptor handleCookieDescriptor = MethodDescriptor.ofMethod(subName, - subMethod.getName() + "$$" + subMethodIndex + "$$handleCookie$$" + paramIdx, - Invocation.Builder.class, - Invocation.Builder.class, param.type); - MethodCreator handleCookieMethod = subClassContext.classCreator.getMethodCreator( - handleCookieDescriptor); - - AssignableResultHandle invocationBuilderRef = handleCookieMethod - .createVariable(Invocation.Builder.class); - handleCookieMethod.assign(invocationBuilderRef, handleCookieMethod.getMethodParam(0)); - addCookieParam(handleCookieMethod, invocationBuilderRef, param.name, - handleCookieMethod.getMethodParam(1), - param.type, - handleCookieMethod.readInstanceField(clientField, handleCookieMethod.getThis()), - subMethodGenericParametersField.get(), subMethodParamAnnotationsField.get(), paramIdx); - handleCookieMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleCookieDescriptor, subMethodCreator.getMethodParam(paramIdx)); - } else if (param.parameterType == ParameterType.FORM) { - formParams = createIfAbsent(subMethodCreator, formParams); - subMethodCreator.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, - subMethodCreator.load(param.name), - subMethodCreator.getMethodParam(paramIdx)); - } else if (param.parameterType == ParameterType.MULTI_PART_FORM) { - if (multipartForm != null) { - throw new IllegalArgumentException("MultipartForm data set twice for method " - + jandexSubMethod.declaringClass().name() + "#" + jandexSubMethod.name()); + + subContext.constructor.writeInstanceField(subMethodTarget, subContext.constructor.getThis(), + subMethodTargetV); + + AssignableResultHandle methodTarget = subMethodCreator.createVariable(WebTarget.class); + subMethodCreator.assign(methodTarget, + subMethodCreator.readInstanceField(subMethodTarget, subMethodCreator.getThis())); + + ResultHandle bodyParameterValue = null; + AssignableResultHandle formParams = null; + Map invocationBuilderEnrichers = new HashMap<>(); + + int inheritedParamIndex = 0; + for (SubResourceParameter subParamField : subParamFields) { + inheritedParamIndex++; + MethodParameter param = subParamField.methodParameter; + ResultHandle paramValue = subMethodCreator.readInstanceField(subParamField.field, + subMethodCreator.getThis()); + if (param.parameterType == ParameterType.QUERY) { + //TODO: converters + + // query params have to be set on a method-level web target (they vary between invocations) + subMethodCreator.assign(methodTarget, + addQueryParam(jandexMethod, subMethodCreator, methodTarget, param.name, + paramValue, subParamField.type, index, + subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), + subMethodCreator.readStaticField(subParamField.genericsParametersField.get()), + subMethodCreator.readStaticField(subParamField.paramAnnotationsField.get()), + subParamField.paramIndex)); + } else if (param.parameterType == ParameterType.BEAN) { + // bean params require both, web-target and Invocation.Builder, modifications + // The web target changes have to be done on the method level. + // Invocation.Builder changes are offloaded to a separate method + // so that we can generate bytecode for both, web target and invocation builder modifications + // at once + ClientBeanParamInfo beanParam = (ClientBeanParamInfo) param; + MethodDescriptor handleBeanParamDescriptor = MethodDescriptor.ofMethod(subName, + subMethod.getName() + "$$" + methodIndex + "$$handleBeanParam$$" + inheritedParamIndex + + "$" + subParamField.paramIndex, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleBeanParamMethod = subContext.classCreator.getMethodCreator( + handleBeanParamDescriptor); + + AssignableResultHandle invocationBuilderRef = handleBeanParamMethod + .createVariable(Invocation.Builder.class); + handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); + formParams = addBeanParamData(jandexMethod, subMethodCreator, handleBeanParamMethod, + invocationBuilderRef, beanParam.getItems(), + paramValue, methodTarget, index, + interfaceClass.name().toString(), + subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), + handleBeanParamMethod.readInstanceField(clientField, handleBeanParamMethod.getThis()), + formParams, + methodGenericParametersField, methodParamAnnotationsField, subParamField.paramIndex); + + handleBeanParamMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleBeanParamDescriptor, paramValue); + } else if (param.parameterType == ParameterType.PATH) { + // methodTarget = methodTarget.resolveTemplate(paramname, paramvalue); + addPathParam(subMethodCreator, methodTarget, param.name, paramValue, + param.type, + subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), + subMethodCreator.readStaticField(subParamField.genericsParametersField.get()), + subMethodCreator.readStaticField(subParamField.paramAnnotationsField.get()), + subParamField.paramIndex); + } else if (param.parameterType == ParameterType.BODY) { + // just store the index of parameter used to create the body, we'll use it later + bodyParameterValue = paramValue; + } else if (param.parameterType == ParameterType.HEADER) { + // headers are added at the invocation builder level + MethodDescriptor handleHeaderDescriptor = MethodDescriptor.ofMethod(subName, + subMethod.getName() + "$$" + subMethodIndex + "$$handleHeader$$param" + + inheritedParamIndex + "$" + subParamField.paramIndex, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleHeaderMethod = subContext.classCreator.getMethodCreator( + handleHeaderDescriptor); + + AssignableResultHandle invocationBuilderRef = handleHeaderMethod + .createVariable(Invocation.Builder.class); + handleHeaderMethod.assign(invocationBuilderRef, handleHeaderMethod.getMethodParam(0)); + addHeaderParam(handleHeaderMethod, invocationBuilderRef, param.name, + handleHeaderMethod.getMethodParam(1), + param.type, + handleHeaderMethod.readInstanceField(clientField, handleHeaderMethod.getThis()), + subParamField.genericsParametersField.get(), + subParamField.paramAnnotationsField.get(), + subParamField.paramIndex); + handleHeaderMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleHeaderDescriptor, paramValue); + } else if (param.parameterType == ParameterType.COOKIE) { + // cookies are added at the invocation builder level + MethodDescriptor handleCookieDescriptor = MethodDescriptor.ofMethod(subName, + subMethod.getName() + "$$" + subMethodIndex + "$$handleCookie$$param" + + inheritedParamIndex + "$" + subParamField.paramIndex, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleCookieMethod = subContext.classCreator.getMethodCreator( + handleCookieDescriptor); + + AssignableResultHandle invocationBuilderRef = handleCookieMethod + .createVariable(Invocation.Builder.class); + handleCookieMethod.assign(invocationBuilderRef, handleCookieMethod.getMethodParam(0)); + addCookieParam(handleCookieMethod, invocationBuilderRef, param.name, + handleCookieMethod.getMethodParam(1), + param.type, + handleCookieMethod.readInstanceField(clientField, handleCookieMethod.getThis()), + subParamField.genericsParametersField.get(), + subParamField.paramAnnotationsField.get(), + subParamField.paramIndex); + handleCookieMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleCookieDescriptor, paramValue); + } else if (param.parameterType == ParameterType.FORM) { + formParams = createIfAbsent(subMethodCreator, formParams); + subMethodCreator.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, + subMethodCreator.load(param.name), paramValue); + } else if (param.parameterType == ParameterType.MULTI_PART_FORM) { + if (multipartForm != null) { + throw new IllegalArgumentException("MultipartForm data set twice for method " + + jandexSubMethod.declaringClass().name() + "#" + jandexSubMethod.name()); + } + multipartForm = createMultipartForm(subMethodCreator, paramValue, subParamField.type, index); } - multipartForm = createMultipartForm(subMethodCreator, - subMethodCreator.getMethodParam(paramIdx), - jandexSubMethod.parameters().get(paramIdx), index); } + // handle sub-method parameters: + for (int paramIdx = 0; paramIdx < subMethod.getParameters().length; ++paramIdx) { + MethodParameter param = subMethod.getParameters()[paramIdx]; + if (param.parameterType == ParameterType.QUERY) { + //TODO: converters - } + // query params have to be set on a method-level web target (they vary between invocations) + subMethodCreator.assign(methodTarget, + addQueryParam(jandexMethod, subMethodCreator, methodTarget, param.name, + subMethodCreator.getMethodParam(paramIdx), + jandexSubMethod.parameters().get(paramIdx), index, + subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), + subMethodCreator.readStaticField(subMethodGenericParametersField.get()), + subMethodCreator.readStaticField(subMethodParamAnnotationsField.get()), + paramIdx)); + } else if (param.parameterType == ParameterType.BEAN) { + // bean params require both, web-target and Invocation.Builder, modifications + // The web target changes have to be done on the method level. + // Invocation.Builder changes are offloaded to a separate method + // so that we can generate bytecode for both, web target and invocation builder modifications + // at once + ClientBeanParamInfo beanParam = (ClientBeanParamInfo) param; + MethodDescriptor handleBeanParamDescriptor = MethodDescriptor.ofMethod(subName, + subMethod.getName() + "$$" + subMethodIndex + "$$handleBeanParam$$" + paramIdx, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleBeanParamMethod = ownerContext.classCreator.getMethodCreator( + handleBeanParamDescriptor); + + AssignableResultHandle invocationBuilderRef = handleBeanParamMethod + .createVariable(Invocation.Builder.class); + handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); + formParams = addBeanParamData(jandexMethod, subMethodCreator, handleBeanParamMethod, + invocationBuilderRef, beanParam.getItems(), + subMethodCreator.getMethodParam(paramIdx), methodTarget, index, + interfaceClass.name().toString(), + subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), + handleBeanParamMethod.readInstanceField(clientField, handleBeanParamMethod.getThis()), + formParams, + subMethodGenericParametersField, subMethodParamAnnotationsField, paramIdx); + + handleBeanParamMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleBeanParamDescriptor, + subMethodCreator.getMethodParam(paramIdx)); + } else if (param.parameterType == ParameterType.PATH) { + addPathParam(subMethodCreator, methodTarget, param.name, + subMethodCreator.getMethodParam(paramIdx), param.type, + subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), + subMethodCreator.readStaticField(subMethodGenericParametersField.get()), + subMethodCreator.readStaticField(subMethodParamAnnotationsField.get()), + paramIdx); + } else if (param.parameterType == ParameterType.BODY) { + // just store the index of parameter used to create the body, we'll use it later + bodyParameterValue = subMethodCreator.getMethodParam(paramIdx); + } else if (param.parameterType == ParameterType.HEADER) { + // headers are added at the invocation builder level + MethodDescriptor handleHeaderDescriptor = MethodDescriptor.ofMethod(subName, + subMethod.getName() + "$$" + subMethodIndex + "$$handleHeader$$" + paramIdx, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleHeaderMethod = subContext.classCreator.getMethodCreator( + handleHeaderDescriptor); + + AssignableResultHandle invocationBuilderRef = handleHeaderMethod + .createVariable(Invocation.Builder.class); + handleHeaderMethod.assign(invocationBuilderRef, handleHeaderMethod.getMethodParam(0)); + addHeaderParam(handleHeaderMethod, invocationBuilderRef, param.name, + handleHeaderMethod.getMethodParam(1), param.type, + handleHeaderMethod.readInstanceField(clientField, handleHeaderMethod.getThis()), + subMethodGenericParametersField.get(), subMethodParamAnnotationsField.get(), paramIdx); + handleHeaderMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleHeaderDescriptor, subMethodCreator.getMethodParam(paramIdx)); + } else if (param.parameterType == ParameterType.COOKIE) { + // cookies are added at the invocation builder level + MethodDescriptor handleCookieDescriptor = MethodDescriptor.ofMethod(subName, + subMethod.getName() + "$$" + subMethodIndex + "$$handleCookie$$" + paramIdx, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleCookieMethod = subContext.classCreator.getMethodCreator( + handleCookieDescriptor); + + AssignableResultHandle invocationBuilderRef = handleCookieMethod + .createVariable(Invocation.Builder.class); + handleCookieMethod.assign(invocationBuilderRef, handleCookieMethod.getMethodParam(0)); + addCookieParam(handleCookieMethod, invocationBuilderRef, param.name, + handleCookieMethod.getMethodParam(1), + param.type, + handleCookieMethod.readInstanceField(clientField, handleCookieMethod.getThis()), + subMethodGenericParametersField.get(), subMethodParamAnnotationsField.get(), paramIdx); + handleCookieMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleCookieDescriptor, subMethodCreator.getMethodParam(paramIdx)); + } else if (param.parameterType == ParameterType.FORM) { + formParams = createIfAbsent(subMethodCreator, formParams); + subMethodCreator.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, + subMethodCreator.load(param.name), + subMethodCreator.getMethodParam(paramIdx)); + } else if (param.parameterType == ParameterType.MULTI_PART_FORM) { + if (multipartForm != null) { + throw new IllegalArgumentException("MultipartForm data set twice for method " + + jandexSubMethod.declaringClass().name() + "#" + jandexSubMethod.name()); + } + multipartForm = createMultipartForm(subMethodCreator, + subMethodCreator.getMethodParam(paramIdx), + jandexSubMethod.parameters().get(paramIdx), index); + } - if (subMethod.getHttpMethod() == null) { - // finding corresponding jandex method, used by enricher (MicroProfile enricher stores it in a field - // to later fill in context with corresponding java.lang.reflect.Method) - String[] subJavaMethodParameters = new String[subMethod.getParameters().length]; - for (int i = 0; i < subMethod.getParameters().length; i++) { - MethodParameter param = subMethod.getParameters()[i]; - subJavaMethodParameters[i] = param.declaredType != null ? param.declaredType : param.type; } - handleSubResourceMethod(enrichers, generatedClasses, subResourceInterface, index, - defaultMediaType, httpAnnotationToMethod, - subName, subClassContext, subMethodCreator, - methodTarget, subMethodIndex, webTargets, subMethod, subJavaMethodParameters, jandexSubMethod, - multipartResponseTypes); - } else { // if the response is multipart, let's add it's class to the appropriate collection: addResponseTypeIfMultipart(multipartResponseTypes, jandexSubMethod, index); @@ -1396,30 +1407,44 @@ private void handleSubResourceMethod(List for (JaxrsClientReactiveEnricherBuildItem enricher : enrichers) { enricher.getEnricher() - .forSubResourceMethod(subClassContext.classCreator, subClassContext.constructor, - subClassContext.clinit, subMethodCreator, interfaceClass, - subResourceInterface, jandexSubMethod, jandexMethod, builder, index, + .forSubResourceMethod(subContext.classCreator, subContext.constructor, + subContext.clinit, subMethodCreator, interfaceClass, + subInterface, jandexSubMethod, jandexMethod, builder, index, generatedClasses, methodIndex, subMethodIndex, subMethodField); } String[] consumes = extractProducesConsumesValues( jandexSubMethod.declaringClass().classAnnotation(CONSUMES), method.getConsumes()); consumes = extractProducesConsumesValues(jandexSubMethod.annotation(CONSUMES), consumes); - handleReturn(subResourceInterface, defaultMediaType, + handleReturn(subInterface, defaultMediaType, getHttpMethod(jandexSubMethod, subMethod.getHttpMethod(), httpAnnotationToMethod), consumes, jandexSubMethod, subMethodCreator, formParams, multipartForm, bodyParameterValue, builder); + } else { + // finding corresponding jandex method, used by enricher (MicroProfile enricher stores it in a field + // to later fill in context with corresponding java.lang.reflect.Method) + String[] subJavaMethodParameters = new String[subMethod.getParameters().length]; + for (int i = 0; i < subMethod.getParameters().length; i++) { + MethodParameter param = subMethod.getParameters()[i]; + subJavaMethodParameters[i] = param.declaredType != null ? param.declaredType : param.type; + } + ResultHandle subMethodTarget = subContext.constructor.getMethodParam(0); + handleSubResourceMethod(enrichers, generatedClasses, subInterface, index, + defaultMediaType, httpAnnotationToMethod, subName, subContext, subMethodTarget, + subMethodIndex, subMethod, subJavaMethodParameters, jandexSubMethod, + multipartResponseTypes, subParamFields); } + } - subClassContext.constructor.returnValue(null); - subClassContext.clinit.returnValue(null); + subContext.constructor.returnValue(null); + subContext.clinit.returnValue(null); - methodCreator.returnValue(subInstance); + ownerMethod.returnValue(subInstance); } } - private AssignableResultHandle createWebTargetForMethod(MethodCreator constructor, AssignableResultHandle baseTarget, + private AssignableResultHandle createWebTargetForMethod(MethodCreator constructor, ResultHandle baseTarget, ResourceMethod method) { AssignableResultHandle target = constructor.createVariable(WebTarget.class); constructor.assign(target, baseTarget); @@ -1853,9 +1878,7 @@ private void handleReturn(ClassInfo restClientInterface, String defaultMediaType continuationIndex = parameters.size() - 1; returnCategory = ReturnCategory.COROUTINE; - try { - Thread.currentThread().getContextClassLoader().loadClass(UNI_KT.toString()); - } catch (ClassNotFoundException e) { + if (!QuarkusClassLoader.isClassPresentAtRuntime(UNI_KT.toString())) { //TODO: make this automatic somehow throw new RuntimeException("Suspendable rest client method" + jandexMethod + " is present on class " + jandexMethod.declaringClass() @@ -2344,8 +2367,7 @@ private ResultHandle addQueryParam(MethodInfo jandexMethod, BytecodeCreator meth } // get the new WebTarget addQueryParamToWebTarget(loopCreator, key, result, client, genericType, paramAnnotations, - paramIndex, - paramArray, componentType, result); + paramIndex, paramArray, componentType, result); } else { ResultHandle paramArray; String componentType = null; @@ -2546,4 +2568,30 @@ private enum ReturnCategory { COROUTINE } + private static class SubResourceParameter { + final MethodParameter methodParameter; + final String typeName; + + final Type type; + final FieldDescriptor field; + + final Supplier paramAnnotationsField; + + final Supplier genericsParametersField; + + final int paramIndex; + + private SubResourceParameter(MethodParameter methodParameter, String typeName, Type type, FieldDescriptor field, + Supplier paramAnnotationsField, Supplier genericsParametersField, + int paramIndex) { + this.methodParameter = methodParameter; + this.typeName = typeName; + this.type = type; + this.field = field; + this.paramAnnotationsField = paramAnnotationsField; + this.genericsParametersField = genericsParametersField; + this.paramIndex = paramIndex; + } + } + } diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/resources/META-INF/services/javax.ws.rs.client.ClientBuilder b/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/resources/META-INF/services/javax.ws.rs.client.ClientBuilder new file mode 100644 index 0000000000000..2c6aca785cad8 --- /dev/null +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/resources/META-INF/services/javax.ws.rs.client.ClientBuilder @@ -0,0 +1 @@ +org.jboss.resteasy.reactive.client.impl.ClientBuilderImpl diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/BasicServerJacksonMessageBodyWriter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/BasicServerJacksonMessageBodyWriter.java index 9ea88c452de67..c45a76407a6fb 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/BasicServerJacksonMessageBodyWriter.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/BasicServerJacksonMessageBodyWriter.java @@ -2,7 +2,6 @@ import static org.jboss.resteasy.reactive.server.jackson.JacksonMessageBodyWriterUtil.createDefaultWriter; import static org.jboss.resteasy.reactive.server.jackson.JacksonMessageBodyWriterUtil.doLegacyWrite; -import static org.jboss.resteasy.reactive.server.providers.serialisers.json.JsonMessageServerBodyWriterUtil.setContentTypeIfNecessary; import java.io.IOException; import java.io.OutputStream; @@ -33,7 +32,6 @@ public BasicServerJacksonMessageBodyWriter(ObjectMapper mapper) { @Override public void writeResponse(Object o, Type genericType, ServerRequestContext context) throws WebApplicationException, IOException { - setContentTypeIfNecessary(context); OutputStream stream = context.getOrCreateOutputStream(); if (o instanceof String) { // YUK: done in order to avoid adding extra quotes... stream.write(((String) o).getBytes(StandardCharsets.UTF_8)); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java index ed1afa6ad0893..3afb3d88302ae 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java @@ -3,7 +3,6 @@ import static org.jboss.resteasy.reactive.server.jackson.JacksonMessageBodyWriterUtil.createDefaultWriter; import static org.jboss.resteasy.reactive.server.jackson.JacksonMessageBodyWriterUtil.doLegacyWrite; import static org.jboss.resteasy.reactive.server.jackson.JacksonMessageBodyWriterUtil.setNecessaryJsonFactoryConfig; -import static org.jboss.resteasy.reactive.server.providers.serialisers.json.JsonMessageServerBodyWriterUtil.setContentTypeIfNecessary; import java.io.IOException; import java.io.OutputStream; @@ -44,7 +43,6 @@ public FullyFeaturedServerJacksonMessageBodyWriter(ObjectMapper mapper) { @Override public void writeResponse(Object o, Type genericType, ServerRequestContext context) throws WebApplicationException, IOException { - setContentTypeIfNecessary(context); OutputStream stream = context.getOrCreateOutputStream(); if (o instanceof String) { // YUK: done in order to avoid adding extra quotes... stream.write(((String) o).getBytes(StandardCharsets.UTF_8)); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/kotlin/serialization/KotlinSerializationMessageBodyWriter.kt b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/kotlin/serialization/KotlinSerializationMessageBodyWriter.kt index c74c5f3b24d26..670449695565d 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/kotlin/serialization/KotlinSerializationMessageBodyWriter.kt +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin-serialization/runtime/src/main/kotlin/io/quarkus/kotlin/serialization/KotlinSerializationMessageBodyWriter.kt @@ -5,7 +5,6 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.encodeToStream import kotlinx.serialization.serializer import org.jboss.resteasy.reactive.common.providers.serialisers.JsonMessageBodyWriterUtil -import org.jboss.resteasy.reactive.server.providers.serialisers.json.JsonMessageServerBodyWriterUtil import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter.AllWriteableMessageBodyWriter import org.jboss.resteasy.reactive.server.spi.ServerRequestContext import java.io.OutputStream @@ -32,7 +31,6 @@ class KotlinSerializationMessageBodyWriter(private val json: Json) : AllWriteabl } override fun writeResponse(o: Any, genericType: Type, context: ServerRequestContext) { - JsonMessageServerBodyWriterUtil.setContentTypeIfNecessary(context) val originalStream = context.orCreateOutputStream val stream: OutputStream = NoopCloseAndFlushOutputStream(originalStream) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java index 853827a7e0877..b85bb99f3d4d6 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java @@ -1,14 +1,26 @@ package io.quarkus.resteasy.reactive.links.deployment; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COMPLETABLE_FUTURE; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COMPLETION_STAGE; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_RESPONSE; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.UNI; + +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; +import javax.ws.rs.HttpMethod; import javax.ws.rs.core.UriBuilder; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; import org.jboss.resteasy.reactive.common.model.ResourceMethod; import org.jboss.resteasy.reactive.common.util.URLUtils; @@ -20,10 +32,16 @@ final class LinksContainerFactory { + private static final String LIST = "list"; + private static final String SELF = "self"; + private static final String REMOVE = "remove"; + private static final String UPDATE = "update"; + private static final String ADD = "add"; + /** * Find the resource methods that are marked with a {@link RestLink} annotations and add them to a links container. */ - LinksContainer getLinksContainer(List entries) { + LinksContainer getLinksContainer(List entries, IndexView index) { LinksContainer linksContainer = new LinksContainer(); for (ResteasyReactiveResourceMethodEntriesBuildItem.Entry entry : entries) { @@ -31,7 +49,7 @@ LinksContainer getLinksContainer(List + * Otherwise, it will return the method name. + * + * @param resourceMethod the resource method definition. + * @return the deducted rel property. + */ + private String deductRel(ResourceMethod resourceMethod, Type returnType, IndexView index) { + String httpMethod = resourceMethod.getHttpMethod(); + boolean isCollection = isCollection(returnType, index); + if (HttpMethod.GET.equals(httpMethod) && isCollection) { + return LIST; + } else if (HttpMethod.GET.equals(httpMethod)) { + return SELF; + } else if (HttpMethod.DELETE.equals(httpMethod)) { + return REMOVE; + } else if (HttpMethod.PUT.equals(httpMethod)) { + return UPDATE; + } else if (HttpMethod.POST.equals(httpMethod)) { + return ADD; + } + + return resourceMethod.getName(); + } + /** * If a method return type is parameterized and has a single argument (e.g. List), then use that argument as an * entity type. Otherwise, use the return type. */ - private String deductEntityType(MethodInfo methodInfo) { - if (methodInfo.returnType().kind() == Type.Kind.PARAMETERIZED_TYPE) { - if (methodInfo.returnType().asParameterizedType().arguments().size() == 1) { - return methodInfo.returnType().asParameterizedType().arguments().get(0).name().toString(); + private String deductEntityType(Type returnType) { + if (returnType.kind() == Type.Kind.PARAMETERIZED_TYPE) { + if (returnType.asParameterizedType().arguments().size() == 1) { + return returnType.asParameterizedType().arguments().get(0).name().toString(); } } - return methodInfo.returnType().name().toString(); + return returnType.name().toString(); } /** @@ -85,4 +135,38 @@ private String getAnnotationValue(AnnotationInstance annotationInstance, String } return value.asString(); } + + private boolean isCollection(Type type, IndexView index) { + if (type.kind() == Type.Kind.PRIMITIVE) { + return false; + } + ClassInfo classInfo = index.getClassByName(type.name()); + if (classInfo == null) { + return false; + } + return classInfo.interfaceNames().stream().anyMatch(DotName.createSimple(Collection.class.getName())::equals); + } + + private Type getNonAsyncReturnType(Type returnType) { + switch (returnType.kind()) { + case ARRAY: + case CLASS: + case PRIMITIVE: + case VOID: + return returnType; + case PARAMETERIZED_TYPE: + // NOTE: same code in RuntimeResourceDeployment.getNonAsyncReturnType + ParameterizedType parameterizedType = returnType.asParameterizedType(); + if (COMPLETION_STAGE.equals(parameterizedType.name()) + || COMPLETABLE_FUTURE.equals(parameterizedType.name()) + || UNI.equals(parameterizedType.name()) + || MULTI.equals(parameterizedType.name()) + || REST_RESPONSE.equals(parameterizedType.name())) { + return parameterizedType.arguments().get(0); + } + return returnType; + default: + } + return returnType; + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java index 997faa96df525..0fec6704edecc 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java @@ -1,6 +1,7 @@ package io.quarkus.resteasy.reactive.links.deployment; import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OBJECT_NAME; import java.util.HashSet; import java.util.List; @@ -12,6 +13,8 @@ import org.jboss.jandex.IndexView; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.annotations.BuildProducer; @@ -28,9 +31,11 @@ import io.quarkus.resteasy.reactive.links.runtime.LinksContainer; import io.quarkus.resteasy.reactive.links.runtime.LinksProviderRecorder; import io.quarkus.resteasy.reactive.links.runtime.RestLinksProviderProducer; -import io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveDeploymentInfoBuildItem; +import io.quarkus.resteasy.reactive.links.runtime.hal.HalServerResponseFilter; +import io.quarkus.resteasy.reactive.links.runtime.hal.ResteasyReactiveHalService; import io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveResourceMethodEntriesBuildItem; import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; +import io.quarkus.resteasy.reactive.spi.CustomContainerResponseFilterBuildItem; import io.quarkus.runtime.RuntimeValue; final class LinksProcessor { @@ -50,7 +55,6 @@ MethodScannerBuildItem linksSupport() { @BuildStep @Record(STATIC_INIT) void initializeLinksProvider(JaxRsResourceIndexBuildItem indexBuildItem, - ResteasyReactiveDeploymentInfoBuildItem deploymentInfoBuildItem, ResteasyReactiveResourceMethodEntriesBuildItem resourceMethodEntriesBuildItem, BuildProducer bytecodeTransformersProducer, BuildProducer generatedClassesProducer, @@ -60,7 +64,7 @@ void initializeLinksProvider(JaxRsResourceIndexBuildItem indexBuildItem, ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClassesProducer, true); // Initialize links container - LinksContainer linksContainer = getLinksContainer(deploymentInfoBuildItem, resourceMethodEntriesBuildItem); + LinksContainer linksContainer = getLinksContainer(resourceMethodEntriesBuildItem, index); // Implement getters to access link path parameter values RuntimeValue getterAccessorsContainer = implementPathParameterValueGetters( index, classOutput, linksContainer, getterAccessorsContainerRecorder, bytecodeTransformersProducer); @@ -74,11 +78,28 @@ AdditionalBeanBuildItem registerRestLinksProviderProducer() { return AdditionalBeanBuildItem.unremovableOf(RestLinksProviderProducer.class); } - private LinksContainer getLinksContainer(ResteasyReactiveDeploymentInfoBuildItem deploymentInfoBuildItem, - ResteasyReactiveResourceMethodEntriesBuildItem resourceMethodEntriesBuildItem) { + @BuildStep + void addHalSupport(Capabilities capabilities, BuildProducer customResponseFilters, + BuildProducer additionalBeans) { + boolean isHalSupported = capabilities.isPresent(Capability.HAL); + if (isHalSupported) { + if (!capabilities.isPresent(Capability.RESTEASY_REACTIVE_JSON_JSONB) && !capabilities.isPresent( + Capability.RESTEASY_REACTIVE_JSON_JACKSON)) { + throw new IllegalStateException("Cannot generate HAL endpoints without " + + "either 'quarkus-resteasy-reactive-jsonb' or 'quarkus-resteasy-reactive-jackson'"); + } + + customResponseFilters.produce( + new CustomContainerResponseFilterBuildItem(HalServerResponseFilter.class.getName())); + + additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(ResteasyReactiveHalService.class)); + } + } + + private LinksContainer getLinksContainer(ResteasyReactiveResourceMethodEntriesBuildItem resourceMethodEntriesBuildItem, + IndexView index) { LinksContainerFactory linksContainerFactory = new LinksContainerFactory(); - return linksContainerFactory.getLinksContainer( - resourceMethodEntriesBuildItem.getEntries()); + return linksContainerFactory.getLinksContainer(resourceMethodEntriesBuildItem.getEntries(), index); } /** @@ -97,7 +118,7 @@ private RuntimeValue implementPathParameterValueGetter String entityType = linkInfo.getEntityType(); for (String parameterName : linkInfo.getPathParameters()) { // We implement a getter inside a class that has the required field. - // We later map that getter's accessor with a entity type. + // We later map that getter's accessor with an entity type. // If a field is inside a parent class, the getter accessor will be mapped to each subclass which // has REST links that need access to that field. FieldInfo fieldInfo = getFieldInfo(index, DotName.createSimple(entityType), parameterName); @@ -140,7 +161,7 @@ private FieldInfo getFieldInfo(IndexView index, DotName className, String fieldN if (fieldInfo != null) { return fieldInfo; } - if (classInfo.superName() != null) { + if (classInfo.superName() != null && !classInfo.superName().equals(OBJECT_NAME)) { return getFieldInfo(index, classInfo.superName(), fieldName); } throw new RuntimeException(String.format("Class '%s' field '%s' was not found", className, fieldName)); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java new file mode 100644 index 0000000000000..5245b1fcec275 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java @@ -0,0 +1,31 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; + +import org.jboss.resteasy.reactive.common.util.RestMediaType; +import org.junit.jupiter.api.Test; + +import io.restassured.response.Response; + +public abstract class AbstractHalLinksTest { + + @Test + void shouldGetHalLinksForCollections() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records") + .thenReturn(); + + assertThat(response.body().jsonPath().getList("_embedded.items.id")).containsOnly(1, 2); + assertThat(response.body().jsonPath().getString("_links.list.href")).endsWith("/records"); + } + + @Test + void shouldGetHalLinksForInstance() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records/1") + .thenReturn(); + + assertThat(response.body().jsonPath().getString("_links.list.href")).endsWith("/records"); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJacksonTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJacksonTest.java new file mode 100644 index 0000000000000..c12da64361dee --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJacksonTest.java @@ -0,0 +1,21 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import java.util.Arrays; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.builder.Version; +import io.quarkus.test.QuarkusProdModeTest; + +public class HalLinksWithJacksonTest extends AbstractHalLinksTest { + @RegisterExtension + static final QuarkusProdModeTest TEST = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(AbstractEntity.class, TestRecord.class, TestResource.class)) + .setForcedDependencies( + Arrays.asList( + new AppArtifact("io.quarkus", "quarkus-resteasy-reactive-jackson", Version.getVersion()), + new AppArtifact("io.quarkus", "quarkus-hal", Version.getVersion()))) + .setRun(true); +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJsonbTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJsonbTest.java new file mode 100644 index 0000000000000..f898a58125e98 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJsonbTest.java @@ -0,0 +1,22 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import java.util.Arrays; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.builder.Version; +import io.quarkus.test.QuarkusProdModeTest; + +public class HalLinksWithJsonbTest extends AbstractHalLinksTest { + @RegisterExtension + static final QuarkusProdModeTest TEST = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(AbstractEntity.class, TestRecord.class, TestResource.class)) + .setForcedDependencies( + Arrays.asList( + new AppArtifact("io.quarkus", "quarkus-resteasy-reactive-jsonb", Version.getVersion()), + new AppArtifact("io.quarkus", "quarkus-hal", Version.getVersion()))) + .setRun(true); + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksInjectionTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksInjectionTest.java index 8232d1fe82a3b..3efd8a338d866 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksInjectionTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksInjectionTest.java @@ -35,9 +35,9 @@ void shouldGetById() { .getValues("Link"); assertThat(firstRecordLinks).containsOnly( Link.fromUri(recordsUrl).rel("list").build().toString(), - Link.fromUri(recordsWithoutLinksUrl).rel("getAllWithoutLinks").build().toString(), + Link.fromUri(recordsWithoutLinksUrl).rel("list-without-links").build().toString(), Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/1")).rel("self").build().toString(), - Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/first")).rel("getBySlug").build().toString()); + Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/first")).rel("get-by-slug").build().toString()); List secondRecordLinks = when().get(recordsUrl + "/2") .thenReturn() @@ -45,10 +45,10 @@ void shouldGetById() { .getValues("Link"); assertThat(secondRecordLinks).containsOnly( Link.fromUri(recordsUrl).rel("list").build().toString(), - Link.fromUri(recordsWithoutLinksUrl).rel("getAllWithoutLinks").build().toString(), + Link.fromUri(recordsWithoutLinksUrl).rel("list-without-links").build().toString(), Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/2")).rel("self").build().toString(), Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/second")) - .rel("getBySlug") + .rel("get-by-slug") .build() .toString()); } @@ -61,9 +61,9 @@ void shouldGetBySlug() { .getValues("Link"); assertThat(firstRecordLinks).containsOnly( Link.fromUri(recordsUrl).rel("list").build().toString(), - Link.fromUri(recordsWithoutLinksUrl).rel("getAllWithoutLinks").build().toString(), + Link.fromUri(recordsWithoutLinksUrl).rel("list-without-links").build().toString(), Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/1")).rel("self").build().toString(), - Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/first")).rel("getBySlug").build().toString()); + Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/first")).rel("get-by-slug").build().toString()); List secondRecordLinks = when().get(recordsUrl + "/second") .thenReturn() @@ -71,10 +71,10 @@ void shouldGetBySlug() { .getValues("Link"); assertThat(secondRecordLinks).containsOnly( Link.fromUri(recordsUrl).rel("list").build().toString(), - Link.fromUri(recordsWithoutLinksUrl).rel("getAllWithoutLinks").build().toString(), + Link.fromUri(recordsWithoutLinksUrl).rel("list-without-links").build().toString(), Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/2")).rel("self").build().toString(), Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/second")) - .rel("getBySlug") + .rel("get-by-slug") .build() .toString()); } @@ -87,7 +87,7 @@ void shouldGetAll() { .getValues("Link"); assertThat(links).containsOnly( Link.fromUri(recordsUrl).rel("list").build().toString(), - Link.fromUri(recordsWithoutLinksUrl).rel("getAllWithoutLinks").build().toString()); + Link.fromUri(recordsWithoutLinksUrl).rel("list-without-links").build().toString()); } @Test diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java index d5102e3fdbd08..3ffb09372ab05 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java @@ -1,5 +1,6 @@ package io.quarkus.resteasy.reactive.links.deployment; +import java.time.Duration; import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -12,9 +13,12 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; +import org.jboss.resteasy.reactive.common.util.RestMediaType; + import io.quarkus.resteasy.reactive.links.InjectRestLinks; import io.quarkus.resteasy.reactive.links.RestLink; import io.quarkus.resteasy.reactive.links.RestLinkType; +import io.smallrye.mutiny.Uni; @Path("/records") public class TestResource { @@ -26,25 +30,25 @@ public class TestResource { new TestRecord(ID_COUNTER.incrementAndGet(), "second", "Second value"))); @GET - @Produces(MediaType.APPLICATION_JSON) - @RestLink(entityType = TestRecord.class, rel = "list") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(entityType = TestRecord.class) @InjectRestLinks - public List getAll() { - return RECORDS; + public Uni> getAll() { + return Uni.createFrom().item(RECORDS).onItem().delayIt().by(Duration.ofMillis(100)); } @GET @Path("/without-links") @Produces(MediaType.APPLICATION_JSON) - @RestLink + @RestLink(rel = "list-without-links") public List getAllWithoutLinks() { return RECORDS; } @GET @Path("/{id: \\d+}") - @Produces(MediaType.APPLICATION_JSON) - @RestLink(entityType = TestRecord.class, rel = "self") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(entityType = TestRecord.class) @InjectRestLinks(RestLinkType.INSTANCE) public TestRecord getById(@PathParam("id") int id) { return RECORDS.stream() @@ -56,7 +60,7 @@ public TestRecord getById(@PathParam("id") int id) { @GET @Path("/{slug: [a-zA-Z-]+}") @Produces(MediaType.APPLICATION_JSON) - @RestLink + @RestLink(rel = "get-by-slug") @InjectRestLinks(RestLinkType.INSTANCE) public TestRecord getBySlug(@PathParam("slug") String slug) { return RECORDS.stream() diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/pom.xml index e42ff8e8c38b1..fdfb35a79cecb 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/pom.xml +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/pom.xml @@ -18,6 +18,12 @@ io.quarkus quarkus-resteasy-reactive + + + io.quarkus + quarkus-hal + true + diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/InjectRestLinks.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/InjectRestLinks.java index ca8ca0f0ed289..b2f23a560d4df 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/InjectRestLinks.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/InjectRestLinks.java @@ -5,9 +5,23 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Inject web links into the response HTTP headers with the "Link" header field. + * Only the response of the REST methods annotated with {@link RestLink} will include the "Link" headers. + *

+ * The InjectRestLinks annotation can be used at either class or method levels. + *

+ * + * @see RFC 5988 Web Linking Standard + */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) public @interface InjectRestLinks { + /** + * Find all the types available in {@link RestLinkType}. + * + * @return what types of links will be injected. + */ RestLinkType value() default RestLinkType.TYPE; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java index 6f6b836a3d9ce..cfa5316efb180 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java @@ -5,11 +5,31 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Represents a Web link to be incorporated into the HTTP response. + * Only the response of methods or classes annotated with {@link InjectRestLinks} will include the "Link" headers. + *

+ * The RestLink annotation can be used at method level. + *

+ * + * @see RFC 5988 Web Linking Standard + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RestLink { + /** + * If not set, it will default to the method name. + * + * @return the link relation. + */ String rel() default ""; + /** + * Declares a link for the given type of resources. + * If not set, it will default to the returning type of the annotated method. + * + * @return the type of returning method. + */ Class entityType() default Object.class; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkType.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkType.java index 5a23ed33ca821..bd1e356b8ad46 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkType.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkType.java @@ -1,6 +1,81 @@ package io.quarkus.resteasy.reactive.links; +/** + * Manage the link types to be injected in the Web links. + */ public enum RestLinkType { + /** + * It will inject the links that return the link type {@link RestLink#entityType()} without filtering or searching. + * For example: + * + *

+     *     @GET
+     *     @Path("/records")
+     *     @RestLink(rel = "list")
+     *     @InjectRestLinks(RestLinkType.TYPE)
+     *     public List getAll() { // ... }
+     *
+     *     @GET
+     *     @Path("/records/valid")
+     *     @RestLink
+     *     public List getValidRecords() { // ... }
+     *
+     *     @GET
+     *     @Path("/records/{id}")
+     *     @RestLink(rel = "self")
+     *     public TestRecord getById(@PathParam("id") int id) { // ... }
+     * 
+ *

+ * Note that the method `getAll` is annotated with `@InjectRestLinks(RestLinkType.TYPE)`, so when calling to the endpoint + * `/records`, it will inject the following links: + * + *

+     * Link: ; rel="list"
+     * Link: ; rel="getValidRecords"
+     * 
+ *

+ * The method `getById` is not injected because it's instance based (it depends on the field `id`). + */ TYPE, + + /** + * It will inject all the links that return the link type {@link RestLink#entityType()}. + * For example: + * + *

+     *
+     *     @GET
+     *     @RestLink(rel = "list")
+     *     public List getAll() { // ... }
+     *
+     *     @GET
+     *     @Path("/records/{id}")
+     *     @RestLink(rel = "self")
+     *     @InjectRestLinks(RestLinkType.INSTANCE)
+     *     public TestRecord getById(@PathParam("id") int id) { // ... }
+     *
+     *     @GET
+     *     @Path("/records/{slug}")
+     *     @RestLink
+     *     public TestRecord getBySlug(@PathParam("slug") String slug) { // ... }
+     *
+     *     @DELETE
+     *     @Path("/records/{id}")
+     *     @RestLink
+     *     public TestRecord delete(@PathParam("slug") String slug) { // ... }
+     * 
+ *

+ * Note that the method `getById` is annotated with `@InjectRestLinks(RestLinkType.INSTANCE)`, so when calling to the + * endpoint `/records/1`, it will inject the following links: + * + *

+     * Link: ; rel="list"
+     * Link: ; rel="self"
+     * Link: ; rel="getBySlug"
+     * Link: ; rel="delete"
+     * 
+ *

+ * Now, all the links have been injected. + */ INSTANCE } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksHandler.java index 10c2e24571c8e..5f21d42bc9785 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksHandler.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksHandler.java @@ -24,6 +24,7 @@ public void setRestLinkData(RestLinkData restLinkData) { @Override public void handle(ResteasyReactiveRequestContext context) { + context.requireCDIRequestScope(); Response response = context.getResponse().get(); for (Link link : getLinks(response)) { response.getHeaders().add("Link", link); @@ -31,11 +32,13 @@ public void handle(ResteasyReactiveRequestContext context) { } private Collection getLinks(Response response) { + RestLinksProvider provider = getRestLinksProvider(); + if ((restLinkData.getRestLinkType() == RestLinkType.INSTANCE) && response.hasEntity()) { - return getTestLinksProvider().getInstanceLinks(response.getEntity()); + return provider.getInstanceLinks(response.getEntity()); } - return getTestLinksProvider() - .getTypeLinks(restLinkData.getEntityType() != null ? entityTypeClass() : response.getEntity().getClass()); + return provider.getTypeLinks( + restLinkData.getEntityType() != null ? entityTypeClass() : response.getEntity().getClass()); } private Class entityTypeClass() { @@ -46,7 +49,7 @@ private Class entityTypeClass() { } } - private RestLinksProvider getTestLinksProvider() { + private RestLinksProvider getRestLinksProvider() { return Arc.container().instance(RestLinksProvider.class).get(); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksProvider.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksProvider.java index ea0a90c331810..b2e7cc3f8d2f4 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksProvider.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksProvider.java @@ -4,9 +4,21 @@ import javax.ws.rs.core.Link; +/** + * An injectable bean that contains methods to get the web links at class and instance levels. + */ public interface RestLinksProvider { + /** + * @param elementType The resource type. + * @return the web links associated with the element type. + */ Collection getTypeLinks(Class elementType); + /** + * @param instance the resource instance. + * @param the resource generic type. + * @return the web links associated with the instance. + */ Collection getInstanceLinks(T instance); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/HalServerResponseFilter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/HalServerResponseFilter.java new file mode 100644 index 0000000000000..d537eb3741843 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/HalServerResponseFilter.java @@ -0,0 +1,80 @@ +package io.quarkus.resteasy.reactive.links.runtime.hal; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +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.resteasy.reactive.common.util.RestMediaType; +import org.jboss.resteasy.reactive.server.ServerResponseFilter; +import org.jboss.resteasy.reactive.server.core.CurrentRequestManager; + +import io.quarkus.hal.HalCollectionWrapper; +import io.quarkus.hal.HalEntityWrapper; +import io.quarkus.hal.HalService; + +public class HalServerResponseFilter { + + private static final String COLLECTION_NAME = "items"; + + private final HalService service; + + @Inject + public HalServerResponseFilter(HalService service) { + this.service = service; + } + + @ServerResponseFilter + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext, Throwable t) { + if (t == null) { + Object entity = responseContext.getEntity(); + if (isHttpStatusSuccessful(responseContext.getStatusInfo()) + && acceptsHalMediaType(requestContext) + && canEntityBeProcessed(entity)) { + if (entity instanceof Collection) { + responseContext.setEntity(service.toHalCollectionWrapper((Collection) entity, COLLECTION_NAME, + findEntityClass())); + } else { + responseContext.setEntity(service.toHalWrapper(entity)); + } + } + } + } + + private boolean canEntityBeProcessed(Object entity) { + return entity != null + && !(entity instanceof String) + && !(entity instanceof HalEntityWrapper || entity instanceof HalCollectionWrapper); + } + + private boolean isHttpStatusSuccessful(Response.StatusType statusInfo) { + return Response.Status.Family.SUCCESSFUL.equals(statusInfo.getFamily()); + } + + private boolean acceptsHalMediaType(ContainerRequestContext requestContext) { + List acceptMediaType = requestContext.getAcceptableMediaTypes().stream().map(MediaType::toString).collect( + Collectors.toList()); + return acceptMediaType.contains(RestMediaType.APPLICATION_HAL_JSON); + } + + private Class findEntityClass() { + Type entityType = CurrentRequestManager.get().getTarget().getReturnType(); + if (entityType instanceof ParameterizedType) { + // we can resolve the entity class from the param type + Type itemEntityType = ((ParameterizedType) entityType).getActualTypeArguments()[0]; + if (itemEntityType instanceof Class) { + return (Class) itemEntityType; + } + } + + return null; + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java new file mode 100644 index 0000000000000..7840131160a9a --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java @@ -0,0 +1,42 @@ +package io.quarkus.resteasy.reactive.links.runtime.hal; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.ws.rs.core.Link; + +import io.quarkus.hal.HalLink; +import io.quarkus.hal.HalService; +import io.quarkus.resteasy.reactive.links.RestLinksProvider; + +@RequestScoped +public class ResteasyReactiveHalService extends HalService { + private final RestLinksProvider linksProvider; + + @Inject + public ResteasyReactiveHalService(RestLinksProvider linksProvider) { + this.linksProvider = linksProvider; + } + + @Override + protected Map getClassLinks(Class entityType) { + return linksToMap(linksProvider.getTypeLinks(entityType)); + } + + @Override + protected Map getInstanceLinks(Object entity) { + return linksToMap(linksProvider.getInstanceLinks(entity)); + } + + private Map linksToMap(Collection refLinks) { + Map links = new HashMap<>(); + for (Link link : refLinks) { + links.put(link.getRel(), new HalLink(link.getUri().toString())); + } + + return links; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/resources/META-INF/quarkus-extension.yaml index b08374e0d12aa..92b6f8b7e9e5c 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -10,7 +10,6 @@ metadata: - "web" - "reactive" status: "stable" - unlisted: true codestart: name: "resteasy-reactive" kind: "core" diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/pom.xml index bac406ccdbc1c..3807caaaff689 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/pom.xml +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/pom.xml @@ -99,6 +99,11 @@ quarkus-jaxrs-client-reactive-deployment test + + io.quarkus + quarkus-reactive-routes-deployment + test + diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CompressionScanner.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CompressionScanner.java index e2e24ed6b4597..2d23b70a6250d 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CompressionScanner.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CompressionScanner.java @@ -1,7 +1,9 @@ package io.quarkus.resteasy.reactive.server.deployment; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; @@ -15,6 +17,7 @@ import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveCompressionHandler; import io.quarkus.vertx.http.Compressed; import io.quarkus.vertx.http.Uncompressed; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpCompression; public class CompressionScanner implements MethodScanner { @@ -22,9 +25,19 @@ public class CompressionScanner implements MethodScanner { static final DotName COMPRESSED = DotName.createSimple(Compressed.class.getName()); static final DotName UNCOMPRESSED = DotName.createSimple(Uncompressed.class.getName()); + private final HttpBuildTimeConfig httpBuildTimeConfig; + + public CompressionScanner(HttpBuildTimeConfig httpBuildTimeConfig) { + this.httpBuildTimeConfig = httpBuildTimeConfig; + } + @Override public List scan(MethodInfo method, ClassInfo actualEndpointClass, Map methodContext) { + if (!httpBuildTimeConfig.enableCompression) { + return Collections.emptyList(); + } + AnnotationStore annotationStore = (AnnotationStore) methodContext.get(EndpointIndexer.METHOD_CONTEXT_ANNOTATION_STORE); HttpCompression compression = HttpCompression.UNDEFINED; if (annotationStore.hasAnnotation(method, COMPRESSED)) { @@ -42,10 +55,17 @@ public List scan(MethodInfo method, ClassInfo actualEndp } if (compression == HttpCompression.OFF) { // No action is needed because the "Content-Encoding: identity" header is set for every request if compression is enabled - return List.of(); + return Collections.emptyList(); } - ResteasyReactiveCompressionHandler handler = new ResteasyReactiveCompressionHandler(); + ResteasyReactiveCompressionHandler handler = new ResteasyReactiveCompressionHandler( + Set.copyOf(httpBuildTimeConfig.compressMediaTypes.orElse(Collections.emptyList()))); handler.setCompression(compression); + String[] produces = (String[]) methodContext.get(EndpointIndexer.METHOD_PRODUCES); + if ((produces != null) && (produces.length > 0)) { + handler.setProduces(produces[0]); + } else { + handler.setProduces(null); + } return List.of(new FixedHandlerChainCustomizer(handler, HandlerChainCustomizer.Phase.AFTER_RESPONSE_CREATED)); } 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 4329be17efe90..d690a15792e12 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 @@ -670,6 +670,41 @@ public void serverSerializers(ResteasyReactiveRecorder recorder, serverSerializersProducer.produce(new ServerSerialisersBuildItem(serialisers)); } + @BuildStep + public void additionalReflection(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, + SetupEndpointsResultBuildItem setupEndpointsResult, List messageBodyWriterBuildItems, + BuildProducer producer) { + List resourceClasses = setupEndpointsResult.getResourceClasses(); + IndexView index = beanArchiveIndexBuildItem.getIndex(); + + // when user provided MessageBodyWriter classes exist that do not extend ServerMessageBodyWriter, we need to enable reflection + // on every resource method, because these providers will be checked first and the JAX-RS API requires + // the method return type and annotations to be passed to the serializers + boolean serializersRequireResourceReflection = false; + for (var writer : messageBodyWriterBuildItems) { + if (writer.isBuiltin()) { + continue; + } + if ((writer.getRuntimeType() != null) && (writer.getRuntimeType() == RuntimeType.CLIENT)) { + continue; + } + ClassInfo writerClassInfo = index.getClassByName(DotName.createSimple(writer.getClassName())); + if (writerClassInfo == null) { + continue; + } + List interfaceNames = writerClassInfo.interfaceNames(); + if (!interfaceNames.contains(ResteasyReactiveServerDotNames.SERVER_MESSAGE_BODY_WRITER)) { + serializersRequireResourceReflection = true; + break; + } + } + if (serializersRequireResourceReflection) { + producer.produce(ReflectiveClassBuildItem + .builder(resourceClasses.stream().map(ResourceClass::getClassName).toArray(String[]::new)).fields(false) + .constructors(false).methods(true).build()); + } + } + @SuppressWarnings("unchecked") @BuildStep @Record(value = ExecutionTime.STATIC_INIT, useIdentityComparisonForParameters = false) 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 6014168931a07..be895cd1977ea 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 @@ -73,6 +73,7 @@ import io.quarkus.resteasy.reactive.spi.JaxrsFeatureBuildItem; import io.quarkus.resteasy.reactive.spi.ParamConverterBuildItem; import io.quarkus.runtime.BlockingOperationNotAllowedException; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; /** * Processor that handles scanning for types and turning them into build items @@ -90,8 +91,8 @@ public MethodScannerBuildItem cacheControlSupport() { } @BuildStep - public MethodScannerBuildItem compressionSupport() { - return new MethodScannerBuildItem(new CompressionScanner()); + public MethodScannerBuildItem compressionSupport(HttpBuildTimeConfig httpBuildTimeConfig) { + return new MethodScannerBuildItem(new CompressionScanner(httpBuildTimeConfig)); } @BuildStep diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/RequestScopedParamConverterTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/RequestScopedParamConverterTest.java new file mode 100644 index 0000000000000..375404bf7df79 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/RequestScopedParamConverterTest.java @@ -0,0 +1,69 @@ +package io.quarkus.resteasy.reactive.server.test; + +import static io.restassured.RestAssured.given; + +import java.util.function.Supplier; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.hamcrest.Matchers; +import org.jboss.resteasy.reactive.server.core.CurrentRequestManager; +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; + +public class RequestScopedParamConverterTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestResource.class, Model.class); + } + }); + + @Test + public void testNoAnnotation() { + given().header("foo", "bar").when().get("/test/test") + .then() + .statusCode(200) + .body(Matchers.equalTo("test/bar")); + } + + @Path("test") + public static class TestResource { + @GET + @Path("{value}") + @Produces(MediaType.TEXT_PLAIN) + public String hello(@PathParam("value") Model model) { + return model.value + "/" + model.fooHeader; + } + } + + public static class Model { + + public final String value; + public final String fooHeader; + + public Model(String value, String fooHeader) { + this.value = value; + this.fooHeader = fooHeader; + } + + // called automatically by RR based on the JAX-RS convention + public static Model valueOf(String value) { + return new Model(value, (String) CurrentRequestManager.get().getHeader("foo", true)); + } + + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/compress/CompressionTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/compress/CompressionTest.java index 4f49ffa6ceeac..0c5bf4ef2ce61 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/compress/CompressionTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/compress/CompressionTest.java @@ -6,6 +6,7 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; +import javax.ws.rs.Produces; import org.jboss.resteasy.reactive.RestResponse; import org.jboss.shrinkwrap.api.asset.StringAsset; @@ -37,6 +38,8 @@ public void testEndpoint() { assertCompressed("/endpoint/content-type-implicitly-compressed"); assertCompressed("/endpoint/content-type-with-param-implicitly-compressed"); assertUncompressed("/endpoint/content-type-implicitly-uncompressed"); + assertCompressed("/endpoint/content-type-in-produces-compressed"); + assertUncompressed("/endpoint/content-type-in-produces-uncompressed"); assertCompressed("/file.txt"); assertUncompressed("/my.doc"); @@ -105,6 +108,19 @@ public RestResponse contentTypeImplicitlyUncompressed() { return RestResponse.ResponseBuilder.ok().entity(MESSAGE).header("Content-type", "foo/bar").build(); } + // uses 'text/plain' as the default type + @GET + @Path("content-type-in-produces-compressed") + public String contentTypeInProducesCompressed() { + return MESSAGE; + } + + @Produces("foo/bar") + @GET + @Path("content-type-in-produces-uncompressed") + public String contentTypeInProducesUncompressed() { + return MESSAGE; + } } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/headers/VertxHeadersTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/headers/VertxHeadersTest.java new file mode 100644 index 0000000000000..c2b902c7e2d50 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/headers/VertxHeadersTest.java @@ -0,0 +1,63 @@ +package io.quarkus.resteasy.reactive.server.test.headers; + +import static io.restassured.RestAssured.when; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.ext.Provider; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.web.RouteFilter; +import io.vertx.ext.web.RoutingContext; + +public class VertxHeadersTest { + + @RegisterExtension + static QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addClasses(VertxFilter.class, JaxRsFilter.class, TestResource.class)); + + @Test + void testVaryHeaderValues() { + var headers = when().get("/test") + .then() + .statusCode(200) + .extract().headers(); + assertThat(headers.getValues(HttpHeaders.VARY)).containsExactlyInAnyOrder("Origin", "Prefer"); + } + + public static class VertxFilter { + @RouteFilter + void addVary(final RoutingContext rc) { + rc.response().headers().add(HttpHeaders.VARY, "Origin"); + rc.next(); + } + } + + @Provider + public static class JaxRsFilter implements ContainerResponseFilter { + @Override + public void filter(final ContainerRequestContext requestContext, final ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().add(HttpHeaders.VARY, "Prefer"); + } + } + + @Path("test") + public static class TestResource { + + @GET + public String test() { + return "test"; + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java index b94b5617c453c..4670039e90755 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java @@ -22,15 +22,6 @@ public class FileTestCase { - private final static String LOREM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut\n" - + - "enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor\n" - + - "in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident,\n" - + - " sunt in culpa qui officia deserunt mollit anim id est laborum.\n" + - "\n" + - ""; private static final String FILE = "src/test/resources/lorem.txt"; @TestHTTPResource @@ -43,13 +34,7 @@ public class FileTestCase { @Test public void testFiles() throws Exception { - // adjusting expected file size for Windows, whose git checkout will adjust line separators - String content; - if (System.lineSeparator().length() == 2) { - content = LOREM.replace("\n", System.lineSeparator()); - } else { - content = LOREM; - } + String content = Files.readString(Path.of(FILE)); String contentLength = String.valueOf(content.length()); RestAssured.get("/providers/file/file") .then() @@ -90,7 +75,7 @@ public void testFiles() throws Exception { .then() .statusCode(200) .header(HttpHeaders.CONTENT_LENGTH, "10") - .body(Matchers.equalTo(LOREM.substring(20, 30))); + .body(Matchers.equalTo(content.substring(20, 30))); } @Test diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/ClassLevelMediaTypeTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/ClassLevelMediaTypeTest.java index f949976fe9444..2ec2baaa7254a 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/ClassLevelMediaTypeTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/ClassLevelMediaTypeTest.java @@ -55,7 +55,7 @@ public void testApplicationJsonMediaType() { Response response = base.request().get(); Assertions.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); String body = response.readEntity(String.class); - Assertions.assertEquals(response.getHeaderString("Content-Type"), "application/json"); + Assertions.assertEquals(response.getHeaderString("Content-Type"), "application/json;charset=UTF-8"); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/MatchedResourceTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/MatchedResourceTest.java index deefb36c25a46..c20cd298b208e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/MatchedResourceTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/MatchedResourceTest.java @@ -88,7 +88,7 @@ public void testMatch() throws Exception { WebTarget base = client.target(generateURL("/match")); Response response = base.request().header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") .get(); - Assertions.assertEquals("text/html", response.getHeaders().getFirst("Content-Type")); + Assertions.assertEquals("text/html;charset=UTF-8", response.getHeaders().getFirst("Content-Type")); String res = response.readEntity(String.class); Assertions.assertEquals("*/*", res, "Wrong response content"); response.close(); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusCurrentRequest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusCurrentRequest.java index 89622da955cc2..786c97e6f4b56 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusCurrentRequest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusCurrentRequest.java @@ -1,5 +1,7 @@ package io.quarkus.resteasy.reactive.server.runtime; +import javax.enterprise.context.ContextNotActiveException; + import org.jboss.resteasy.reactive.server.core.CurrentRequest; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; @@ -22,8 +24,12 @@ public ResteasyReactiveRequestContext get() { @Override public void set(ResteasyReactiveRequestContext set) { if (set == null) { - currentVertxRequest.setOtherHttpContextObject(null); - currentVertxRequest.setCurrent(null); + try { + currentVertxRequest.setOtherHttpContextObject(null); + currentVertxRequest.setCurrent(null); + } catch (ContextNotActiveException ignored) { + // ignored because for HTTP pipelining it can already be closed + } } else { currentVertxRequest.setOtherHttpContextObject(set); currentVertxRequest.setCurrent(set.unwrap(RoutingContext.class)); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusResteasyReactiveRequestContext.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusResteasyReactiveRequestContext.java index 4496dd8b19f1f..fb47a5faf78e4 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusResteasyReactiveRequestContext.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusResteasyReactiveRequestContext.java @@ -1,6 +1,5 @@ package io.quarkus.resteasy.reactive.server.runtime; -import javax.enterprise.event.Event; import javax.ws.rs.core.SecurityContext; import org.jboss.resteasy.reactive.server.core.Deployment; @@ -9,7 +8,6 @@ import org.jboss.resteasy.reactive.server.vertx.VertxResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.spi.ThreadSetupAction; -import io.quarkus.arc.Arc; import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle; @@ -47,10 +45,6 @@ protected void handleRequestScopeActivation() { } } - private static Event createEvent() { - return Arc.container().beanManager().getEvent().select(SecurityIdentity.class); - } - protected SecurityContext createSecurityContext() { return new ResteasyReactiveSecurityContext(context); } @@ -75,4 +69,52 @@ public void handleUnmappedException(Throwable throwable) { private RuntimeException sneakyThrow(Throwable e) throws E { throw (E) e; } + + /** + * The implementation looks like it makes no sense, but it in fact does make sense from a performance perspective. + * The idea is to reduce the use instances of megamorphic calls into a series of instance checks and monomorphic calls. + * The rationale behind this is fully explored in + * https://shipilev.net/blog/2015/black-magic-method-dispatch/#_cheating_the_runtime_2 + * and this specific instance has been verified experimentally to result in better performance. + */ + @Override + protected void invokeHandler(int pos) throws Exception { + var handler = handlers[pos]; + if (handler instanceof org.jboss.resteasy.reactive.server.handlers.MatrixParamHandler) { + handler.handle(this); + } else if (handler instanceof io.quarkus.resteasy.reactive.server.runtime.security.SecurityContextOverrideHandler) { + handler.handle(this); + } else if (handler instanceof org.jboss.resteasy.reactive.server.handlers.RestInitialHandler) { + handler.handle(this); + } else if (handler instanceof org.jboss.resteasy.reactive.server.handlers.ClassRoutingHandler) { + handler.handle(this); + } else if (handler instanceof org.jboss.resteasy.reactive.server.handlers.AbortChainHandler) { + handler.handle(this); + } else if (handler instanceof org.jboss.resteasy.reactive.server.handlers.NonBlockingHandler) { + handler.handle(this); + } else if (handler instanceof org.jboss.resteasy.reactive.server.handlers.BlockingHandler) { + handler.handle(this); + } else if (handler instanceof org.jboss.resteasy.reactive.server.handlers.ResourceRequestFilterHandler) { + handler.handle(this); + } else if (handler instanceof org.jboss.resteasy.reactive.server.handlers.InputHandler) { + handler.handle(this); + } else if (handler instanceof org.jboss.resteasy.reactive.server.handlers.RequestDeserializeHandler) { + handler.handle(this); + } else if (handler instanceof org.jboss.resteasy.reactive.server.handlers.ParameterHandler) { + handler.handle(this); + } else if (handler instanceof org.jboss.resteasy.reactive.server.handlers.InstanceHandler) { + handler.handle(this); + } else if (handler instanceof org.jboss.resteasy.reactive.server.handlers.InvocationHandler) { + handler.handle(this); + } else if (handler instanceof org.jboss.resteasy.reactive.server.handlers.FixedProducesHandler) { + handler.handle(this); + } else if (handler instanceof org.jboss.resteasy.reactive.server.handlers.ResponseHandler) { + handler.handle(this); + } else if (handler instanceof org.jboss.resteasy.reactive.server.handlers.ResponseWriterHandler) { + handler.handle(this); + } else { + // megamorphic call for other handlers + handler.handle(this); + } + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveCompressionHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveCompressionHandler.java index 2dca6fd726952..237cd8c8694fc 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveCompressionHandler.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveCompressionHandler.java @@ -5,23 +5,27 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; +import org.jboss.resteasy.reactive.server.core.EncodedMediaType; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; -import org.jboss.resteasy.reactive.server.spi.RuntimeConfigurableServerRestHandler; -import org.jboss.resteasy.reactive.server.spi.RuntimeConfiguration; import org.jboss.resteasy.reactive.server.spi.ServerHttpResponse; import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; import io.quarkus.vertx.http.runtime.HttpCompression; -public class ResteasyReactiveCompressionHandler implements ServerRestHandler, RuntimeConfigurableServerRestHandler { +public class ResteasyReactiveCompressionHandler implements ServerRestHandler { private HttpCompression compression; - private volatile boolean enableCompression; - private volatile Set compressMediaTypes; + private Set compressMediaTypes; + private String produces; + private volatile EncodedMediaType encodedProduces; public ResteasyReactiveCompressionHandler() { } + public ResteasyReactiveCompressionHandler(Set compressMediaTypes) { + this.compressMediaTypes = compressMediaTypes; + } + public HttpCompression getCompression() { return compression; } @@ -30,34 +34,55 @@ public void setCompression(HttpCompression compression) { this.compression = compression; } + public Set getCompressMediaTypes() { + return compressMediaTypes; + } + + public void setCompressMediaTypes(Set compressMediaTypes) { + this.compressMediaTypes = compressMediaTypes; + } + + public String getProduces() { + return produces; + } + + public void setProduces(String produces) { + this.produces = produces; + } + @Override public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { - if (enableCompression) { - ServerHttpResponse response = requestContext.serverResponse(); - String contentEncoding = response.getResponseHeader(HttpHeaders.CONTENT_ENCODING); - if (contentEncoding != null && io.vertx.core.http.HttpHeaders.IDENTITY.toString().equals(contentEncoding)) { - switch (compression) { - case ON: - response.removeResponseHeader(HttpHeaders.CONTENT_ENCODING); - break; - case UNDEFINED: - MediaType contentType = requestContext.getResponseContentType().getMediaType(); + ServerHttpResponse response = requestContext.serverResponse(); + String contentEncoding = response.getResponseHeader(HttpHeaders.CONTENT_ENCODING); + if (contentEncoding != null && io.vertx.core.http.HttpHeaders.IDENTITY.toString().equals(contentEncoding)) { + switch (compression) { + case ON: + response.removeResponseHeader(HttpHeaders.CONTENT_ENCODING); + break; + case UNDEFINED: + EncodedMediaType responseContentType = requestContext.getResponseContentType(); + if ((responseContentType == null) && (produces != null)) { + if (encodedProduces == null) { + synchronized (this) { + if (encodedProduces == null) { + encodedProduces = new EncodedMediaType(MediaType.valueOf(produces)); + } + } + } + responseContentType = encodedProduces; + } + if (responseContentType != null) { + MediaType contentType = responseContentType.getMediaType(); if (contentType != null && compressMediaTypes.contains(contentType.getType() + '/' + contentType.getSubtype())) { response.removeResponseHeader(HttpHeaders.CONTENT_ENCODING); } - break; - default: - // OFF - no action is needed because the "Content-Encoding: identity" header is set - break; - } + } + break; + default: + // OFF - no action is needed because the "Content-Encoding: identity" header is set + break; } } } - - @Override - public void configure(RuntimeConfiguration configuration) { - enableCompression = configuration.enableCompression(); - compressMediaTypes = configuration.compressMediaTypes(); - } } 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 c11335aaef04b..ad23a9e42c26f 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 @@ -2,7 +2,6 @@ import java.util.List; import java.util.Optional; -import java.util.Set; import org.jboss.resteasy.reactive.server.core.Deployment; import org.jboss.resteasy.reactive.server.spi.DefaultRuntimeConfiguration; @@ -34,14 +33,13 @@ public void configure(RuntimeValue deployment, RuntimeConfiguration runtimeConfiguration = new DefaultRuntimeConfiguration(httpConf.readTimeout, httpConf.body.deleteUploadedFilesOnEnd, httpConf.body.uploadsDirectory, runtimeConf.multipart.inputPart.defaultCharset, maxBodySize, - httpConf.limits.maxFormAttributeSize.asLongValue(), httpConf.enableCompression, - Set.copyOf(httpConf.compressMediaTypes.orElse(List.of()))); + httpConf.limits.maxFormAttributeSize.asLongValue()); List runtimeConfigurableServerRestHandlers = deployment.getValue() .getRuntimeConfigurableServerRestHandlers(); deployment.getValue().setRuntimeConfiguration(runtimeConfiguration); - for (RuntimeConfigurableServerRestHandler handler : runtimeConfigurableServerRestHandlers) { - handler.configure(runtimeConfiguration); + for (int i = 0; i < runtimeConfigurableServerRestHandlers.size(); i++) { + runtimeConfigurableServerRestHandlers.get(i).configure(runtimeConfiguration); } } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/UserAgentFromConfigTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/UserAgentFromConfigTest.java new file mode 100644 index 0000000000000..2fef92992e0d9 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/UserAgentFromConfigTest.java @@ -0,0 +1,83 @@ +package io.quarkus.rest.client.reactive.headers; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; +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.quarkus.test.common.http.TestHTTPResource; + +public class UserAgentFromConfigTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addClasses(Resource.class, Client.class) + .addAsResource(new StringAsset( + "quarkus.rest-client.user-agent=base\n" + + "quarkus.rest-client.client1.url=http://localhost:${quarkus.http.test-port:8081}\n" + + "quarkus.rest-client.client2.url=http://localhost:${quarkus.http.test-port:8081}\n" + + "quarkus.rest-client.client2.user-agent=specific"), + "application.properties")); + + @TestHTTPResource + URI baseUri; + + @RestClient + Client client; + + @RestClient + Client2 client2; + + @Test + void testProgrammatic() { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + assertThat(client.call()).isEqualTo("base"); + } + + @Test + void testBaseUserAgent() { + assertThat(client.call()).isEqualTo("base"); + } + + @Test + void testSpecificUserAgent() { + assertThat(client2.call()).isEqualTo("specific"); + } + + @Path("/") + @ApplicationScoped + public static class Resource { + @GET + public String returnHeaders(@HeaderParam("user-agent") String header) { + return header; + } + } + + @RegisterRestClient(configKey = "client1") + public interface Client { + + @Path("/") + @GET + String call(); + } + + @RegisterRestClient(configKey = "client2") + public interface Client2 { + + @Path("/") + @GET + String call(); + } + +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/Resource.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/Resource.java index 541a1ee0507eb..653850c982c85 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/Resource.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/Resource.java @@ -16,6 +16,18 @@ @Path("/path") public class Resource { + @GET + @Path("{part1}/{part2}/{part3}") + public String getUriParts(@RestPath String part1, @RestPath String part2, @RestPath String part3) { + return String.format("%s/%s/%s", part1, part2, part3); + } + + @GET + @Path("{part1}/{part2}/{part3}/{part4}") + public String getUriParts(@RestPath String part1, @RestPath String part2, @RestPath String part3, @RestPath String part4) { + return String.format("%s/%s/%s/%s", part1, part2, part3, part4); + } + @GET @Path("{part1}/{part2}/{part3}/{part4}/{part5}") public String getUriParts(@RestPath String part1, @RestPath String part2, @RestPath String part3, @RestPath String part4, @@ -23,12 +35,6 @@ public String getUriParts(@RestPath String part1, @RestPath String part2, @RestP return String.format("%s/%s/%s/%s/%s", part1, part2, part3, part4, part5); } - @GET - @Path("{part1}/{part2}/{part3}") - public String getUriParts(@RestPath String part1, @RestPath String part2, @RestPath String part3) { - return String.format("%s/%s/%s", part1, part2, part3); - } - @POST @Path("{part1}/{part2}") public Response getUriEntityAndQueryParam(@RestPath String part1, @RestPath String part2, @RestQuery String queryParam, @@ -46,11 +52,11 @@ public Response getUriEntityAndQueryParam(@RestPath String part1, @RestPath Stri } @POST - @Path("{part1}/{part2}/{part3}/{part4}") + @Path("{part1}/{part2}/{part3}") public Response getUriEntityAndQueryParamFromSubResource(@RestPath String part1, @RestPath String part2, - @RestPath String part3, @RestPath String part4, - @RestQuery String queryParam, String entity, @Context HttpHeaders headers) { - Response.ResponseBuilder responseBuilder = Response.ok(String.format("%s/%s:%s:%s", part1, part2, entity, queryParam)); + @RestPath String part3, @RestQuery String queryParam, String entity, @Context HttpHeaders headers) { + Response.ResponseBuilder responseBuilder = Response + .ok(String.format("%s/%s/%s:%s:%s", part1, part2, part3, entity, queryParam)); for (Map.Entry> headerEntry : headers.getRequestHeaders().entrySet()) { String headerName = headerEntry.getKey(); 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 5cb7c853d44bc..fad7d095aea4a 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 @@ -54,10 +54,10 @@ void shouldPassParamsToSubResource() { @Test void shouldPassParamsToSubSubResource() { - // should result in sending GET /path/rt/mthd/sub/sub/simple + // should result in sending GET /path/rt/mthd/sub/simple RootClient rootClient = RestClientBuilder.newBuilder().baseUri(baseUri).build(RootClient.class); String result = rootClient.sub("rt", "mthd").sub().simpleSub(); - assertThat(result).isEqualTo("rt/mthd/sub/sub/simple"); + assertThat(result).isEqualTo("rt/mthd/sub/subSimple"); } @Test @@ -84,14 +84,14 @@ void shouldDoMultiplePostsInSubSubResource() { SubSubClient sub = rootClient.sub("rt", "mthd").sub(); Response result = sub.postWithQueryParam("prm", "ent1t1"); - assertThat(result.readEntity(String.class)).isEqualTo("rt/mthd:ent1t1:prm"); + assertThat(result.readEntity(String.class)).isEqualTo("rt/mthd/sub:ent1t1:prm"); MultivaluedMap headers = result.getHeaders(); assertThat(headers.get("overridable").get(0)).isEqualTo("SubSubClient"); assertThat(headers.get("fromSubMethod").get(0)).isEqualTo("SubSubClientComputed"); // check that a second usage of the sub stub works result = sub.postWithQueryParam("prm", "ent1t1"); - assertThat(result.readEntity(String.class)).isEqualTo("rt/mthd:ent1t1:prm"); + assertThat(result.readEntity(String.class)).isEqualTo("rt/mthd/sub:ent1t1:prm"); } @Path("/path/{rootParam}") @@ -136,7 +136,7 @@ default String fillingMethod() { @Produces("text/plain") interface SubSubClient { @GET - @Path("/simple") + @Path("/subSimple") String simpleSub(); @POST 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 5c54f60cc8693..12c830977b9c8 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 @@ -25,6 +25,7 @@ import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; import org.jboss.resteasy.reactive.client.api.InvalidRestClientDefinitionException; import org.jboss.resteasy.reactive.client.api.LoggingScope; +import org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties; import org.jboss.resteasy.reactive.client.impl.ClientBuilderImpl; import org.jboss.resteasy.reactive.client.impl.ClientImpl; import org.jboss.resteasy.reactive.client.impl.WebTargetImpl; @@ -314,6 +315,13 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi clientBuilder.trustAll(trustAll); + String userAgent = (String) getConfiguration().getProperty(QuarkusRestClientProperties.USER_AGENT); + if (userAgent != null) { + clientBuilder.setUserAgent(userAgent); + } else if (restClientsConfig.userAgent.isPresent()) { + clientBuilder.setUserAgent(restClientsConfig.userAgent.get()); + } + if (proxyHost != null) { configureProxy(proxyHost, proxyPort, proxyUser, proxyPassword, nonProxyHosts); } else if (restClientsConfig.proxyAddress.isPresent()) { 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 9e791616ec5da..22d3941be8489 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 @@ -107,6 +107,12 @@ private void configureCustomProperties(RestClientBuilder builder) { builder.property(QuarkusRestClientProperties.DISABLE_CONTEXTUAL_ERROR_MESSAGES, configRoot.disableContextualErrorMessages); + + Optional userAgent = oneOf(clientConfigByClassName().userAgent, + clientConfigByConfigKey().userAgent, configRoot.userAgent); + if (userAgent.isPresent()) { + builder.property(QuarkusRestClientProperties.USER_AGENT, userAgent.get()); + } } private void configureProxy(RestClientBuilderImpl builder) { diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java index c43bb7d913dc5..f378ea58bf377 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java @@ -103,6 +103,7 @@ public void testGlobalTimeouts() { configRoot.connectTimeout = 5000L; configRoot.readTimeout = 10000L; configRoot.multipartPostEncoderMode = Optional.empty(); + configRoot.userAgent = Optional.empty(); RestClientBuilderImpl restClientBuilderMock = Mockito.mock(RestClientBuilderImpl.class); new RestClientCDIDelegateBuilder<>(TestClient.class, "http://localhost:8080", @@ -142,6 +143,7 @@ private static RestClientsConfig createSampleConfiguration() { clientConfig.headers = Collections.emptyMap(); clientConfig.shared = Optional.of(true); clientConfig.name = Optional.of("my-client"); + clientConfig.userAgent = Optional.of("rest-client"); RestClientsConfig configRoot = new RestClientsConfig(); configRoot.multipartPostEncoderMode = Optional.of("HTML5"); diff --git a/extensions/scala/deployment/src/main/java/io/quarkus/scala/deployment/ScalaCompilationProvider.java b/extensions/scala/deployment/src/main/java/io/quarkus/scala/deployment/ScalaCompilationProvider.java index b071478a0a1db..924d294b928bd 100644 --- a/extensions/scala/deployment/src/main/java/io/quarkus/scala/deployment/ScalaCompilationProvider.java +++ b/extensions/scala/deployment/src/main/java/io/quarkus/scala/deployment/ScalaCompilationProvider.java @@ -13,6 +13,12 @@ import scala.tools.nsc.Settings; public class ScalaCompilationProvider implements CompilationProvider { + + @Override + public String getProviderKey() { + return "scala"; + } + @Override public Set handledExtensions() { return Collections.singleton(".scala"); diff --git a/extensions/scala/deployment/src/main/java/io/quarkus/scala/deployment/ScalaProcessor.java b/extensions/scala/deployment/src/main/java/io/quarkus/scala/deployment/ScalaProcessor.java index bfa4f0d823d77..56a191677b81f 100644 --- a/extensions/scala/deployment/src/main/java/io/quarkus/scala/deployment/ScalaProcessor.java +++ b/extensions/scala/deployment/src/main/java/io/quarkus/scala/deployment/ScalaProcessor.java @@ -1,5 +1,6 @@ package io.quarkus.scala.deployment; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -22,10 +23,10 @@ FeatureBuildItem feature() { */ @BuildStep void registerScalaJacksonModule(BuildProducer classPathJacksonModules) { - try { - Class.forName(SCALA_JACKSON_MODULE, false, Thread.currentThread().getContextClassLoader()); - classPathJacksonModules.produce(new ClassPathJacksonModuleBuildItem(SCALA_JACKSON_MODULE)); - } catch (Exception ignored) { + if (!QuarkusClassLoader.isClassPresentAtRuntime(SCALA_JACKSON_MODULE)) { + return; } + + classPathJacksonModules.produce(new ClassPathJacksonModuleBuildItem(SCALA_JACKSON_MODULE)); } } diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java index 5a141a3c8bac9..0988fc5e30d30 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java @@ -55,12 +55,12 @@ /** * Optionally defines a unique identifier for this job. *

- * If the value starts with "{" and ends with "}" the scheduler attempts to find a corresponding config property - * and use the configured value instead: {@code @Scheduled(identity = "{myservice.check.identity.expr}")}. - * - *

- * If the value is not given, Quarkus will generate a unique id. + * The value can be a property expression. In this case, the scheduler attempts to use the configured value instead: + * {@code @Scheduled(identity = "${myJob.identity}")}. + * Additionally, the property expression can specify a default value: {@code @Scheduled(identity = + * "${myJob.identity:defaultIdentity}")}. *

+ * If the value is not provided then a unique id is generated. * * @return the unique identity of the schedule */ @@ -69,8 +69,14 @@ /** * Defines a cron-like expression. For example "0 15 10 * * ?" fires at 10:15am every day. *

- * If the value starts with "{" and ends with "}" the scheduler attempts to find a corresponding config property - * and use the configured value instead: {@code @Scheduled(cron = "{myservice.check.cron.expr}")}. + * The value can be a property expression. In this case, the scheduler attempts to use the configured value instead: + * {@code @Scheduled(cron = "${myJob.cronExpression}")}. + * Additionally, the property expression can specify a default value: {@code @Scheduled(cron = "${myJob.cronExpression:0/2 * + * * * * ?}")}. + *

+ * Furthermore, two special constants can be used to disable the scheduled method: {@code off} and {@code disabled}. For + * example, {@code @Scheduled(cron="${myJob.cronExpression:off}")} means that if the property is undefined then + * the method is never executed. * * @return the cron-like expression */ @@ -83,8 +89,14 @@ * is added automatically, so for example, {@code 15m} can be used instead of {@code PT15M} and is parsed as "15 minutes". * Note that the absolute value of the value is always used. *

- * If the value starts with "{" and ends with "}" the scheduler attempts to find a corresponding config property - * and use the configured value instead: {@code @Scheduled(every = "{myservice.check.every.expr}")}. + * The value can be a property expression. In this case, the scheduler attempts to use the configured value instead: + * {@code @Scheduled(every = "${myJob.everyExpression}")}. + * Additionally, the property expression can specify a default value: {@code @Scheduled(every = + * "${myJob.everyExpression:5m}")}. + *

+ * Furthermore, two special constants can be used to disable the scheduled method: {@code off} and {@code disabled}. For + * example, {@code @Scheduled(every="${myJob.everyExpression:off}")} means that if the property is undefined then + * the method is never executed. * * @return the period expression based on the ISO-8601 duration format {@code PnDTnHnMn.nS} */ @@ -114,8 +126,10 @@ * is added automatically, so for example, {@code 15s} can be used instead of {@code PT15S} and is parsed as "15 seconds". * Note that the absolute value of the value is always used. *

- * If the value starts with "{" and ends with "}" the scheduler attempts to find a corresponding config property - * and use the configured value instead: {@code @Scheduled(delayed = "{myservice.delayed}")}. + * The value can be a property expression. In this case, the scheduler attempts to use the configured value instead: + * {@code @Scheduled(delayed = "${myJob.delayedExpression}")}. + * Additionally, the property expression can specify a default value: {@code @Scheduled(delayed = + * "${myJob.delayedExpression:5m}")}. * * @return the period expression based on the ISO-8601 duration format {@code PnDTnHnMn.nS} */ @@ -147,8 +161,10 @@ * is added automatically, so for example, {@code 15m} can be used instead of {@code PT15M} and is parsed as "15 minutes". * Note that the absolute value of the value is always used. *

- * If the value starts with "{" and ends with "}" the scheduler attempts to find a corresponding config property - * and use the configured value instead: {@code @Scheduled(every = "{myservice.check.overdue-grace-period.expr}")}. + * The value can be a property expression. In this case, the scheduler attempts to use the configured value instead: + * {@code @Scheduled(overdueGracePeriod = "${myJob.overdueExpression}")}. + * Additionally, the property expression can specify a default value: {@code @Scheduled(overdueGracePeriod = + * "${myJob.overdueExpression:5m}")}. * * @return the period expression based on the ISO-8601 duration format {@code PnDTnHnMn.nS} */ 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 96ce287992775..ed590c058e17f 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 @@ -52,6 +52,7 @@ import io.quarkus.arc.processor.BuiltinScope; import io.quarkus.arc.processor.DotNames; import io.quarkus.arc.runtime.BeanLookupSupplier; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; @@ -601,15 +602,13 @@ UnremovableBeanBuildItem unremoveableSkipPredicates() { @BuildStep void produceCoroutineScope(BuildProducer buildItemBuildProducer) { - try { - Thread.currentThread().getContextClassLoader().loadClass("kotlinx.coroutines.CoroutineScope"); - buildItemBuildProducer.produce(AdditionalBeanBuildItem.builder() - .addBeanClass("io.quarkus.scheduler.kotlin.runtime.ApplicationCoroutineScope") - .setUnremovable().build()); - } catch (ClassNotFoundException e) { - // ignore + if (!QuarkusClassLoader.isClassPresentAtRuntime("kotlinx.coroutines.CoroutineScope")) { + return; } + buildItemBuildProducer.produce(AdditionalBeanBuildItem.builder() + .addBeanClass("io.quarkus.scheduler.kotlin.runtime.ApplicationCoroutineScope") + .setUnremovable().build()); } } diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/OverdueCronExecutionTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/OverdueCronExecutionTest.java new file mode 100644 index 0000000000000..7e868b8e4445e --- /dev/null +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/OverdueCronExecutionTest.java @@ -0,0 +1,74 @@ +package io.quarkus.scheduler.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +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.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.Trigger; +import io.quarkus.test.QuarkusUnitTest; + +public class OverdueCronExecutionTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> root.addClasses(Jobs.class) + .addAsResource(new StringAsset("quarkus.scheduler.overdue-grace-period=2H\njob.gracePeriod=2H"), + "application.properties")); + + @Inject + Scheduler scheduler; + + @Test + public void testExecution() { + try { + Trigger overdueJob = scheduler.getScheduledJob("overdueJob"); + Trigger tolerantJob = scheduler.getScheduledJob("tolerantJob"); + Trigger gracePeriodFromConfigJob = scheduler.getScheduledJob("gracePeriodFromConfigJob"); + Trigger defaultGracePeriodJob = scheduler.getScheduledJob("defaultGracePeriodJob"); + assertTrue(Jobs.LATCH.await(5, TimeUnit.SECONDS)); + scheduler.pause(); + Thread.sleep(1250); + assertTrue(overdueJob.isOverdue()); + assertFalse(tolerantJob.isOverdue()); + assertFalse(gracePeriodFromConfigJob.isOverdue()); + assertFalse(defaultGracePeriodJob.isOverdue()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + } + + static class Jobs { + + static final String CRON = "0/1 * * * * ?"; + + static final CountDownLatch LATCH = new CountDownLatch(1); + + @Scheduled(identity = "overdueJob", cron = CRON, overdueGracePeriod = "0.1s") + void overdueJob() { + LATCH.countDown(); + } + + @Scheduled(identity = "tolerantJob", cron = CRON, overdueGracePeriod = "2H") + void tolerantJob() { + } + + @Scheduled(identity = "gracePeriodFromConfigJob", cron = CRON, overdueGracePeriod = "{job.gracePeriod}") + void gracePeriodFromConfigJob() { + } + + @Scheduled(identity = "defaultGracePeriodJob", cron = CRON) + void defaultGracePeriodJob() { + } + } +} diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/OverdueExecutionTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/OverdueExecutionTest.java index 88e6ea82452f9..9eebf27f9026b 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/OverdueExecutionTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/OverdueExecutionTest.java @@ -52,7 +52,7 @@ public void testExecution() { static class Jobs { - static final CountDownLatch LATCH = new CountDownLatch(2); + static final CountDownLatch LATCH = new CountDownLatch(1); @Scheduled(identity = "overdueJob", every = "0.1s", overdueGracePeriod = "0.1s") void overdueJob() throws InterruptedException { diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java index ae6e282a0e301..40a2bf2af9dd9 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java @@ -51,6 +51,7 @@ import io.quarkus.scheduler.common.runtime.SkipPredicateInvoker; import io.quarkus.scheduler.common.runtime.StatusEmitterInvoker; import io.quarkus.scheduler.common.runtime.util.SchedulerUtils; +import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle; import io.smallrye.common.vertx.VertxContext; import io.vertx.core.Context; import io.vertx.core.Handler; @@ -319,6 +320,7 @@ public void run() { } } else { Context context = VertxContext.getOrCreateDuplicatedContext(vertx); + VertxContextSafetyToggle.setContextSafe(context, true); context.runOnContext(new Handler() { @Override public void handle(Void event) { @@ -358,6 +360,12 @@ static abstract class SimpleTrigger implements Trigger { */ abstract ZonedDateTime evaluate(ZonedDateTime now); + @Override + public Instant getPreviousFireTime() { + ZonedDateTime last = lastFireTime; + return last != null ? lastFireTime.toInstant() : null; + } + public String getId() { return id; } @@ -406,12 +414,11 @@ ZonedDateTime evaluate(ZonedDateTime now) { @Override public Instant getNextFireTime() { - return lastFireTime.plus(Duration.ofMillis(interval)).toInstant(); - } - - @Override - public Instant getPreviousFireTime() { - return lastFireTime.toInstant(); + ZonedDateTime last = lastFireTime; + if (last == null) { + last = start; + } + return last.plus(Duration.ofMillis(interval)).toInstant(); } @Override @@ -443,22 +450,16 @@ static class CronTrigger extends SimpleTrigger { super(id, start); this.cron = cron; this.executionTime = ExecutionTime.forCron(cron); - this.lastFireTime = ZonedDateTime.now(); + this.lastFireTime = start; this.gracePeriod = gracePeriod; } @Override public Instant getNextFireTime() { - Optional nextFireTime = executionTime.nextExecution(ZonedDateTime.now()); + Optional nextFireTime = executionTime.nextExecution(lastFireTime); return nextFireTime.isPresent() ? nextFireTime.get().toInstant() : null; } - @Override - public Instant getPreviousFireTime() { - Optional prevFireTime = executionTime.lastExecution(ZonedDateTime.now()); - return prevFireTime.isPresent() ? prevFireTime.get().toInstant() : null; - } - ZonedDateTime evaluate(ZonedDateTime now) { if (now.isBefore(start)) { return null; @@ -481,7 +482,7 @@ public boolean isOverdue() { if (now.isBefore(start)) { return false; } - Optional nextFireTime = executionTime.nextExecution(ZonedDateTime.now()); + Optional nextFireTime = executionTime.nextExecution(lastFireTime); return nextFireTime.isEmpty() || nextFireTime.get().plus(gracePeriod).isBefore(now); } diff --git a/extensions/schema-registry/apicurio/avro/deployment/pom.xml b/extensions/schema-registry/apicurio/avro/deployment/pom.xml new file mode 100644 index 0000000000000..3c0009c1b7cf9 --- /dev/null +++ b/extensions/schema-registry/apicurio/avro/deployment/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + io.quarkus + quarkus-apicurio-registry-avro-parent + 999-SNAPSHOT + + + quarkus-apicurio-registry-avro-deployment + Quarkus - Apicurio Registry - Avro - Deployment + + + + io.quarkus + quarkus-apicurio-registry-avro + + + + io.quarkus + quarkus-apicurio-registry-common-deployment + + + io.quarkus + quarkus-avro-deployment + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/schema-registry/apicurio/avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/ApicurioRegistryAvroProcessor.java b/extensions/schema-registry/apicurio/avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/ApicurioRegistryAvroProcessor.java new file mode 100644 index 0000000000000..1e029ff53218e --- /dev/null +++ b/extensions/schema-registry/apicurio/avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/ApicurioRegistryAvroProcessor.java @@ -0,0 +1,45 @@ +package io.quarkus.apicurio.registry.avro; + +import io.quarkus.deployment.Feature; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; + +public class ApicurioRegistryAvroProcessor { + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(Feature.APICURIO_REGISTRY_AVRO); + } + + @BuildStep + public void apicurioRegistryAvro(BuildProducer reflectiveClass, + BuildProducer sslNativeSupport) { + + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, false, + "io.apicurio.registry.serde.avro.AvroKafkaDeserializer", + "io.apicurio.registry.serde.avro.AvroKafkaSerializer")); + + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, true, + "io.apicurio.registry.serde.strategy.SimpleTopicIdStrategy", + "io.apicurio.registry.serde.strategy.TopicIdStrategy", + "io.apicurio.registry.serde.avro.DefaultAvroDatumProvider", + "io.apicurio.registry.serde.avro.ReflectAvroDatumProvider", + "io.apicurio.registry.serde.avro.strategy.RecordIdStrategy", + "io.apicurio.registry.serde.avro.strategy.TopicRecordIdStrategy")); + + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, true, + "io.apicurio.registry.serde.DefaultSchemaResolver", + "io.apicurio.registry.serde.DefaultIdHandler", + "io.apicurio.registry.serde.Legacy4ByteIdHandler", + "io.apicurio.registry.serde.fallback.DefaultFallbackArtifactProvider", + "io.apicurio.registry.serde.headers.DefaultHeadersHandler")); + } + + @BuildStep + ExtensionSslNativeSupportBuildItem enableSslInNative() { + return new ExtensionSslNativeSupportBuildItem(Feature.APICURIO_REGISTRY_AVRO); + } + +} diff --git a/extensions/apicurio-registry-avro/pom.xml b/extensions/schema-registry/apicurio/avro/pom.xml similarity index 91% rename from extensions/apicurio-registry-avro/pom.xml rename to extensions/schema-registry/apicurio/avro/pom.xml index 3689c6ada15f2..85be4515a4d63 100644 --- a/extensions/apicurio-registry-avro/pom.xml +++ b/extensions/schema-registry/apicurio/avro/pom.xml @@ -3,7 +3,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> - quarkus-extensions-parent + quarkus-apicurio-registry-parent io.quarkus 999-SNAPSHOT ../pom.xml diff --git a/extensions/apicurio-registry-avro/runtime/pom.xml b/extensions/schema-registry/apicurio/avro/runtime/pom.xml similarity index 71% rename from extensions/apicurio-registry-avro/runtime/pom.xml rename to extensions/schema-registry/apicurio/avro/runtime/pom.xml index 7f4be7fd6dbc6..c30d659c43c4e 100644 --- a/extensions/apicurio-registry-avro/runtime/pom.xml +++ b/extensions/schema-registry/apicurio/avro/runtime/pom.xml @@ -12,7 +12,7 @@ quarkus-apicurio-registry-avro Quarkus - Apicurio Registry - Avro - Runtime - Provide support for the Apicurio Registry Avro library + io.apicurio @@ -24,32 +24,15 @@ - - io.apicurio - apicurio-common-rest-client-vertx - - - io.quarkus - quarkus-kubernetes-service-binding - true - - + io.quarkus - quarkus-core + quarkus-apicurio-registry-common io.quarkus quarkus-avro - - io.quarkus - quarkus-vertx - - - org.apache.commons - commons-lang3 - @@ -79,4 +62,29 @@ + + + + jakarta-rewrite + + + jakarta-rewrite + + + + + + org.openrewrite.maven + rewrite-maven-plugin + + + io.quarkus.exclude-javax-jaxrs + + + + + + + + diff --git a/extensions/apicurio-registry-avro/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/schema-registry/apicurio/avro/runtime/src/main/resources/META-INF/quarkus-extension.yaml similarity index 100% rename from extensions/apicurio-registry-avro/runtime/src/main/resources/META-INF/quarkus-extension.yaml rename to extensions/schema-registry/apicurio/avro/runtime/src/main/resources/META-INF/quarkus-extension.yaml diff --git a/extensions/apicurio-registry-avro/deployment/pom.xml b/extensions/schema-registry/apicurio/common/deployment/pom.xml similarity index 58% rename from extensions/apicurio-registry-avro/deployment/pom.xml rename to extensions/schema-registry/apicurio/common/deployment/pom.xml index b07b8f192566b..723a788758709 100644 --- a/extensions/apicurio-registry-avro/deployment/pom.xml +++ b/extensions/schema-registry/apicurio/common/deployment/pom.xml @@ -5,61 +5,37 @@ 4.0.0 + quarkus-apicurio-registry-common-parent io.quarkus - quarkus-apicurio-registry-avro-parent 999-SNAPSHOT + ../pom.xml - quarkus-apicurio-registry-avro-deployment - Quarkus - Apicurio Registry - Avro - Deployment + quarkus-apicurio-registry-common-deployment + Quarkus - Apicurio Registry - Common - Deployment io.quarkus - quarkus-apicurio-registry-avro + quarkus-apicurio-registry-common io.quarkus quarkus-core-deployment - io.quarkus - quarkus-avro-deployment + quarkus-schema-registry-devservice-deployment io.quarkus quarkus-vertx-deployment - - io.quarkus - quarkus-smallrye-openapi-spi - - - - org.testcontainers - testcontainers - - - junit - junit - - - - - io.quarkus - quarkus-junit4-mock - - - io.quarkus - quarkus-devservices-deployment - io.quarkus - quarkus-junit5-internal - test + quarkus-smallrye-openapi-spi diff --git a/extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/binding/ServiceRegistryBindingExtensionProcessor.java b/extensions/schema-registry/apicurio/common/deployment/src/main/java/io/quarkus/apicurio/registry/binding/ServiceRegistryBindingExtensionProcessor.java similarity index 86% rename from extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/binding/ServiceRegistryBindingExtensionProcessor.java rename to extensions/schema-registry/apicurio/common/deployment/src/main/java/io/quarkus/apicurio/registry/binding/ServiceRegistryBindingExtensionProcessor.java index c1835061bac21..88e394e3691f3 100644 --- a/extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/binding/ServiceRegistryBindingExtensionProcessor.java +++ b/extensions/schema-registry/apicurio/common/deployment/src/main/java/io/quarkus/apicurio/registry/binding/ServiceRegistryBindingExtensionProcessor.java @@ -1,6 +1,5 @@ -package io.quarkus.apicurio.registry.avro.binding; +package io.quarkus.apicurio.registry.binding; -import io.quarkus.apicurio.registry.binding.ServiceRegistryBindingConverter; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; diff --git a/extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/ApicurioRegistryAvroProcessor.java b/extensions/schema-registry/apicurio/common/deployment/src/main/java/io/quarkus/apicurio/registry/common/ApicurioRegistryClientProcessor.java similarity index 58% rename from extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/ApicurioRegistryAvroProcessor.java rename to extensions/schema-registry/apicurio/common/deployment/src/main/java/io/quarkus/apicurio/registry/common/ApicurioRegistryClientProcessor.java index 16cb347e8630d..7bb230d95463d 100644 --- a/extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/ApicurioRegistryAvroProcessor.java +++ b/extensions/schema-registry/apicurio/common/deployment/src/main/java/io/quarkus/apicurio/registry/common/ApicurioRegistryClientProcessor.java @@ -1,49 +1,23 @@ -package io.quarkus.apicurio.registry.avro; +package io.quarkus.apicurio.registry.common; import java.io.IOException; import io.apicurio.rest.client.spi.ApicurioHttpClientProvider; -import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; -import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.smallrye.openapi.deployment.spi.IgnoreStaticDocumentBuildItem; import io.quarkus.vertx.deployment.VertxBuildItem; -public class ApicurioRegistryAvroProcessor { - @BuildStep - FeatureBuildItem feature() { - return new FeatureBuildItem(Feature.APICURIO_REGISTRY_AVRO); - } +public class ApicurioRegistryClientProcessor { @BuildStep - public void apicurioRegistryAvro(BuildProducer reflectiveClass, + public void apicurioRegistryClient(BuildProducer reflectiveClass, BuildProducer sslNativeSupport) { - - reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, false, - "io.apicurio.registry.serde.avro.AvroKafkaDeserializer", - "io.apicurio.registry.serde.avro.AvroKafkaSerializer")); - - reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, true, - "io.apicurio.registry.serde.strategy.SimpleTopicIdStrategy", - "io.apicurio.registry.serde.strategy.TopicIdStrategy", - "io.apicurio.registry.serde.avro.DefaultAvroDatumProvider", - "io.apicurio.registry.serde.avro.ReflectAvroDatumProvider", - "io.apicurio.registry.serde.avro.strategy.RecordIdStrategy", - "io.apicurio.registry.serde.avro.strategy.TopicRecordIdStrategy")); - - reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, true, - "io.apicurio.registry.serde.DefaultSchemaResolver", - "io.apicurio.registry.serde.DefaultIdHandler", - "io.apicurio.registry.serde.Legacy4ByteIdHandler", - "io.apicurio.registry.serde.fallback.DefaultFallbackArtifactProvider", - "io.apicurio.registry.serde.headers.DefaultHeadersHandler")); - reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, true, "io.apicurio.rest.client.auth.exception.NotAuthorizedException", "io.apicurio.rest.client.auth.exception.ForbiddenException", @@ -78,9 +52,4 @@ public void apicurioRegistryClient(VertxBuildItem vertx, ApicurioRegistryClient client.setup(vertx.getVertx()); } - @BuildStep - ExtensionSslNativeSupportBuildItem enableSslInNative() { - return new ExtensionSslNativeSupportBuildItem(Feature.APICURIO_REGISTRY_AVRO); - } - } diff --git a/extensions/schema-registry/apicurio/common/pom.xml b/extensions/schema-registry/apicurio/common/pom.xml new file mode 100644 index 0000000000000..2f0628e909639 --- /dev/null +++ b/extensions/schema-registry/apicurio/common/pom.xml @@ -0,0 +1,22 @@ + + + + quarkus-apicurio-registry-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + + 4.0.0 + quarkus-apicurio-registry-common-parent + Quarkus - Apicurio Registry - Common + pom + + + deployment + runtime + + + diff --git a/extensions/schema-registry/apicurio/common/runtime/pom.xml b/extensions/schema-registry/apicurio/common/runtime/pom.xml new file mode 100644 index 0000000000000..b14f017cb2585 --- /dev/null +++ b/extensions/schema-registry/apicurio/common/runtime/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + + quarkus-apicurio-registry-common-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + + quarkus-apicurio-registry-common + Quarkus - Apicurio Registry - Common - Runtime + + + + io.apicurio + apicurio-registry-client + + + io.apicurio + apicurio-common-rest-client-jdk + + + + + io.apicurio + apicurio-common-rest-client-vertx + + + org.apache.commons + commons-lang3 + + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-schema-registry-devservice + + + io.quarkus + quarkus-vertx + + + + io.quarkus + quarkus-kubernetes-service-binding + true + + + + + + + jakarta-rewrite + + + jakarta-rewrite + + + + + + org.openrewrite.maven + rewrite-maven-plugin + + + io.quarkus.exclude-javax-jaxrs + + + + + + + + diff --git a/extensions/apicurio-registry-avro/runtime/src/main/java/io/quarkus/apicurio/registry/binding/ServiceRegistryBindingConverter.java b/extensions/schema-registry/apicurio/common/runtime/src/main/java/io/quarkus/apicurio/registry/binding/ServiceRegistryBindingConverter.java similarity index 100% rename from extensions/apicurio-registry-avro/runtime/src/main/java/io/quarkus/apicurio/registry/binding/ServiceRegistryBindingConverter.java rename to extensions/schema-registry/apicurio/common/runtime/src/main/java/io/quarkus/apicurio/registry/binding/ServiceRegistryBindingConverter.java diff --git a/extensions/apicurio-registry-avro/runtime/src/main/java/io/quarkus/apicurio/registry/avro/ApicurioRegistryClient.java b/extensions/schema-registry/apicurio/common/runtime/src/main/java/io/quarkus/apicurio/registry/common/ApicurioRegistryClient.java similarity index 90% rename from extensions/apicurio-registry-avro/runtime/src/main/java/io/quarkus/apicurio/registry/avro/ApicurioRegistryClient.java rename to extensions/schema-registry/apicurio/common/runtime/src/main/java/io/quarkus/apicurio/registry/common/ApicurioRegistryClient.java index ceae6a11f3a5c..1a072229c2fcd 100644 --- a/extensions/apicurio-registry-avro/runtime/src/main/java/io/quarkus/apicurio/registry/avro/ApicurioRegistryClient.java +++ b/extensions/schema-registry/apicurio/common/runtime/src/main/java/io/quarkus/apicurio/registry/common/ApicurioRegistryClient.java @@ -1,4 +1,4 @@ -package io.quarkus.apicurio.registry.avro; +package io.quarkus.apicurio.registry.common; import io.apicurio.registry.rest.client.RegistryClientFactory; import io.apicurio.rest.client.VertxHttpClientProvider; diff --git a/extensions/apicurio-registry-avro/runtime/src/main/resources/META-INF/services/io.quarkus.kubernetes.service.binding.runtime.ServiceBindingConverter b/extensions/schema-registry/apicurio/common/runtime/src/main/resources/META-INF/services/io.quarkus.kubernetes.service.binding.runtime.ServiceBindingConverter similarity index 100% rename from extensions/apicurio-registry-avro/runtime/src/main/resources/META-INF/services/io.quarkus.kubernetes.service.binding.runtime.ServiceBindingConverter rename to extensions/schema-registry/apicurio/common/runtime/src/main/resources/META-INF/services/io.quarkus.kubernetes.service.binding.runtime.ServiceBindingConverter diff --git a/extensions/schema-registry/apicurio/pom.xml b/extensions/schema-registry/apicurio/pom.xml new file mode 100644 index 0000000000000..ce3c9d4de020f --- /dev/null +++ b/extensions/schema-registry/apicurio/pom.xml @@ -0,0 +1,21 @@ + + + + quarkus-schema-registry-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + + 4.0.0 + quarkus-apicurio-registry-parent + Quarkus - Apicurio Registry + pom + + + common + avro + + diff --git a/extensions/schema-registry/confluent/avro/deployment/pom.xml b/extensions/schema-registry/confluent/avro/deployment/pom.xml new file mode 100644 index 0000000000000..c98b4f7c0e4ae --- /dev/null +++ b/extensions/schema-registry/confluent/avro/deployment/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + + io.quarkus + quarkus-confluent-registry-avro-parent + 999-SNAPSHOT + + + quarkus-confluent-registry-avro-deployment + Quarkus - Confluent Schema Registry - Avro - Deployment + + + + io.quarkus + quarkus-confluent-registry-avro + + + + io.quarkus + quarkus-avro-deployment + + + io.quarkus + quarkus-confluent-registry-common-deployment + + + io.quarkus + quarkus-schema-registry-devservice-deployment + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/schema-registry/confluent/avro/deployment/src/main/java/io/quarkus/confluent/registry/avro/ConfluentRegistryAvroProcessor.java b/extensions/schema-registry/confluent/avro/deployment/src/main/java/io/quarkus/confluent/registry/avro/ConfluentRegistryAvroProcessor.java new file mode 100644 index 0000000000000..ee0f05e8a3a73 --- /dev/null +++ b/extensions/schema-registry/confluent/avro/deployment/src/main/java/io/quarkus/confluent/registry/avro/ConfluentRegistryAvroProcessor.java @@ -0,0 +1,30 @@ +package io.quarkus.confluent.registry.avro; + +import io.quarkus.deployment.Feature; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; + +public class ConfluentRegistryAvroProcessor { + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(Feature.CONFLUENT_REGISTRY_AVRO); + } + + @BuildStep + public void confluentRegistryAvro(BuildProducer reflectiveClass, + BuildProducer sslNativeSupport) { + reflectiveClass + .produce(new ReflectiveClassBuildItem(true, false, + "io.confluent.kafka.serializers.KafkaAvroDeserializer", + "io.confluent.kafka.serializers.KafkaAvroSerializer")); + } + + @BuildStep + ExtensionSslNativeSupportBuildItem enableSslInNative() { + return new ExtensionSslNativeSupportBuildItem(Feature.CONFLUENT_REGISTRY_AVRO); + } + +} diff --git a/extensions/schema-registry/confluent/avro/pom.xml b/extensions/schema-registry/confluent/avro/pom.xml new file mode 100644 index 0000000000000..aa04537073af1 --- /dev/null +++ b/extensions/schema-registry/confluent/avro/pom.xml @@ -0,0 +1,21 @@ + + + + quarkus-confluent-registry-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + + 4.0.0 + quarkus-confluent-registry-avro-parent + Quarkus - Confluent Schema Registry - Avro + pom + + + deployment + runtime + + diff --git a/extensions/schema-registry/confluent/avro/runtime/pom.xml b/extensions/schema-registry/confluent/avro/runtime/pom.xml new file mode 100644 index 0000000000000..61a73137c31f0 --- /dev/null +++ b/extensions/schema-registry/confluent/avro/runtime/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + io.quarkus + quarkus-confluent-registry-avro-parent + 999-SNAPSHOT + + + quarkus-confluent-registry-avro + Quarkus - Confluent Schema Registry - Avro - Runtime + + + + io.quarkus + quarkus-avro + + + io.quarkus + quarkus-confluent-registry-common + + + io.quarkus + quarkus-schema-registry-devservice + + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + io.quarkus.confluent.registry.avro + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/schema-registry/confluent/avro/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/schema-registry/confluent/avro/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..0cbb47999d4f8 --- /dev/null +++ b/extensions/schema-registry/confluent/avro/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,13 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Confluent Schema Registry - Avro" +metadata: + keywords: + - "confluent" + - "avro" + guide: "https://quarkus.io/guides/kafka-schema-registry-avro" + categories: + - "serialization" + status: "experimental" + config: + - "avro.codegen." diff --git a/extensions/schema-registry/confluent/common/deployment/pom.xml b/extensions/schema-registry/confluent/common/deployment/pom.xml new file mode 100644 index 0000000000000..2c4608470b710 --- /dev/null +++ b/extensions/schema-registry/confluent/common/deployment/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + + quarkus-confluent-registry-common-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + + quarkus-confluent-registry-common-deployment + Quarkus - Confluent Schema Registry - Common - Deployment + + + + io.quarkus + quarkus-confluent-registry-common + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-schema-registry-devservice-deployment + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/schema-registry/confluent/common/deployment/src/main/java/io/quarkus/confluent/registry/common/ConfluentRegistryClientProcessor.java b/extensions/schema-registry/confluent/common/deployment/src/main/java/io/quarkus/confluent/registry/common/ConfluentRegistryClientProcessor.java new file mode 100644 index 0000000000000..8a11694af4e2f --- /dev/null +++ b/extensions/schema-registry/confluent/common/deployment/src/main/java/io/quarkus/confluent/registry/common/ConfluentRegistryClientProcessor.java @@ -0,0 +1,54 @@ +package io.quarkus.confluent.registry.common; + +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; +import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; + +public class ConfluentRegistryClientProcessor { + + @BuildStep + public void confluentRegistryClient( + BuildProducer reflectiveClass, + BuildProducer serviceProviders, + BuildProducer sslNativeSupport) { + + reflectiveClass + .produce(new ReflectiveClassBuildItem(true, false, false, + "io.confluent.kafka.serializers.context.NullContextNameStrategy")); + + reflectiveClass + .produce(new ReflectiveClassBuildItem(true, true, false, + "io.confluent.kafka.serializers.subject.TopicNameStrategy", + "io.confluent.kafka.serializers.subject.TopicRecordNameStrategy", + "io.confluent.kafka.serializers.subject.RecordNameStrategy")); + + reflectiveClass + .produce(new ReflectiveClassBuildItem(true, true, false, + "io.confluent.kafka.schemaregistry.client.rest.entities.ErrorMessage", + "io.confluent.kafka.schemaregistry.client.rest.entities.Schema", + "io.confluent.kafka.schemaregistry.client.rest.entities.Config", + "io.confluent.kafka.schemaregistry.client.rest.entities.SchemaReference", + "io.confluent.kafka.schemaregistry.client.rest.entities.SchemaString", + "io.confluent.kafka.schemaregistry.client.rest.entities.SchemaTypeConverter", + "io.confluent.kafka.schemaregistry.client.rest.entities.ServerClusterId", + "io.confluent.kafka.schemaregistry.client.rest.entities.SujectVersion")); + + reflectiveClass + .produce(new ReflectiveClassBuildItem(true, true, false, + "io.confluent.kafka.schemaregistry.client.rest.entities.requests.CompatibilityCheckResponse", + "io.confluent.kafka.schemaregistry.client.rest.entities.requests.ConfigUpdateRequest", + "io.confluent.kafka.schemaregistry.client.rest.entities.requests.ModeGetResponse", + "io.confluent.kafka.schemaregistry.client.rest.entities.requests.ModeUpdateRequest", + "io.confluent.kafka.schemaregistry.client.rest.entities.requests.RegisterSchemaRequest", + "io.confluent.kafka.schemaregistry.client.rest.entities.requests.RegisterSchemaResponse")); + + serviceProviders + .produce(new ServiceProviderBuildItem( + "io.confluent.kafka.schemaregistry.client.security.basicauth.BasicAuthCredentialProvider", + "io.confluent.kafka.schemaregistry.client.security.basicauth.SaslBasicAuthCredentialProvider", + "io.confluent.kafka.schemaregistry.client.security.basicauth.UrlBasicAuthCredentialProvider", + "io.confluent.kafka.schemaregistry.client.security.basicauth.UserInfoCredentialProvider")); + } +} diff --git a/extensions/schema-registry/confluent/common/pom.xml b/extensions/schema-registry/confluent/common/pom.xml new file mode 100644 index 0000000000000..9e6255a236371 --- /dev/null +++ b/extensions/schema-registry/confluent/common/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + + quarkus-confluent-registry-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + + quarkus-confluent-registry-common-parent + Quarkus - Confluent Schema Registry - Common + pom + + + deployment + runtime + + + diff --git a/extensions/schema-registry/confluent/common/runtime/pom.xml b/extensions/schema-registry/confluent/common/runtime/pom.xml new file mode 100644 index 0000000000000..9d192312c3236 --- /dev/null +++ b/extensions/schema-registry/confluent/common/runtime/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + + quarkus-confluent-registry-common-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + + quarkus-confluent-registry-common + Quarkus - Confluent Schema Registry - Common - Runtime + + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-schema-registry-devservice + + + + diff --git a/extensions/schema-registry/confluent/pom.xml b/extensions/schema-registry/confluent/pom.xml new file mode 100644 index 0000000000000..08e3f6c6262ee --- /dev/null +++ b/extensions/schema-registry/confluent/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + + quarkus-schema-registry-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + + quarkus-confluent-registry-parent + Quarkus - Confluent Schema Registry + pom + + + common + avro + + diff --git a/extensions/schema-registry/devservice/deployment/pom.xml b/extensions/schema-registry/devservice/deployment/pom.xml new file mode 100644 index 0000000000000..e9d4bc9dc7df7 --- /dev/null +++ b/extensions/schema-registry/devservice/deployment/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + + quarkus-schema-registry-devservice-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + + quarkus-schema-registry-devservice-deployment + Quarkus - Schema Registry - DevService - Deployment + + + + io.quarkus + quarkus-schema-registry-devservice + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-vertx-deployment + + + + io.quarkus + quarkus-devservices-deployment + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/ApicurioRegistryDevServicesBuildTimeConfig.java b/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/ApicurioRegistryDevServicesBuildTimeConfig.java similarity index 93% rename from extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/ApicurioRegistryDevServicesBuildTimeConfig.java rename to extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/ApicurioRegistryDevServicesBuildTimeConfig.java index 15f810d58363e..55ecf543606a0 100644 --- a/extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/ApicurioRegistryDevServicesBuildTimeConfig.java +++ b/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/ApicurioRegistryDevServicesBuildTimeConfig.java @@ -1,4 +1,4 @@ -package io.quarkus.apicurio.registry.avro; +package io.quarkus.apicurio.registry.devservice; import java.util.Optional; @@ -12,7 +12,8 @@ public class ApicurioRegistryDevServicesBuildTimeConfig { /** * If Dev Services for Apicurio Registry has been explicitly enabled or disabled. Dev Services are generally enabled * by default, unless there is an existing configuration present. For Apicurio Registry, Dev Services starts a registry - * unless {@code mp.messaging.connector.smallrye-kafka.apicurio.registry.url} is set. + * unless {@code mp.messaging.connector.smallrye-kafka.apicurio.registry.url} or + * {@code mp.messaging.connector.smallrye-kafka.schema.registry.url} is set. */ @ConfigItem public Optional enabled = Optional.empty(); @@ -29,7 +30,7 @@ public class ApicurioRegistryDevServicesBuildTimeConfig { * The Apicurio Registry image to use. * Note that only Apicurio Registry 2.x images are supported. */ - @ConfigItem(defaultValue = "quay.io/apicurio/apicurio-registry-mem:2.2.0.Final") + @ConfigItem(defaultValue = "quay.io/apicurio/apicurio-registry-mem:2.2.3.Final") public String imageName; /** diff --git a/extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/DevServicesApicurioRegistryProcessor.java b/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/DevServicesApicurioRegistryProcessor.java similarity index 84% rename from extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/DevServicesApicurioRegistryProcessor.java rename to extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/DevServicesApicurioRegistryProcessor.java index 7456b684a2eb7..c363046c7ef4c 100644 --- a/extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/DevServicesApicurioRegistryProcessor.java +++ b/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/DevServicesApicurioRegistryProcessor.java @@ -1,7 +1,8 @@ -package io.quarkus.apicurio.registry.avro; +package io.quarkus.apicurio.registry.devservice; import java.time.Duration; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -12,13 +13,13 @@ import org.testcontainers.utility.DockerImageName; import io.quarkus.deployment.Feature; -import io.quarkus.deployment.IsDockerWorking; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.console.ConsoleInstalledBuildItem; import io.quarkus.deployment.console.StartupLogCompressor; @@ -31,16 +32,14 @@ /** * Starts Apicurio Registry as dev service if needed. - *

- * In the future, when we have multiple Apicurio Registry extensions (Avro, Protobuf, ...), - * this dev service support should probably be moved into an extra extension (quarkus-apicurio-registry-internal). */ public class DevServicesApicurioRegistryProcessor { private static final Logger log = Logger.getLogger(DevServicesApicurioRegistryProcessor.class); private static final int APICURIO_REGISTRY_PORT = 8080; // inside the container - private static final String REGISTRY_URL_CONFIG = "mp.messaging.connector.smallrye-kafka.apicurio.registry.url"; + private static final String APLICURIO_REGISTRY_URL_CONFIG = "mp.messaging.connector.smallrye-kafka.apicurio.registry.url"; + private static final String CONFLUENT_SCHEMA_REGISTRY_URL_CONFIG = "mp.messaging.connector.smallrye-kafka.schema.registry.url"; /** * Label to add to shared Dev Service for Apicurio Registry running in containers. @@ -55,10 +54,9 @@ public class DevServicesApicurioRegistryProcessor { static volatile ApicurioRegistryDevServiceCfg cfg; static volatile boolean first = true; - private final IsDockerWorking isDockerWorking = new IsDockerWorking(true); - @BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) public DevServicesResultBuildItem startApicurioRegistryDevService(LaunchModeBuildItem launchMode, + DockerStatusBuildItem dockerStatusBuildItem, ApicurioRegistryDevServicesBuildTimeConfig apicurioRegistryDevServices, List devServicesSharedNetworkBuildItem, Optional consoleInstalledBuildItem, @@ -79,7 +77,7 @@ public DevServicesResultBuildItem startApicurioRegistryDevService(LaunchModeBuil (launchMode.isTest() ? "(test) " : "") + "Apicurio Registry Dev Services Starting:", consoleInstalledBuildItem, loggingSetupBuildItem); try { - devService = startApicurioRegistry(configuration, launchMode, + devService = startApicurioRegistry(dockerStatusBuildItem, configuration, launchMode, !devServicesSharedNetworkBuildItem.isEmpty(), devServicesConfig.timeout); compressor.close(); } catch (Throwable t) { @@ -95,7 +93,7 @@ public DevServicesResultBuildItem startApicurioRegistryDevService(LaunchModeBuil if (devService.isOwner()) { log.infof("Dev Services for Apicurio Registry started. The registry is available at %s", - devService.getConfig().get(REGISTRY_URL_CONFIG)); + devService.getConfig().get(APLICURIO_REGISTRY_URL_CONFIG)); } // Configure the watch dog @@ -117,8 +115,10 @@ public void run() { return devService.toBuildItem(); } - private String getRegistryUrlConfig(String baseUrl) { - return baseUrl + "/apis/registry/v2"; + private Map getRegistryUrlConfigs(String baseUrl) { + return Map.of( + APLICURIO_REGISTRY_URL_CONFIG, baseUrl + "/apis/registry/v2", + CONFLUENT_SCHEMA_REGISTRY_URL_CONFIG, baseUrl + "/apis/ccompat/v6"); } private void shutdownApicurioRegistry() { @@ -133,7 +133,8 @@ private void shutdownApicurioRegistry() { } } - private RunningDevService startApicurioRegistry(ApicurioRegistryDevServiceCfg config, LaunchModeBuildItem launchMode, + private RunningDevService startApicurioRegistry(DockerStatusBuildItem dockerStatusBuildItem, + ApicurioRegistryDevServiceCfg config, LaunchModeBuildItem launchMode, boolean useSharedNetwork, Optional timeout) { if (!config.devServicesEnabled) { // explicitly disabled @@ -141,17 +142,23 @@ private RunningDevService startApicurioRegistry(ApicurioRegistryDevServiceCfg co return null; } - if (ConfigUtils.isPropertyPresent(REGISTRY_URL_CONFIG)) { - log.debug("Not starting dev services for Apicurio Registry, " + REGISTRY_URL_CONFIG + " is configured."); + if (ConfigUtils.isPropertyPresent(APLICURIO_REGISTRY_URL_CONFIG)) { + log.debug("Not starting dev services for Apicurio Registry, " + APLICURIO_REGISTRY_URL_CONFIG + " is configured."); + return null; + } + + if (ConfigUtils.isPropertyPresent(CONFLUENT_SCHEMA_REGISTRY_URL_CONFIG)) { + log.debug("Not starting dev services for Apicurio Registry, " + CONFLUENT_SCHEMA_REGISTRY_URL_CONFIG + + " is configured."); return null; } - if (!hasKafkaChannelWithoutApicurioRegistry()) { + if (!hasKafkaChannelWithoutRegistry()) { log.debug("Not starting dev services for Apicurio Registry, all the channels have a registry URL configured."); return null; } - if (!isDockerWorking.getAsBoolean()) { + if (!dockerStatusBuildItem.isDockerAvailable()) { log.warn("Docker isn't working, please run Apicurio Registry yourself."); return null; } @@ -159,9 +166,9 @@ private RunningDevService startApicurioRegistry(ApicurioRegistryDevServiceCfg co // Starting the broker return apicurioRegistryContainerLocator.locateContainer(config.serviceName, config.shared, launchMode.getLaunchMode()) .map(address -> new RunningDevService(Feature.APICURIO_REGISTRY_AVRO.getName(), - address.getId(), null, REGISTRY_URL_CONFIG, + address.getId(), null, // address does not have the URL Scheme - just the host:port, so prepend http:// - getRegistryUrlConfig("http://" + address.getUrl()))) + getRegistryUrlConfigs("http://" + address.getUrl()))) .orElseGet(() -> { ApicurioRegistryContainer container = new ApicurioRegistryContainer( DockerImageName.parse(config.imageName), config.fixedExposedPort, @@ -171,11 +178,11 @@ private RunningDevService startApicurioRegistry(ApicurioRegistryDevServiceCfg co container.start(); return new RunningDevService(Feature.APICURIO_REGISTRY_AVRO.getName(), container.getContainerId(), - container::close, REGISTRY_URL_CONFIG, getRegistryUrlConfig(container.getUrl())); + container::close, getRegistryUrlConfigs(container.getUrl())); }); } - private boolean hasKafkaChannelWithoutApicurioRegistry() { + private boolean hasKafkaChannelWithoutRegistry() { Config config = ConfigProvider.getConfig(); for (String name : config.getPropertyNames()) { boolean isIncoming = name.startsWith("mp.messaging.incoming."); @@ -185,7 +192,8 @@ private boolean hasKafkaChannelWithoutApicurioRegistry() { && "smallrye-kafka".equals(config.getOptionalValue(name, String.class).orElse("ignored")); boolean isConfigured = false; if ((isIncoming || isOutgoing) && isKafka) { - isConfigured = ConfigUtils.isPropertyPresent(name.replace(".connector", ".apicurio.registry.url")); + isConfigured = ConfigUtils.isPropertyPresent(name.replace(".connector", ".apicurio.registry.url")) + || ConfigUtils.isPropertyPresent(name.replace(".connector", ".schema.registry.url")); } if (!isConfigured) { return true; diff --git a/extensions/schema-registry/devservice/pom.xml b/extensions/schema-registry/devservice/pom.xml new file mode 100644 index 0000000000000..218ab03ab9dff --- /dev/null +++ b/extensions/schema-registry/devservice/pom.xml @@ -0,0 +1,22 @@ + + + + quarkus-schema-registry-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + + 4.0.0 + quarkus-schema-registry-devservice-parent + Quarkus - Schema Registry - DevService + pom + + + deployment + runtime + + + diff --git a/extensions/schema-registry/devservice/runtime/pom.xml b/extensions/schema-registry/devservice/runtime/pom.xml new file mode 100644 index 0000000000000..a9008082c9df4 --- /dev/null +++ b/extensions/schema-registry/devservice/runtime/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + + quarkus-schema-registry-devservice-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + + quarkus-schema-registry-devservice + Quarkus - Schema Registry - DevService - Runtime + + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-vertx + + + + diff --git a/extensions/schema-registry/pom.xml b/extensions/schema-registry/pom.xml new file mode 100644 index 0000000000000..ac5f78da15335 --- /dev/null +++ b/extensions/schema-registry/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + + quarkus-extensions-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + + quarkus-schema-registry-parent + Quarkus - Schema Registry Parent + pom + + + apicurio + confluent + devservice + + diff --git a/extensions/security-webauthn/deployment/src/main/java/io/quarkus/security/webauthn/deployment/QuarkusSecurityWebAuthnProcessor.java b/extensions/security-webauthn/deployment/src/main/java/io/quarkus/security/webauthn/deployment/QuarkusSecurityWebAuthnProcessor.java index f607e0f7ec781..3c8a08aace334 100644 --- a/extensions/security-webauthn/deployment/src/main/java/io/quarkus/security/webauthn/deployment/QuarkusSecurityWebAuthnProcessor.java +++ b/extensions/security-webauthn/deployment/src/main/java/io/quarkus/security/webauthn/deployment/QuarkusSecurityWebAuthnProcessor.java @@ -13,6 +13,7 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.security.webauthn.WebAuthnAuthenticationMechanism; import io.quarkus.security.webauthn.WebAuthnAuthenticatorStorage; import io.quarkus.security.webauthn.WebAuthnBuildTimeConfig; @@ -23,6 +24,7 @@ import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.VertxWebRouterBuildItem; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.vertx.ext.auth.webauthn.impl.attestation.Attestation; class QuarkusSecurityWebAuthnProcessor { @@ -53,6 +55,11 @@ public void setup( nonApplicationRootPathBuildItem.getNonApplicationRootPath()); } + @BuildStep(onlyIf = IsEnabled.class) + public ServiceProviderBuildItem serviceLoader() { + return ServiceProviderBuildItem.allProvidersFromClassPath(Attestation.class.getName()); + } + @BuildStep(onlyIf = IsEnabled.class) @Record(ExecutionTime.RUNTIME_INIT) SyntheticBeanBuildItem initWebAuthnAuth( diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/JCAProviderBuildItem.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/JCAProviderBuildItem.java index 8c37b803f5a53..35d4b6b11911d 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/JCAProviderBuildItem.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/JCAProviderBuildItem.java @@ -6,13 +6,23 @@ * Metadata for the names of JCA {@linkplain java.security.Provider} to register for reflection */ public final class JCAProviderBuildItem extends MultiBuildItem { - private String providerName; + final private String providerName; + final private String providerConfig; public JCAProviderBuildItem(String providerName) { + this(providerName, null); + } + + public JCAProviderBuildItem(String providerName, String providerConfig) { this.providerName = providerName; + this.providerConfig = providerConfig; } public String getProviderName() { return providerName; } + + public String getProviderConfig() { + return providerConfig; + } } diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityConfig.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityConfig.java index 44c1701855ebd..3b267255a15c0 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityConfig.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityConfig.java @@ -1,7 +1,8 @@ package io.quarkus.security.deployment; -import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; @@ -20,8 +21,14 @@ public final class SecurityConfig { public boolean authorizationEnabledInDevMode; /** - * List of security providers to enable for reflection + * List of security providers to register */ @ConfigItem - public Optional> securityProviders; + public Optional> securityProviders; + + /** + * Security provider configuration + */ + @ConfigItem + public Map securityProviderConfig; } 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 3614198de1c7e..47e1a82a4c012 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 @@ -12,7 +12,6 @@ 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; @@ -94,7 +93,7 @@ public class SecurityProcessor { void produceJcaSecurityProviders(BuildProducer jcaProviders, BuildProducer bouncyCastleProvider, BuildProducer bouncyCastleJsseProvider) { - Set providers = new HashSet<>(security.securityProviders.orElse(Collections.emptyList())); + Set providers = security.securityProviders.orElse(Set.of()); for (String providerName : providers) { if (SecurityProviderUtils.BOUNCYCASTLE_PROVIDER_NAME.equals(providerName)) { bouncyCastleProvider.produce(new BouncyCastleProviderBuildItem()); @@ -105,7 +104,7 @@ void produceJcaSecurityProviders(BuildProducer jcaProvider } else if (SecurityProviderUtils.BOUNCYCASTLE_FIPS_JSSE_PROVIDER_NAME.equals(providerName)) { bouncyCastleJsseProvider.produce(new BouncyCastleJsseProviderBuildItem(true)); } else { - jcaProviders.produce(new JCAProviderBuildItem(providerName)); + jcaProviders.produce(new JCAProviderBuildItem(providerName, security.securityProviderConfig.get(providerName))); } log.debugf("Added providerName: %s", providerName); } @@ -121,9 +120,11 @@ void produceJcaSecurityProviders(BuildProducer jcaProvider */ @BuildStep void registerJCAProvidersForReflection(BuildProducer classes, - List jcaProviders) throws IOException, URISyntaxException { + List jcaProviders, + BuildProducer additionalProviders) throws IOException, URISyntaxException { for (JCAProviderBuildItem provider : jcaProviders) { - List providerClasses = registerProvider(provider.getProviderName()); + List providerClasses = registerProvider(provider.getProviderName(), provider.getProviderConfig(), + additionalProviders); for (String className : providerClasses) { classes.produce(new ReflectiveClassBuildItem(true, true, className)); log.debugf("Register JCA class: %s", className); @@ -352,7 +353,9 @@ private Optional getOne(List items) { * @param providerName - JCA provider name * @return class names that make up the provider and its services */ - private List registerProvider(String providerName) { + private List registerProvider(String providerName, + String providerConfig, + BuildProducer additionalProviders) { List providerClasses = new ArrayList<>(); Provider provider = Security.getProvider(providerName); if (provider != null) { @@ -365,6 +368,19 @@ private List registerProvider(String providerName) { providerClasses.addAll(Arrays.asList(supportedKeyClasses.split("\\|"))); } } + + if (providerConfig != null) { + Provider configuredProvider = provider.configure(providerConfig); + if (configuredProvider != null) { + Security.addProvider(configuredProvider); + providerClasses.add(configuredProvider.getClass().getName()); + } + } + } + + if (SecurityProviderUtils.SUN_PROVIDERS.containsKey(providerName)) { + additionalProviders.produce( + new NativeImageSecurityProviderBuildItem(SecurityProviderUtils.SUN_PROVIDERS.get(providerName))); } return providerClasses; } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityProviderUtils.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityProviderUtils.java index 32370b8c11024..d34a25235ae90 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityProviderUtils.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityProviderUtils.java @@ -3,6 +3,7 @@ import java.lang.reflect.Constructor; import java.security.Provider; import java.security.Security; +import java.util.Map; import io.quarkus.runtime.configuration.ConfigurationException; @@ -18,10 +19,16 @@ public final class SecurityProviderUtils { public static final String BOUNCYCASTLE_JSSE_PROVIDER_CLASS_NAME = "org.bouncycastle.jsse.provider.BouncyCastleJsseProvider"; public static final String BOUNCYCASTLE_FIPS_PROVIDER_CLASS_NAME = "org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider"; + public static final Map SUN_PROVIDERS = Map.of("SunPKCS11", "sun.security.pkcs11.SunPKCS11"); + private SecurityProviderUtils() { } + public static void addProvider(String provider) { + addProvider(loadProvider(provider)); + } + public static void addProvider(Provider provider) { try { if (Security.getProvider(provider.getName()) == null) { diff --git a/extensions/smallrye-graphql-client/deployment/pom.xml b/extensions/smallrye-graphql-client/deployment/pom.xml index 43ba287ad9517..8c20533d1223c 100644 --- a/extensions/smallrye-graphql-client/deployment/pom.xml +++ b/extensions/smallrye-graphql-client/deployment/pom.xml @@ -29,6 +29,10 @@ io.quarkus quarkus-vertx-deployment + + io.quarkus + quarkus-smallrye-stork-deployment + io.quarkus quarkus-smallrye-graphql-client @@ -49,6 +53,11 @@ quarkus-smallrye-graphql-deployment test + + io.smallrye.stork + stork-service-discovery-static-list + test + @@ -70,4 +79,4 @@ - \ No newline at end of file + diff --git a/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/StorkAndGraphQLClientTest.java b/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/StorkAndGraphQLClientTest.java new file mode 100644 index 0000000000000..70e0f1e9fb9f0 --- /dev/null +++ b/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/StorkAndGraphQLClientTest.java @@ -0,0 +1,42 @@ +package io.quarkus.smallrye.graphql.client.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import javax.inject.Inject; + +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.smallrye.graphql.client.deployment.model.Person; +import io.quarkus.smallrye.graphql.client.deployment.model.TestingGraphQLApi; +import io.quarkus.smallrye.graphql.client.deployment.model.TestingGraphQLClientApi; +import io.quarkus.test.QuarkusUnitTest; + +public class StorkAndGraphQLClientTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(TestingGraphQLApi.class, TestingGraphQLClientApi.class, Person.class) + .addAsResource(new StringAsset( + "quarkus.smallrye-graphql-client.typesafeclient.url=stork://foo-service/graphql\n" + + "quarkus.stork.foo-service.service-discovery.type=static\n" + + "quarkus.stork.foo-service.service-discovery.address-list=${quarkus.http.host:localhost}:${quarkus.http.test-port:8081}"), + "application.properties") + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); + + @Inject + TestingGraphQLClientApi client; + + @Test + public void performQuery() { + List people = client.people(); + assertEquals("John", people.get(0).getFirstName()); + assertEquals("Arthur", people.get(1).getFirstName()); + } + +} diff --git a/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/model/TestingGraphQLApi.java b/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/model/TestingGraphQLApi.java index 766c8a340e92d..42abcf84dd638 100644 --- a/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/model/TestingGraphQLApi.java +++ b/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/model/TestingGraphQLApi.java @@ -12,6 +12,9 @@ @GraphQLApi public class TestingGraphQLApi { + @Inject + CurrentVertxRequest request; + @Query public List people() { Person person1 = new Person(); @@ -25,9 +28,6 @@ public List people() { return List.of(person1, person2); } - @Inject - CurrentVertxRequest request; - /** * Returns the value of the HTTP header denoted by 'key'. */ diff --git a/extensions/smallrye-graphql-client/runtime/pom.xml b/extensions/smallrye-graphql-client/runtime/pom.xml index a52ac59907745..c825d628c6917 100644 --- a/extensions/smallrye-graphql-client/runtime/pom.xml +++ b/extensions/smallrye-graphql-client/runtime/pom.xml @@ -30,10 +30,19 @@ io.quarkus quarkus-vertx - + + io.quarkus + quarkus-smallrye-stork + io.smallrye smallrye-graphql-client-implementation-vertx + + + org.glassfish + jakarta.json + + diff --git a/extensions/smallrye-graphql/deployment/pom.xml b/extensions/smallrye-graphql/deployment/pom.xml index 711280f6bbf03..e2feaa37733a6 100644 --- a/extensions/smallrye-graphql/deployment/pom.xml +++ b/extensions/smallrye-graphql/deployment/pom.xml @@ -53,6 +53,13 @@ quarkus-smallrye-context-propagation-deployment + + io.quarkus + quarkus-hibernate-validator-deployment + true + provided + + io.quarkus @@ -94,11 +101,6 @@ opentracing-mock test - - io.quarkus - quarkus-hibernate-validator-deployment - test - 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 97ae812f44928..18e154dac28d8 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 @@ -51,6 +51,7 @@ import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLConfig; import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLConfigMapping; +import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLLocaleResolver; import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLRecorder; import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLRuntimeConfig; import io.quarkus.vertx.http.deployment.BodyHandlerBuildItem; @@ -65,7 +66,6 @@ import io.smallrye.graphql.cdi.config.ConfigKey; import io.smallrye.graphql.cdi.config.MicroProfileConfig; import io.smallrye.graphql.cdi.producer.GraphQLProducer; -import io.smallrye.graphql.cdi.producer.SmallRyeContextAccessorProxy; import io.smallrye.graphql.schema.Annotations; import io.smallrye.graphql.schema.SchemaBuilder; import io.smallrye.graphql.schema.model.Argument; @@ -143,11 +143,16 @@ void additionalBeanDefiningAnnotation(BuildProducer additionalBeanProducer) { + void additionalBean(Capabilities capabilities, BuildProducer additionalBeanProducer) { + additionalBeanProducer.produce(AdditionalBeanBuildItem.builder() .addBeanClass(GraphQLProducer.class) - .addBeanClass(SmallRyeContextAccessorProxy.class) .setUnremovable().build()); + if (capabilities.isPresent(Capability.HIBERNATE_VALIDATOR)) { + additionalBeanProducer.produce(AdditionalBeanBuildItem.builder() + .addBeanClass(SmallRyeGraphQLLocaleResolver.class) + .setUnremovable().build()); + } } @BuildStep @@ -249,7 +254,6 @@ void buildSchemaEndpoint( .nestedRoute(graphQLConfig.rootPath, SCHEMA_PATH) .handler(schemaHandler) .displayOnNotFoundPage("MicroProfile GraphQL Schema") - .blockingRoute() .build()); } @@ -286,10 +290,10 @@ void buildExecutionEndpoint( Handler graphqlOverWebsocketHandler = recorder .graphqlOverWebsocketHandler(beanContainer.getValue(), graphQLInitializedBuildItem.getInitialized()); - routeProducer.produce(httpRootPathBuildItem.routeBuilder() + HttpRootPathBuildItem.Builder subscriptionsBuilder = httpRootPathBuildItem.routeBuilder() .orderedRoute(graphQLConfig.rootPath, Integer.MIN_VALUE) - .handler(graphqlOverWebsocketHandler) - .build()); + .handler(graphqlOverWebsocketHandler); + routeProducer.produce(subscriptionsBuilder.build()); // WebSocket subprotocols graphQLConfig.websocketSubprotocols.ifPresentOrElse(subprotocols -> { @@ -308,18 +312,31 @@ void buildExecutionEndpoint( }); // Queries and Mutations + boolean runBlocking = shouldRunBlockingRoute(graphQLConfig); boolean allowGet = getBooleanConfigValue(ConfigKey.ALLOW_GET, false); boolean allowQueryParametersOnPost = getBooleanConfigValue(ConfigKey.ALLOW_POST_WITH_QUERY_PARAMETERS, false); Handler executionHandler = recorder.executionHandler(graphQLInitializedBuildItem.getInitialized(), - allowGet, allowQueryParametersOnPost); - routeProducer.produce(httpRootPathBuildItem.routeBuilder() + allowGet, allowQueryParametersOnPost, runBlocking); + + HttpRootPathBuildItem.Builder requestBuilder = httpRootPathBuildItem.routeBuilder() .routeFunction(graphQLConfig.rootPath, recorder.routeFunction(bodyHandlerBuildItem.getHandler())) .handler(executionHandler) .routeConfigKey("quarkus.smallrye-graphql.root-path") - .displayOnNotFoundPage("MicroProfile GraphQL Endpoint") - .blockingRoute() - .build()); + .displayOnNotFoundPage("MicroProfile GraphQL Endpoint"); + if (runBlocking) { + requestBuilder = requestBuilder.blockingRoute(); + } + + routeProducer.produce(requestBuilder.build()); + + } + + private boolean shouldRunBlockingRoute(SmallRyeGraphQLConfig graphQLConfig) { + if (graphQLConfig.nonBlockingEnabled.isPresent()) { + return !graphQLConfig.nonBlockingEnabled.get(); + } + return false; } private boolean getBooleanConfigValue(String smallryeKey, boolean defaultValue) { @@ -338,7 +355,7 @@ private String[] getSchemaJavaClasses(Schema schema) { classes.addAll(getInputClassNames(schema.getInputs().values())); classes.addAll(getInterfaceClassNames(schema.getInterfaces().values())); - return classes.toArray(new String[] {}); + return classes.toArray(String[]::new); } private Class[] getGraphQLJavaClasses() { @@ -361,7 +378,7 @@ private Class[] getGraphQLJavaClasses() { classes.add(graphql.schema.GraphQLTypeReference.class); classes.add(List.class); classes.add(Collection.class); - return classes.toArray(new Class[] {}); + return classes.toArray(Class[]::new); } private Set getOperationClassNames(Set operations) { diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/BeanValidationGraphQLDirectivesTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/BeanValidationGraphQLDirectivesTest.java new file mode 100644 index 0000000000000..faab1536063f1 --- /dev/null +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/BeanValidationGraphQLDirectivesTest.java @@ -0,0 +1,72 @@ +package io.quarkus.smallrye.graphql.deployment; + +import static io.restassured.RestAssured.get; +import static org.hamcrest.Matchers.containsString; + +import javax.validation.Valid; +import javax.validation.constraints.Size; + +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; + +/** + * Basic test to verify that bean validation constraints on input fields are transformed into GraphQL directives when + * the option to include directives in the schema is enabled. + */ +public class BeanValidationGraphQLDirectivesTest extends AbstractGraphQLTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Person.class) + .addAsResource(new StringAsset("quarkus.smallrye-graphql.schema-include-directives=true"), + "application.properties") + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); + + @Test + public void validateDirectivesPresentInSchema() { + get("/graphql/schema.graphql") + .then() + .body(containsString("input PersonInput {\n" + + " name: String @constraint(maxLength : 20, minLength : 5)\n" + + "}\n")) + .body(containsString( + "queryWithConstrainedArgument(constrained: String @constraint(maxLength : 123)): String")); + } + + @GraphQLApi + public static class ValidationApi { + + @Query + public String query(@Valid Person person) { + return null; + } + + @Query + public String queryWithConstrainedArgument(@Size(max = 123) String constrained) { + return null; + } + + } + + public static class Person { + + @Size(min = 5, max = 20) + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + +} diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/ErrorTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/ErrorTest.java new file mode 100644 index 0000000000000..2b86563923544 --- /dev/null +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/ErrorTest.java @@ -0,0 +1,83 @@ +package io.quarkus.smallrye.graphql.deployment; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +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.common.annotation.NonBlocking; + +public class ErrorTest extends AbstractGraphQLTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(ErrorApi.class, Foo.class) + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); + + @Test + public void testNonBlockingError() { + String query = getPayload("{ foo { message} }"); + RestAssured.given() + .body(query) + .contentType(MEDIATYPE_JSON) + .post("/graphql/") + .then() + .assertThat() + .statusCode(500); + } + + @Test + public void testBlockingError() { + String query = getPayload("{ blockingFoo { message} }"); + RestAssured.given() + .body(query) + .contentType(MEDIATYPE_JSON) + .post("/graphql/") + .then() + .log().everything() + .assertThat() + .statusCode(500); + } + + public static class Foo { + + private String message; + + public Foo(String foo) { + this.message = foo; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + } + + @GraphQLApi + @ApplicationScoped + public static class ErrorApi { + + @Query + @NonBlocking + public Foo foo() { + throw new OutOfMemoryError("a SuperHero has used all the memory"); + } + + @Query + public Foo blockingFoo() { + throw new OutOfMemoryError("a SuperHero has used all the memory"); + } + + } + +} diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLBlockingModeTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLBlockingModeTest.java new file mode 100644 index 0000000000000..26a5e44d04698 --- /dev/null +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLBlockingModeTest.java @@ -0,0 +1,447 @@ +package io.quarkus.smallrye.graphql.deployment; + +import static io.quarkus.smallrye.graphql.deployment.AbstractGraphQLTest.MEDIATYPE_JSON; + +import java.util.concurrent.CompletionStage; + +import javax.inject.Inject; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +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.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.smallrye.common.annotation.Blocking; +import io.smallrye.common.annotation.NonBlocking; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Context; +import io.vertx.core.Vertx; + +/** + * Testing the tread used. + */ +public class GraphQLBlockingModeTest extends AbstractGraphQLTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((JavaArchive jar) -> jar + .addClasses(TestThreadResource.class, TestThread.class) + .addAsResource(new StringAsset("quarkus.smallrye-graphql.nonblocking.enabled=false"), + "application.properties") + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); + + @Test + public void testOnlyObject() { + + String fooRequest = getPayload("{\n" + + " onlyObject {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.onlyObject.name", Matchers.startsWith("executor-thread")) + .and() + .body("data.onlyObject.vertxContextClassName", Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + } + + @Test + public void testAnnotatedNonBlockingObject() { + + String fooRequest = getPayload("{\n" + + " annotatedNonBlockingObject {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.annotatedNonBlockingObject.name", Matchers.startsWith("executor-thread")) + .and() + .body("data.annotatedNonBlockingObject.vertxContextClassName", + Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + } + + @Test + public void testAnnotatedBlockingObject() { + + String fooRequest = getPayload("{\n" + + " annotatedBlockingObject {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.annotatedBlockingObject.name", Matchers.startsWith("executor-thread")) + .and() + .body("data.annotatedBlockingObject.vertxContextClassName", + Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + } + + @Test + public void testOnlyReactiveUni() { + + String fooRequest = getPayload("{\n" + + " onlyReactiveUni {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.onlyReactiveUni.name", Matchers.startsWith("executor-thread")) + .and() + .body("data.onlyReactiveUni.vertxContextClassName", Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + } + + @Test + public void testAnnotatedBlockingReactiveUni() { + + String fooRequest = getPayload("{\n" + + " annotatedBlockingReactiveUni {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.annotatedBlockingReactiveUni.name", Matchers.startsWith("executor-thread")) + .and() + .body("data.annotatedBlockingReactiveUni.vertxContextClassName", + Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + + } + + @Test + public void testAnnotatedNonBlockingReactiveUni() { + + String fooRequest = getPayload("{\n" + + " annotatedNonBlockingReactiveUni {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.annotatedNonBlockingReactiveUni.name", Matchers.startsWith("executor-thread")) + .and() + .body("data.annotatedNonBlockingReactiveUni.vertxContextClassName", + Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + + } + + @Test + public void testOnlyCompletionStage() { + + String fooRequest = getPayload("{\n" + + " onlyCompletionStage {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.onlyCompletionStage.name", Matchers.startsWith("executor-thread")) + .and() + .body("data.onlyCompletionStage.vertxContextClassName", + Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + + } + + @Test + public void testAnnotatedBlockingCompletionStage() { + + String fooRequest = getPayload("{\n" + + " annotatedBlockingCompletionStage {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.annotatedBlockingCompletionStage.name", Matchers.startsWith("executor-thread")) + .and() + .body("data.annotatedBlockingCompletionStage.vertxContextClassName", + Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + + } + + @Test + public void testAnnotatedNonBlockingCompletionStage() { + + String fooRequest = getPayload("{\n" + + " annotatedNonBlockingCompletionStage {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.annotatedNonBlockingCompletionStage.name", Matchers.startsWith("executor-thread")) + .and() + .body("data.annotatedNonBlockingCompletionStage.vertxContextClassName", + Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + + } + + @GraphQLApi + public static class TestThreadResource { + + @Inject + Vertx vertx; + + // Return type Object + @Query + public TestThread onlyObject() { + return getTestThread(); + } + + // Return type Object, Annotated with @NonBlocking + @Query + @NonBlocking + public TestThread annotatedNonBlockingObject() { + return getTestThread(); + } + + // Return type Object with @Blocking (default) + @Query + @Blocking + public TestThread annotatedBlockingObject() { + return getTestThread(); + } + + // Return type Uni + @Query + public Uni onlyReactiveUni() { + return Uni.createFrom().item(() -> getTestThread()); + } + + // Return type Reactive with @Blocking + @Query + @Blocking + public Uni annotatedBlockingReactiveUni() { + return Uni.createFrom().item(() -> getTestThread()); + } + + // Return type Reactive with @NonBlocking (default) + @Query + @NonBlocking + public Uni annotatedNonBlockingReactiveUni() { + return Uni.createFrom().item(() -> getTestThread()); + } + + @Query + public CompletionStage onlyCompletionStage() { + return Uni.createFrom().item(() -> getTestThread()).subscribeAsCompletionStage(); + } + + // Return type CompletionStage with @Blocking + @Query + @Blocking + public CompletionStage annotatedBlockingCompletionStage() { + return Uni.createFrom().item(() -> getTestThread()).subscribeAsCompletionStage(); + } + + // Return type CompletionStage with @NonBlocking (default) + @Query + @NonBlocking + public CompletionStage annotatedNonBlockingCompletionStage() { + return Uni.createFrom().item(() -> getTestThread()).subscribeAsCompletionStage(); + } + + private TestThread getTestThread() { + Thread t = Thread.currentThread(); + long id = t.getId(); + String name = t.getName(); + int priority = t.getPriority(); + String state = t.getState().name(); + String group = t.getThreadGroup().getName(); + return new TestThread(id, name, priority, state, group); + } + } + + /** + * Hold info about a thread + */ + public static class TestThread { + + private long id; + private String name; + private int priority; + private String state; + private String group; + + public TestThread() { + super(); + } + + public TestThread(long id, String name, int priority, String state, String group) { + this.id = id; + this.name = name; + this.priority = priority; + this.state = state; + this.group = group; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public String getVertxContextClassName() { + Context vc = Vertx.currentContext(); + return vc.getClass().getName(); + } + } +} diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLCDIContextPropagationTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLCDIContextPropagationTest.java index 0cffe3ddc95c1..ef29ff2a41612 100644 --- a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLCDIContextPropagationTest.java +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLCDIContextPropagationTest.java @@ -149,7 +149,8 @@ public CompletionStage getPojoWithProgrammaticContextPropagation() { @Name("duplicatedMessage") public List duplicatedMessage(@Source List pojos) { if (!this.injectedBean.getId().equals(this.injectedBeanId)) { - throw new IllegalStateException("duplicatedMessage must be executed in the same request context as getPojos"); + throw new IllegalStateException("duplicatedMessage must be executed in the same request context as getPojos [" + + this.injectedBean.getId() + " != " + this.injectedBeanId); } return pojos.stream() .map(pojo -> pojo.getMessage() + pojo.getMessage()) @@ -163,7 +164,8 @@ public List duplicatedMessage(@Source List pojos) { public CompletionStage> duplicatedMessageAsync(@Source List pojos) { if (!this.injectedBean.getId().equals(this.injectedBeanId)) { throw new IllegalStateException( - "duplicatedMessageAsync must be executed in the same request context as getPojos"); + "duplicatedMessageAsync must be executed in the same request context as getPojos [" + + this.injectedBean.getId() + " != " + this.injectedBeanId); } return CompletableFuture.completedFuture(pojos.stream() .map(pojo -> pojo.getMessage() + pojo.getMessage()) diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLThreadTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLThreadTest.java new file mode 100644 index 0000000000000..0566b1ddcaeed --- /dev/null +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLThreadTest.java @@ -0,0 +1,482 @@ +package io.quarkus.smallrye.graphql.deployment; + +import static io.quarkus.smallrye.graphql.deployment.AbstractGraphQLTest.MEDIATYPE_JSON; + +import java.util.concurrent.CompletionStage; + +import javax.inject.Inject; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +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; +import io.smallrye.common.annotation.Blocking; +import io.smallrye.common.annotation.NonBlocking; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Context; +import io.vertx.core.Vertx; + +/** + * Testing the tread used. + */ +public class GraphQLThreadTest extends AbstractGraphQLTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((JavaArchive jar) -> jar + .addClasses(TestThreadResource.class, TestThread.class) + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); + + @Test + public void testOnlyObject() { + + String fooRequest = getPayload("{\n" + + " onlyObject {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.onlyObject.name", Matchers.startsWith("executor-thread")) + .and() + .body("data.onlyObject.vertxContextClassName", Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + } + + @Test + public void testAnnotatedNonBlockingObject() { + + String fooRequest = getPayload("{\n" + + " annotatedNonBlockingObject {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.annotatedNonBlockingObject.name", Matchers.startsWith("vert.x-eventloop-thread")) + .and() + .body("data.annotatedNonBlockingObject.vertxContextClassName", + Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + } + + @Test + public void testAnnotatedBlockingObject() { + + String fooRequest = getPayload("{\n" + + " annotatedBlockingObject {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.annotatedBlockingObject.name", Matchers.startsWith("executor-thread")) + .and() + .body("data.annotatedBlockingObject.vertxContextClassName", + Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + } + + @Test + public void testOnlyReactiveUni() { + + String fooRequest = getPayload("{\n" + + " onlyReactiveUni {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.onlyReactiveUni.name", Matchers.startsWith("vert.x-eventloop-thread")) + .and() + .body("data.onlyReactiveUni.vertxContextClassName", Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + } + + @Test + public void testOnlyReactiveUniWithDelay() { + + String fooRequest = getPayload("{\n" + + " onlyReactiveUniWithDelay {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.onlyReactiveUniWithDelay.name", Matchers.startsWith("vert.x-eventloop-thread")) + .and() + .body("data.onlyReactiveUniWithDelay.vertxContextClassName", + Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + } + + @Test + public void testAnnotatedBlockingReactiveUni() { + + String fooRequest = getPayload("{\n" + + " annotatedBlockingReactiveUni {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.annotatedBlockingReactiveUni.name", Matchers.startsWith("executor-thread")) + .and() + .body("data.annotatedBlockingReactiveUni.vertxContextClassName", + Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + + } + + @Test + public void testAnnotatedNonBlockingReactiveUni() { + + String fooRequest = getPayload("{\n" + + " annotatedNonBlockingReactiveUni {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.annotatedNonBlockingReactiveUni.name", Matchers.startsWith("vert.x-eventloop-thread")) + .and() + .body("data.annotatedNonBlockingReactiveUni.vertxContextClassName", + Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + + } + + @Test + public void testOnlyCompletionStage() { + + String fooRequest = getPayload("{\n" + + " onlyCompletionStage {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.onlyCompletionStage.name", Matchers.startsWith("vert.x-eventloop-thread")) + .and() + .body("data.onlyCompletionStage.vertxContextClassName", + Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + + } + + @Test + public void testAnnotatedBlockingCompletionStage() { + + String fooRequest = getPayload("{\n" + + " annotatedBlockingCompletionStage {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.annotatedBlockingCompletionStage.name", Matchers.startsWith("executor-thread")) + .and() + .body("data.annotatedBlockingCompletionStage.vertxContextClassName", + Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + + } + + @Test + public void testAnnotatedNonBlockingCompletionStage() { + + String fooRequest = getPayload("{\n" + + " annotatedNonBlockingCompletionStage {\n" + + " name\n" + + " priority\n" + + " state\n" + + " group\n" + + " vertxContextClassName\n" + + " }\n" + + "}"); + + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .log().body().and() + .body("data.annotatedNonBlockingCompletionStage.name", Matchers.startsWith("vert.x-eventloop-thread")) + .and() + .body("data.annotatedNonBlockingCompletionStage.vertxContextClassName", + Matchers.equalTo("io.vertx.core.impl.DuplicatedContext")); + + } + + @GraphQLApi + public static class TestThreadResource { + + @Inject + Vertx vertx; + + // Return type Object + @Query + public TestThread onlyObject() { + return getTestThread(); + } + + // Return type Object, Annotated with @NonBlocking + @Query + @NonBlocking + public TestThread annotatedNonBlockingObject() { + return getTestThread(); + } + + // Return type Object with @Blocking (default) + @Query + @Blocking + public TestThread annotatedBlockingObject() { + return getTestThread(); + } + + // Return type Uni + @Query + public Uni onlyReactiveUni() { + return Uni.createFrom().item(() -> getTestThread()); + } + + // Return type Uni With Delay + @Query + public Uni onlyReactiveUniWithDelay() { + return Uni.createFrom().emitter( + emitter -> { + vertx.setTimer(1000, x -> emitter.complete(getTestThread())); + }); + } + + // Return type Reactive with @Blocking + @Query + @Blocking + public Uni annotatedBlockingReactiveUni() { + return Uni.createFrom().item(() -> getTestThread()); + } + + // Return type Reactive with @NonBlocking (default) + @Query + @NonBlocking + public Uni annotatedNonBlockingReactiveUni() { + return Uni.createFrom().item(() -> getTestThread()); + } + + @Query + public CompletionStage onlyCompletionStage() { + return Uni.createFrom().item(() -> getTestThread()).subscribeAsCompletionStage(); + } + + // Return type CompletionStage with @Blocking + @Query + @Blocking + public CompletionStage annotatedBlockingCompletionStage() { + return Uni.createFrom().item(() -> getTestThread()).subscribeAsCompletionStage(); + } + + // Return type CompletionStage with @NonBlocking (default) + @Query + @NonBlocking + public CompletionStage annotatedNonBlockingCompletionStage() { + return Uni.createFrom().item(() -> getTestThread()).subscribeAsCompletionStage(); + } + + private TestThread getTestThread() { + Thread t = Thread.currentThread(); + long id = t.getId(); + String name = t.getName(); + int priority = t.getPriority(); + String state = t.getState().name(); + String group = t.getThreadGroup().getName(); + return new TestThread(id, name, priority, state, group); + } + } + + /** + * Hold info about a thread + */ + public static class TestThread { + + private long id; + private String name; + private int priority; + private String state; + private String group; + + public TestThread() { + super(); + } + + public TestThread(long id, String name, int priority, String state, String group) { + this.id = id; + this.name = name; + this.priority = priority; + this.state = state; + this.group = group; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public String getVertxContextClassName() { + Context vc = Vertx.currentContext(); + return vc.getClass().getName(); + } + } +} diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLTracingTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLTracingTest.java index 06ddc3594e9ed..83a73610a1ad7 100644 --- a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLTracingTest.java +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLTracingTest.java @@ -28,7 +28,7 @@ public class GraphQLTracingTest extends AbstractGraphQLTest { static MockTracer mockTracer = new MockTracer(); static { - GlobalTracer.register(mockTracer); + GlobalTracer.registerIfAbsent(mockTracer); } @BeforeEach diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLValidationMessagesLocalizationTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLValidationMessagesLocalizationTest.java index 48de03e8adeda..ef8cd46b9aede 100644 --- a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLValidationMessagesLocalizationTest.java +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLValidationMessagesLocalizationTest.java @@ -14,6 +14,8 @@ import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; import io.restassured.http.Header; +import io.smallrye.common.annotation.Blocking; +import io.smallrye.common.annotation.NonBlocking; /** * Test for localization of bean validation messages from a GraphQL endpoint, @@ -43,10 +45,48 @@ public class GraphQLValidationMessagesLocalizationTest extends AbstractGraphQLTe public static class ApiWithValidation { @Query + @NonBlocking public String echo(@Size(max = 4, message = "{message.too.long}") String input) { return input; } + @Query + @Blocking + public String echoBlocking(@Size(max = 4, message = "{message.too.long}") String input) { + return input; + } + } + + @Test + public void testAcceptSpanishBlocking() { + String query = getPayload("{echoBlocking(input:\"TOO LONG\")}"); + + RestAssured.given() + .body(query) + .contentType(MEDIATYPE_JSON) + .header(new Header("Accept-Language", "es")) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .body("errors[0].message", containsString(ERROR_MESSAGE_SPANISH)); + } + + @Test + public void testDefaultLocaleBlocking() { + String query = getPayload("{echoBlocking(input:\"TOO LONG\")}"); + + // default language should be German because we set quarkus.default-locale=de_DE + RestAssured.given() + .body(query) + .contentType(MEDIATYPE_JSON) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .body("errors[0].message", containsString(ERROR_MESSAGE_GERMAN)); } @Test diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/SecurityTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/SecurityTest.java index 6811a5fdbe264..807b4d3dd53e8 100644 --- a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/SecurityTest.java +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/SecurityTest.java @@ -21,6 +21,7 @@ import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; import io.restassured.http.Header; +import io.smallrye.common.annotation.NonBlocking; public class SecurityTest extends AbstractGraphQLTest { @@ -61,6 +62,34 @@ public void testAuthenticatedUserWithSource() { .body("data.foo.bonusFoo", equalTo("bonus")); } + @Test + public void testAuthenticatedUserBlocking() { + String query = getPayload("{ blockingFoo { message} }"); + RestAssured.given() + .header(new Header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz")) + .body(query) + .contentType(MEDIATYPE_JSON) + .post("/graphql/") + .then() + .assertThat() + .body("errors", nullValue()) + .body("data.blockingFoo.message", equalTo("foo")); + } + + @Test + public void testAuthenticatedUserWithSourceBlocking() { + String query = getPayload("{ blockingFoo { blockingBonusFoo } }"); + RestAssured.given() + .header(new Header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz")) + .body(query) + .contentType(MEDIATYPE_JSON) + .post("/graphql/") + .then() + .assertThat() + .body("errors", nullValue()) + .body("data.blockingFoo.blockingBonusFoo", equalTo("bonus")); + } + @Test public void testUnauthorizedRole() { String query = getPayload("{ bar { message } }"); @@ -117,16 +146,30 @@ public static class SecuredApi { @Query @RolesAllowed("fooRole") + @NonBlocking public Foo foo() { return new Foo("foo"); } @Name("bonusFoo") @RolesAllowed("fooRole") + @NonBlocking public List bonusFoo(@Source List foos) { return foos.stream().map(foo -> "bonus").collect(Collectors.toList()); } + @Query + @RolesAllowed("fooRole") + public Foo blockingFoo() { + return new Foo("foo"); + } + + @Name("blockingBonusFoo") + @RolesAllowed("fooRole") + public List blockingBonusFoo(@Source List foos) { + return foos.stream().map(foo -> "bonus").collect(Collectors.toList()); + } + @Query @RolesAllowed("barRole") public Foo bar() { diff --git a/extensions/smallrye-graphql/deployment/src/test/resources/application-secured.properties b/extensions/smallrye-graphql/deployment/src/test/resources/application-secured.properties index dbe0d0ba62978..cd7522f2e5a59 100644 --- a/extensions/smallrye-graphql/deployment/src/test/resources/application-secured.properties +++ b/extensions/smallrye-graphql/deployment/src/test/resources/application-secured.properties @@ -2,4 +2,8 @@ quarkus.security.users.file.enabled=true quarkus.security.users.file.plain-text=true quarkus.security.users.file.users=users.properties quarkus.security.users.file.roles=roles.properties -quarkus.http.auth.basic=true \ No newline at end of file +quarkus.http.auth.basic=true + +quarkus.smallrye-graphql.log-payload=queryAndVariables +quarkus.smallrye-graphql.print-data-fetcher-exception=true +quarkus.smallrye-graphql.error-extension-fields=exception,classification,code,description,validationErrorType,queryPath \ No newline at end of file diff --git a/extensions/smallrye-graphql/runtime/pom.xml b/extensions/smallrye-graphql/runtime/pom.xml index b15446b6d2a70..40a92c3a6f4c2 100644 --- a/extensions/smallrye-graphql/runtime/pom.xml +++ b/extensions/smallrye-graphql/runtime/pom.xml @@ -47,6 +47,12 @@ org.eclipse.microprofile.metrics microprofile-metrics-api + + io.quarkus + quarkus-hibernate-validator + true + provided + diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLAbstractHandler.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLAbstractHandler.java index 5441b0d7a8d47..0f30abe98176e 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLAbstractHandler.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLAbstractHandler.java @@ -24,6 +24,7 @@ public abstract class SmallRyeGraphQLAbstractHandler implements Handler { + currentManagedContext.terminate(); + }); } } - private void handleWithIdentity(final RoutingContext ctx) { + private Void handleWithIdentity(final RoutingContext ctx) { if (currentIdentityAssociation != null) { QuarkusHttpUser existing = (QuarkusHttpUser) ctx.user(); if (existing != null) { @@ -64,6 +67,7 @@ private void handleWithIdentity(final RoutingContext ctx) { } currentVertxRequest.setCurrent(ctx); doHandle(ctx); + return null; } protected abstract void doHandle(final RoutingContext ctx); diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLConfig.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLConfig.java index 5d745d22927f0..865d5d511f4dd 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLConfig.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLConfig.java @@ -44,6 +44,12 @@ public class SmallRyeGraphQLConfig { @ConfigItem(name = "events.enabled", defaultValue = "false") public boolean eventsEnabled; + /** + * Enable non-blocking support. Default is true. + */ + @ConfigItem(name = "nonblocking.enabled") + public Optional nonBlockingEnabled; + /** * Enable GET Requests. Allow queries via HTTP GET. */ diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLExecutionHandler.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLExecutionHandler.java index 78da47f5f5662..fbecebb4aa3d6 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLExecutionHandler.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLExecutionHandler.java @@ -5,7 +5,10 @@ import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; import javax.json.Json; @@ -13,9 +16,14 @@ import javax.json.JsonObjectBuilder; import javax.json.JsonReader; +import graphql.ErrorType; +import graphql.ExecutionResult; +import graphql.GraphQLError; import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.smallrye.graphql.execution.ExecutionResponse; +import io.smallrye.graphql.execution.ExecutionResponseWriter; +import io.vertx.core.MultiMap; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; @@ -28,8 +36,9 @@ * Handler that does the execution of GraphQL Requests */ public class SmallRyeGraphQLExecutionHandler extends SmallRyeGraphQLAbstractHandler { - private boolean allowGet = false; - private boolean allowPostWithQueryParameters = false; + private final boolean allowGet; + private final boolean allowPostWithQueryParameters; + private final boolean runBlocking; private static final String QUERY = "query"; private static final String OPERATION_NAME = "operationName"; private static final String VARIABLES = "variables"; @@ -42,12 +51,13 @@ public class SmallRyeGraphQLExecutionHandler extends SmallRyeGraphQLAbstractHand + StandardCharsets.UTF_8.name(); private static final String MISSING_OPERATION = "Missing operation body"; - public SmallRyeGraphQLExecutionHandler(boolean allowGet, boolean allowPostWithQueryParameters, + public SmallRyeGraphQLExecutionHandler(boolean allowGet, boolean allowPostWithQueryParameters, boolean runBlocking, CurrentIdentityAssociation currentIdentityAssociation, CurrentVertxRequest currentVertxRequest) { super(currentIdentityAssociation, currentVertxRequest); this.allowGet = allowGet; this.allowPostWithQueryParameters = allowPostWithQueryParameters; + this.runBlocking = runBlocking; } @Override @@ -90,7 +100,6 @@ private void handleOptions(HttpServerResponse response) { private void handlePost(HttpServerResponse response, RoutingContext ctx, String requestedCharset) { try { JsonObject jsonObjectFromBody = getJsonObjectFromBody(ctx); - String postResponse; if (hasQueryParameters(ctx) && allowPostWithQueryParameters) { JsonObject jsonObjectFromQueryParameters = getJsonObjectFromQueryParameters(ctx); JsonObject mergedJsonObject; @@ -104,15 +113,14 @@ private void handlePost(HttpServerResponse response, RoutingContext ctx, String response.setStatusCode(400).end(MISSING_OPERATION); return; } - postResponse = doRequest(mergedJsonObject); + doRequest(mergedJsonObject, response, ctx, requestedCharset); } else { if (jsonObjectFromBody == null) { response.setStatusCode(400).end(MISSING_OPERATION); return; } - postResponse = doRequest(jsonObjectFromBody); + doRequest(jsonObjectFromBody, response, ctx, requestedCharset); } - response.setStatusCode(200).setStatusMessage(OK).end(Buffer.buffer(postResponse, requestedCharset)); } catch (IOException ex) { throw new RuntimeException(ex); } @@ -124,11 +132,7 @@ private void handleGet(HttpServerResponse response, RoutingContext ctx, String r JsonObject input = getJsonObjectFromQueryParameters(ctx); if (input.containsKey(QUERY)) { - String getResponse = doRequest(input); - response.setStatusCode(200) - .setStatusMessage(OK) - .end(Buffer.buffer(getResponse, requestedCharset)); - + doRequest(input, response, ctx, requestedCharset); } else { response.setStatusCode(400).end(MISSING_OPERATION); } @@ -281,10 +285,7 @@ private boolean hasQueryParameters(RoutingContext ctx) { private boolean hasQueryParameter(RoutingContext ctx, String parameterName) { List all = ctx.queryParam(parameterName); - if (all != null && !all.isEmpty()) { - return true; - } - return false; + return all != null && !all.isEmpty(); } private String getAllowedMethods() { @@ -295,12 +296,23 @@ private String getAllowedMethods() { } } - private String doRequest(JsonObject jsonInput) { - ExecutionResponse executionResponse = getExecutionService().execute(jsonInput); - if (executionResponse != null) { - return executionResponse.getExecutionResultAsString(); + private void doRequest(JsonObject jsonInput, HttpServerResponse response, RoutingContext ctx, + String requestedCharset) { + VertxExecutionResponseWrtiter writer = new VertxExecutionResponseWrtiter(response, ctx, requestedCharset); + // Add some context to dfe + Map metaData = new ConcurrentHashMap<>(); + metaData.put("httpHeaders", getHeaders(ctx)); + metaData.put("runBlocking", runBlocking); + getExecutionService().executeAsync(jsonInput, metaData, writer); + } + + private Map> getHeaders(RoutingContext ctx) { + Map> h = new HashMap<>(); + MultiMap headers = ctx.request().headers(); + for (String header : headers.names()) { + h.put(header, headers.getAll(header)); } - return null; + return h; } private static JsonObject toJsonObject(String jsonString) { @@ -312,4 +324,49 @@ private static JsonObject toJsonObject(String jsonString) { return jsonReader.readObject(); } } + + class VertxExecutionResponseWrtiter implements ExecutionResponseWriter { + + HttpServerResponse response; + String requestedCharset; + RoutingContext ctx; + + VertxExecutionResponseWrtiter(HttpServerResponse response, RoutingContext ctx, String requestedCharset) { + this.response = response; + this.ctx = ctx; + this.requestedCharset = requestedCharset; + } + + @Override + public void write(ExecutionResponse er) { + + if (shouldFail(er)) { + response.setStatusCode(500) + .end(); + } else { + response.setStatusCode(200) + .setStatusMessage(OK) + .end(Buffer.buffer(er.getExecutionResultAsString(), requestedCharset)); + } + } + + @Override + public void fail(Throwable t) { + ctx.fail(t); + } + + private boolean shouldFail(ExecutionResponse er) { + ExecutionResult executionResult = er.getExecutionResult(); + + if (executionResult.isDataPresent() && executionResult.getErrors().size() > 0) { + // See if there was a httpfailure + for (GraphQLError error : executionResult.getErrors()) { + if (error.getErrorType().equals(ErrorType.ExecutionAborted)) { + return true; + } + } + } + return false; + } + } } diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLLocaleResolver.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLLocaleResolver.java new file mode 100644 index 0000000000000..0bba02a64c008 --- /dev/null +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLLocaleResolver.java @@ -0,0 +1,60 @@ +package io.quarkus.smallrye.graphql.runtime; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +import javax.inject.Singleton; + +import org.hibernate.validator.spi.messageinterpolation.LocaleResolver; +import org.hibernate.validator.spi.messageinterpolation.LocaleResolverContext; + +import graphql.schema.DataFetchingEnvironment; +import io.smallrye.graphql.execution.context.SmallRyeContext; +import io.smallrye.graphql.execution.context.SmallRyeContextManager; + +/** + * Resolving BV messages for SmallRye GraphQL + */ +@Singleton +public class SmallRyeGraphQLLocaleResolver implements LocaleResolver { + + private static final String ACCEPT_HEADER = "Accept-Language"; + + @Override + public Locale resolve(LocaleResolverContext context) { + Optional> localePriorities = getAcceptableLanguages(); + if (!localePriorities.isPresent()) { + return null; + } + List resolvedLocales = Locale.filter(localePriorities.get(), context.getSupportedLocales()); + if (!resolvedLocales.isEmpty()) { + return resolvedLocales.get(0); + } + + return null; + } + + private Optional> getAcceptableLanguages() { + Map> httpHeaders = getHeaders(); + if (httpHeaders != null) { + List acceptLanguageList = httpHeaders.get(ACCEPT_HEADER); + if (acceptLanguageList != null && !acceptLanguageList.isEmpty()) { + return Optional.of(Locale.LanguageRange.parse(acceptLanguageList.get(0))); + } + } + return Optional.empty(); + } + + @SuppressWarnings("unchecked") + private Map> getHeaders() { + SmallRyeContext smallRyeContext = SmallRyeContextManager.getCurrentSmallRyeContext(); + if (smallRyeContext != null) { + DataFetchingEnvironment dfe = smallRyeContext.unwrap(DataFetchingEnvironment.class); + return (Map>) dfe.getGraphQlContext().get("httpHeaders"); + } else { + return null; + } + } +} diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java index 7d4c65ba5f246..1c396f0023baf 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java @@ -34,11 +34,11 @@ protected void doHandle(final RoutingContext ctx) { switch (subprotocol) { case "graphql-transport-ws": handler = new GraphQLTransportWSSubprotocolHandler( - new QuarkusVertxWebSocketSession(serverWebSocket), getExecutionService()); + new QuarkusVertxWebSocketSession(serverWebSocket)); break; case "graphql-ws": handler = new GraphQLWSSubprotocolHandler( - new QuarkusVertxWebSocketSession(serverWebSocket), getExecutionService()); + new QuarkusVertxWebSocketSession(serverWebSocket)); break; default: log.warn("Unknown graphql-over-websocket protocol: " + subprotocol); diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLRecorder.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLRecorder.java index bbfc4432d4ac5..79d80618d4352 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLRecorder.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLRecorder.java @@ -3,10 +3,9 @@ import java.util.List; import java.util.function.Consumer; -import javax.enterprise.inject.Instance; -import javax.enterprise.inject.spi.CDI; - import graphql.schema.GraphQLSchema; +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.ShutdownContext; @@ -33,10 +32,11 @@ public RuntimeValue createExecutionService(BeanContainer beanContainer, } public Handler executionHandler(RuntimeValue initialized, boolean allowGet, - boolean allowPostWithQueryParameters) { + boolean allowPostWithQueryParameters, boolean runBlocking) { if (initialized.getValue()) { - return new SmallRyeGraphQLExecutionHandler(allowGet, allowPostWithQueryParameters, getCurrentIdentityAssociation(), - CDI.current().select(CurrentVertxRequest.class).get()); + return new SmallRyeGraphQLExecutionHandler(allowGet, allowPostWithQueryParameters, runBlocking, + getCurrentIdentityAssociation(), + Arc.container().instance(CurrentVertxRequest.class).get()); } else { return new SmallRyeGraphQLNoEndpointHandler(); } @@ -44,7 +44,7 @@ public Handler executionHandler(RuntimeValue initialize public Handler graphqlOverWebsocketHandler(BeanContainer beanContainer, RuntimeValue initialized) { return new SmallRyeGraphQLOverWebSocketHandler(getCurrentIdentityAssociation(), - CDI.current().select(CurrentVertxRequest.class).get()); + Arc.container().instance(CurrentVertxRequest.class).get()); } public Handler schemaHandler(RuntimeValue initialized, boolean schemaAvailable) { @@ -89,9 +89,9 @@ public void accept(Route route) { } private CurrentIdentityAssociation getCurrentIdentityAssociation() { - Instance identityAssociations = CDI.current() - .select(CurrentIdentityAssociation.class); - if (identityAssociations.isResolvable()) { + InstanceHandle identityAssociations = Arc.container() + .instance(CurrentIdentityAssociation.class); + if (identityAssociations.isAvailable()) { return identityAssociations.get(); } return null; diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/AbstractAsyncDataFetcher.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/AbstractAsyncDataFetcher.java new file mode 100644 index 0000000000000..80d55240d4d2f --- /dev/null +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/AbstractAsyncDataFetcher.java @@ -0,0 +1,85 @@ +package io.quarkus.smallrye.graphql.runtime.spi.datafetcher; + +import java.util.List; +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.graphql.GraphQLException; + +import graphql.execution.DataFetcherResult; +import graphql.schema.DataFetchingEnvironment; +import io.smallrye.graphql.SmallRyeGraphQLServerMessages; +import io.smallrye.graphql.execution.datafetcher.AbstractDataFetcher; +import io.smallrye.graphql.schema.model.Operation; +import io.smallrye.graphql.schema.model.Type; +import io.smallrye.graphql.transformation.AbstractDataFetcherException; +import io.smallrye.mutiny.Uni; + +public abstract class AbstractAsyncDataFetcher extends AbstractDataFetcher { + + public AbstractAsyncDataFetcher(Operation operation, Type type) { + super(operation, type); + } + + @Override + @SuppressWarnings("unchecked") + protected O invokeAndTransform( + DataFetchingEnvironment dfe, + DataFetcherResult.Builder resultBuilder, + Object[] transformedArguments) throws Exception { + + Uni uni = handleUserMethodCall(dfe, transformedArguments); + return (O) uni + .onItemOrFailure() + .transformToUni((result, throwable, emitter) -> { + if (throwable != null) { + eventEmitter.fireOnDataFetchError(dfe.getExecutionId().toString(), throwable); + if (throwable instanceof GraphQLException) { + GraphQLException graphQLException = (GraphQLException) throwable; + errorResultHelper.appendPartialResult(resultBuilder, dfe, graphQLException); + } else if (throwable instanceof Exception) { + emitter.fail(SmallRyeGraphQLServerMessages.msg.dataFetcherException(operation, throwable)); + return; + } else if (throwable instanceof Error) { + emitter.fail(throwable); + return; + } + } else { + try { + resultBuilder.data(fieldHelper.transformOrAdaptResponse(result, dfe)); + } catch (AbstractDataFetcherException te) { + te.appendDataFetcherResult(resultBuilder, dfe); + } + } + + emitter.complete(resultBuilder.build()); + }) + .subscribe() + .asCompletionStage(); + } + + protected abstract Uni handleUserMethodCall(DataFetchingEnvironment dfe, final Object[] transformedArguments) + throws Exception; + + @Override + @SuppressWarnings("unchecked") + protected O invokeFailure(DataFetcherResult.Builder resultBuilder) { + return (O) Uni.createFrom() + .item(resultBuilder::build) + .subscribe() + .asCompletionStage(); + } + + @Override + @SuppressWarnings("unchecked") + protected CompletionStage> invokeBatch(DataFetchingEnvironment dfe, Object[] arguments) { + try { + return handleUserBatchLoad(dfe, arguments) + .subscribe().asCompletionStage(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + protected abstract Uni> handleUserBatchLoad(DataFetchingEnvironment dfe, final Object[] arguments) + throws Exception; +} diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/BlockingHelper.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/BlockingHelper.java new file mode 100644 index 0000000000000..e998b0c9f4a3a --- /dev/null +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/BlockingHelper.java @@ -0,0 +1,34 @@ +package io.quarkus.smallrye.graphql.runtime.spi.datafetcher; + +import java.util.concurrent.Callable; + +import io.smallrye.graphql.schema.model.Execute; +import io.smallrye.graphql.schema.model.Operation; +import io.vertx.core.Context; +import io.vertx.core.Promise; + +public class BlockingHelper { + + public static boolean blockingShouldExecuteNonBlocking(Operation operation, Context vc) { + // Rule is that by default this should execute blocking except if marked as non blocking) + return operation.getExecute().equals(Execute.NON_BLOCKING); + } + + public static boolean nonBlockingShouldExecuteBlocking(Operation operation, Context vc) { + // Rule is that by default this should execute non-blocking except if marked as blocking) + return operation.getExecute().equals(Execute.BLOCKING) && vc.isEventLoopContext(); + } + + @SuppressWarnings("unchecked") + public static void runBlocking(Context vc, Callable contextualCallable, Promise result) { + // Here call blocking with context + vc.executeBlocking(future -> { + try { + future.complete(contextualCallable.call()); + } catch (Exception ex) { + future.fail(ex); + } + }, result); + } + +} diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusCompletionStageDataFetcher.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusCompletionStageDataFetcher.java new file mode 100644 index 0000000000000..01870a6a6fb74 --- /dev/null +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusCompletionStageDataFetcher.java @@ -0,0 +1,96 @@ +package io.quarkus.smallrye.graphql.runtime.spi.datafetcher; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionStage; + +import graphql.schema.DataFetchingEnvironment; +import io.quarkus.arc.Arc; +import io.smallrye.context.SmallRyeThreadContext; +import io.smallrye.graphql.schema.model.Operation; +import io.smallrye.graphql.schema.model.Type; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Context; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; + +public class QuarkusCompletionStageDataFetcher extends AbstractAsyncDataFetcher { + + public QuarkusCompletionStageDataFetcher(Operation operation, Type type) { + super(operation, type); + } + + @Override + protected Uni handleUserMethodCall(DataFetchingEnvironment dfe, Object[] transformedArguments) throws Exception { + Context vc = Vertx.currentContext(); + if (runBlocking(dfe) || !BlockingHelper.nonBlockingShouldExecuteBlocking(operation, vc)) { + return handleUserMethodCallNonBlocking(transformedArguments); + } else { + return handleUserMethodCallBlocking(transformedArguments, vc); + } + } + + @Override + protected Uni> handleUserBatchLoad(DataFetchingEnvironment dfe, Object[] arguments) throws Exception { + Context vc = Vertx.currentContext(); + if (runBlocking(dfe) || !BlockingHelper.nonBlockingShouldExecuteBlocking(operation, vc)) { + return handleUserBatchLoadNonBlocking(arguments); + } else { + return handleUserBatchLoadBlocking(arguments, vc); + } + } + + private Uni handleUserMethodCallNonBlocking(Object[] transformedArguments) + throws Exception { + return Uni.createFrom() + .completionStage((CompletionStage) operationInvoker.invoke(transformedArguments)); + } + + @SuppressWarnings("unchecked") + private Uni handleUserMethodCallBlocking(Object[] transformedArguments, Context vc) + throws Exception { + + SmallRyeThreadContext threadContext = Arc.container().select(SmallRyeThreadContext.class).get(); + final Promise result = Promise.promise(); + + // We need some make sure that we call given the context + Callable contextualCallable = threadContext.contextualCallable(() -> { + CompletionStage resultFromMethodCall = (CompletionStage) operationInvoker + .invoke(transformedArguments); + return resultFromMethodCall.toCompletableFuture().get(); + }); + + // Here call blocking with context + BlockingHelper.runBlocking(vc, contextualCallable, result); + return Uni.createFrom().completionStage(result.future().toCompletionStage()); + } + + @SuppressWarnings("unchecked") + private Uni> handleUserBatchLoadNonBlocking(Object[] arguments) throws Exception { + return Uni.createFrom().completionStage((CompletionStage>) operationInvoker.invoke(arguments)); + } + + @SuppressWarnings("unchecked") + private Uni> handleUserBatchLoadBlocking(Object[] arguments, Context vc) + throws Exception { + + SmallRyeThreadContext threadContext = Arc.container().select(SmallRyeThreadContext.class).get(); + final Promise> result = Promise.promise(); + + // We need some make sure that we call given the context + Callable contextualCallable = threadContext.contextualCallable(() -> { + @SuppressWarnings("unchecked") + CompletionStage> resultFromMethodCall = (CompletionStage>) operationInvoker + .invoke(arguments); + return resultFromMethodCall.toCompletableFuture().get(); + }); + + // Here call blocking with context + BlockingHelper.runBlocking(vc, contextualCallable, result); + return Uni.createFrom().completionStage(result.future().toCompletionStage()); + } + + private boolean runBlocking(DataFetchingEnvironment dfe) { + return dfe.getGraphQlContext().get("runBlocking"); + } +} diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusDataFetcherService.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusDataFetcherService.java new file mode 100644 index 0000000000000..cde1260bb81ab --- /dev/null +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusDataFetcherService.java @@ -0,0 +1,35 @@ +package io.quarkus.smallrye.graphql.runtime.spi.datafetcher; + +import io.smallrye.graphql.execution.datafetcher.PlugableDataFetcher; +import io.smallrye.graphql.schema.model.Operation; +import io.smallrye.graphql.schema.model.Type; +import io.smallrye.graphql.spi.DataFetcherService; + +/** + * Some Quarkus specific datafetchers to execute reactive on the correct thread + */ +public class QuarkusDataFetcherService implements DataFetcherService { + + private final int priority = 1; + + @Override + public Integer getPriority() { + return priority; + } + + @Override + public PlugableDataFetcher getUniDataFetcher(Operation operation, Type type) { + return new QuarkusUniDataFetcher(operation, type); + } + + @Override + public PlugableDataFetcher getDefaultDataFetcher(Operation operation, Type type) { + return new QuarkusDefaultDataFetcher(operation, type); + } + + @Override + public PlugableDataFetcher getCompletionStageDataFetcher(Operation operation, Type type) { + return new QuarkusCompletionStageDataFetcher(operation, type); + } + +} \ No newline at end of file diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusDefaultDataFetcher.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusDefaultDataFetcher.java new file mode 100644 index 0000000000000..1907a29fb2978 --- /dev/null +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusDefaultDataFetcher.java @@ -0,0 +1,101 @@ +package io.quarkus.smallrye.graphql.runtime.spi.datafetcher; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.graphql.GraphQLException; + +import graphql.execution.AbortExecutionException; +import graphql.execution.DataFetcherResult; +import graphql.schema.DataFetchingEnvironment; +import io.quarkus.arc.Arc; +import io.smallrye.context.SmallRyeThreadContext; +import io.smallrye.graphql.execution.datafetcher.DefaultDataFetcher; +import io.smallrye.graphql.schema.model.Operation; +import io.smallrye.graphql.schema.model.Type; +import io.smallrye.graphql.transformation.AbstractDataFetcherException; +import io.vertx.core.Context; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; + +public class QuarkusDefaultDataFetcher extends DefaultDataFetcher { + + public QuarkusDefaultDataFetcher(Operation operation, Type type) { + super(operation, type); + } + + @Override + public T invokeAndTransform(DataFetchingEnvironment dfe, DataFetcherResult.Builder resultBuilder, + Object[] transformedArguments) throws Exception { + + Context vc = Vertx.currentContext(); + if (runBlocking(dfe) || BlockingHelper.blockingShouldExecuteNonBlocking(operation, vc)) { + return super.invokeAndTransform(dfe, resultBuilder, transformedArguments); + } else { + return invokeAndTransformBlocking(dfe, resultBuilder, transformedArguments, vc); + } + } + + @Override + public CompletionStage> invokeBatch(DataFetchingEnvironment dfe, Object[] arguments) { + + Context vc = Vertx.currentContext(); + if (runBlocking(dfe) || BlockingHelper.blockingShouldExecuteNonBlocking(operation, vc)) { + return super.invokeBatch(dfe, arguments); + } else { + return invokeBatchBlocking(arguments, vc); + } + } + + @SuppressWarnings("unchecked") + private T invokeAndTransformBlocking(final DataFetchingEnvironment dfe, DataFetcherResult.Builder resultBuilder, + Object[] transformedArguments, Context vc) throws Exception { + + SmallRyeThreadContext threadContext = Arc.container().select(SmallRyeThreadContext.class).get(); + final Promise result = Promise.promise(); + + // We need some make sure that we call given the context + @SuppressWarnings("unchecked") + Callable contextualCallable = threadContext.contextualCallable(() -> { + try { + Object resultFromMethodCall = operationInvoker.invoke(transformedArguments); + Object resultFromTransform = fieldHelper.transformOrAdaptResponse(resultFromMethodCall, dfe); + resultBuilder.data(resultFromTransform); + return (T) resultBuilder.build(); + } catch (AbstractDataFetcherException te) { + te.appendDataFetcherResult(resultBuilder, dfe); + return (T) resultBuilder.build(); + } catch (GraphQLException graphQLException) { + errorResultHelper.appendPartialResult(resultBuilder, dfe, graphQLException); + return (T) resultBuilder.build(); + } catch (Error e) { + resultBuilder.clearErrors().data(null).error(new AbortExecutionException(e)); + return (T) resultBuilder.build(); + } + }); + + // Here call blocking with context + BlockingHelper.runBlocking(vc, contextualCallable, result); + return (T) result.future().toCompletionStage(); + } + + @SuppressWarnings("unchecked") + private CompletionStage> invokeBatchBlocking(Object[] arguments, Context vc) { + SmallRyeThreadContext threadContext = Arc.container().select(SmallRyeThreadContext.class).get(); + final Promise> result = Promise.promise(); + + // We need some make sure that we call given the context + Callable contextualCallable = threadContext.contextualCallable(() -> { + return (List) operationInvoker.invokePrivileged(arguments); + }); + + // Here call blocking with context + BlockingHelper.runBlocking(vc, contextualCallable, result); + return result.future().toCompletionStage(); + } + + private boolean runBlocking(DataFetchingEnvironment dfe) { + return dfe.getGraphQlContext().get("runBlocking"); + } +} diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusUniDataFetcher.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusUniDataFetcher.java new file mode 100644 index 0000000000000..a824a7e906633 --- /dev/null +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusUniDataFetcher.java @@ -0,0 +1,93 @@ +package io.quarkus.smallrye.graphql.runtime.spi.datafetcher; + +import java.util.List; +import java.util.concurrent.Callable; + +import graphql.schema.DataFetchingEnvironment; +import io.quarkus.arc.Arc; +import io.smallrye.context.SmallRyeThreadContext; +import io.smallrye.graphql.schema.model.Operation; +import io.smallrye.graphql.schema.model.Type; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Context; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; + +public class QuarkusUniDataFetcher extends AbstractAsyncDataFetcher { + + public QuarkusUniDataFetcher(Operation operation, Type type) { + super(operation, type); + } + + @Override + protected Uni handleUserMethodCall(DataFetchingEnvironment dfe, Object[] transformedArguments) throws Exception { + Context vc = Vertx.currentContext(); + if (runBlocking(dfe) || !BlockingHelper.nonBlockingShouldExecuteBlocking(operation, vc)) { + return handleUserMethodCallNonBlocking(transformedArguments); + } else { + return handleUserMethodCallBlocking(transformedArguments, vc); + } + } + + @Override + protected Uni> handleUserBatchLoad(DataFetchingEnvironment dfe, Object[] arguments) throws Exception { + Context vc = Vertx.currentContext(); + if (runBlocking(dfe) || !BlockingHelper.nonBlockingShouldExecuteBlocking(operation, vc)) { + return handleUserBatchLoadNonBlocking(arguments); + } else { + return handleUserBatchLoadBlocking(arguments, vc); + } + } + + private Uni handleUserMethodCallNonBlocking(final Object[] transformedArguments) + throws Exception { + return (Uni) operationInvoker.invoke(transformedArguments); + } + + private Uni handleUserMethodCallBlocking(Object[] transformedArguments, Context vc) + throws Exception { + + SmallRyeThreadContext threadContext = Arc.container().select(SmallRyeThreadContext.class).get(); + final Promise result = Promise.promise(); + + // We need some make sure that we call given the context + Callable contextualCallable = threadContext.contextualCallable(() -> { + Object resultFromMethodCall = operationInvoker.invoke(transformedArguments); + Uni uniFromMethodCall = (Uni) resultFromMethodCall; + return uniFromMethodCall.subscribeAsCompletionStage().get(); + }); + + // Here call blocking with context + BlockingHelper.runBlocking(vc, contextualCallable, result); + return Uni.createFrom().completionStage(result.future().toCompletionStage()); + } + + @SuppressWarnings("unchecked") + protected Uni> handleUserBatchLoadNonBlocking(final Object[] arguments) + throws Exception { + return ((Uni>) operationInvoker.invoke(arguments)); + } + + private Uni> handleUserBatchLoadBlocking(Object[] arguments, Context vc) + throws Exception { + + SmallRyeThreadContext threadContext = Arc.container().select(SmallRyeThreadContext.class).get(); + final Promise> result = Promise.promise(); + + // We need some make sure that we call given the context + Callable contextualCallable = threadContext.contextualCallable(() -> { + Object resultFromMethodCall = operationInvoker.invoke(arguments); + @SuppressWarnings("unchecked") + Uni> uniFromMethodCall = (Uni>) resultFromMethodCall; + return uniFromMethodCall.subscribeAsCompletionStage().get(); + }); + + // Here call blocking with context + BlockingHelper.runBlocking(vc, contextualCallable, result); + return Uni.createFrom().completionStage(result.future().toCompletionStage()); + } + + private boolean runBlocking(DataFetchingEnvironment dfe) { + return dfe.getGraphQlContext().get("runBlocking"); + } +} diff --git a/extensions/smallrye-graphql/runtime/src/main/resources/META-INF/services/io.smallrye.graphql.spi.DataFetcherService b/extensions/smallrye-graphql/runtime/src/main/resources/META-INF/services/io.smallrye.graphql.spi.DataFetcherService new file mode 100644 index 0000000000000..6b64c6606810e --- /dev/null +++ b/extensions/smallrye-graphql/runtime/src/main/resources/META-INF/services/io.smallrye.graphql.spi.DataFetcherService @@ -0,0 +1 @@ +io.quarkus.smallrye.graphql.runtime.spi.datafetcher.QuarkusDataFetcherService diff --git a/extensions/smallrye-health/runtime/pom.xml b/extensions/smallrye-health/runtime/pom.xml index 018c484b3a73d..e51d706811b2c 100644 --- a/extensions/smallrye-health/runtime/pom.xml +++ b/extensions/smallrye-health/runtime/pom.xml @@ -17,6 +17,13 @@ io.smallrye smallrye-health + + + + org.glassfish + jakarta.json + + io.smallrye diff --git a/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/SignSecretKeyUnitTest.java b/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/SignSecretKeyUnitTest.java new file mode 100644 index 0000000000000..63175c5a4d5c4 --- /dev/null +++ b/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/SignSecretKeyUnitTest.java @@ -0,0 +1,34 @@ +package io.quarkus.jwt.test; + +import static org.hamcrest.Matchers.equalTo; + +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.jwt.build.Jwt; + +public class SignSecretKeyUnitTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(DefaultGroupsEndpoint.class) + .addAsResource("secretKey.jwk") + .addAsResource("applicationSignSecretKey.properties", "application.properties")); + + /** + * Validate a request with MP-JWT without a 'groups' claim is successful + * due to the default value being provided in the configuration + * + */ + @Test + public void echoGroups() { + String token = Jwt.upn("upn").groups("User").sign(); + RestAssured.given().auth() + .oauth2(token) + .get("/endp/echo") + .then().assertThat().statusCode(200) + .body(equalTo("User")); + } +} diff --git a/extensions/smallrye-jwt/deployment/src/test/resources/applicationSignSecretKey.properties b/extensions/smallrye-jwt/deployment/src/test/resources/applicationSignSecretKey.properties new file mode 100644 index 0000000000000..8070992d3d53d --- /dev/null +++ b/extensions/smallrye-jwt/deployment/src/test/resources/applicationSignSecretKey.properties @@ -0,0 +1,8 @@ +smallrye.jwt.verify.key.location=/secretKey.jwk +smallrye.jwt.sign.key.location=/secretKey.jwk +smallrye.jwt.new-token.issuer=https://server.example.com +mp.jwt.verify.issuer=https://server.example.com +smallrye.jwt.verify.algorithm=HS256 + +quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".min-level=TRACE +quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".level=TRACE diff --git a/extensions/smallrye-jwt/deployment/src/test/resources/secretKey.jwk b/extensions/smallrye-jwt/deployment/src/test/resources/secretKey.jwk new file mode 100644 index 0000000000000..d88add7780fd9 --- /dev/null +++ b/extensions/smallrye-jwt/deployment/src/test/resources/secretKey.jwk @@ -0,0 +1,6 @@ +{ + "keys": + [ + { "kty":"oct", "k":"uWlwBLGv4EpifZ52EhTuU9L-76AF9Vf4yumSD1P-2uE", "alg":"HS256" } + ] +} diff --git a/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java b/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java index 4673ada2d6022..0a432379fb03a 100644 --- a/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java +++ b/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java @@ -168,6 +168,7 @@ void createRoute(BuildProducer routes, .build()); routes.produce(frameworkRoot.routeBuilder() .route(metrics.path) + .routeConfigKey("quarkus.smallrye-metrics.path") .handler(recorder.handler(frameworkRoot.resolvePath(metrics.path))) .displayOnNotFoundPage("Metrics") .blockingRoute() diff --git a/extensions/smallrye-metrics/deployment/src/main/resources/dev-templates/embedded.html b/extensions/smallrye-metrics/deployment/src/main/resources/dev-templates/embedded.html new file mode 100644 index 0000000000000..016463825f5d9 --- /dev/null +++ b/extensions/smallrye-metrics/deployment/src/main/resources/dev-templates/embedded.html @@ -0,0 +1,16 @@ + + + All Metrics +
+ + + Vendor Metrics +
+ + + Application Metrics +
+ + + Base Metrics +
\ No newline at end of file diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/OpenApiConfigMapping.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/OpenApiConfigMapping.java index e694f14cbb465..1ab490ae45a54 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/OpenApiConfigMapping.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/OpenApiConfigMapping.java @@ -18,6 +18,7 @@ */ public class OpenApiConfigMapping extends RelocateConfigSourceInterceptor { private static final Map RELOCATIONS = relocations(); + @SuppressWarnings("unchecked") private final HyphenateEnumConverter enumConverter = new HyphenateEnumConverter(OpenApiConfig.OperationIdStrategy.class); public OpenApiConfigMapping() { diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java index b614f89e0af35..c87ed39582d9f 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java @@ -78,6 +78,7 @@ import io.quarkus.resteasy.server.common.spi.ResteasyJaxrsConfigBuildItem; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.util.ClassPathUtils; +import io.quarkus.security.Authenticated; import io.quarkus.smallrye.openapi.common.deployment.SmallRyeOpenApiConfig; import io.quarkus.smallrye.openapi.deployment.filter.AutoRolesAllowedFilter; import io.quarkus.smallrye.openapi.deployment.filter.AutoTagFilter; @@ -405,13 +406,21 @@ private OASFilter getAutoRolesAllowedFilter(String securitySchemeName, OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem, SmallRyeOpenApiConfig config) { if (config.autoAddSecurityRequirement) { + if (securitySchemeName == null) { + securitySchemeName = config.securitySchemeName; + } + Map> rolesAllowedMethodReferences = getRolesAllowedMethodReferences( apiFilteredIndexViewBuildItem); - if (rolesAllowedMethodReferences != null && !rolesAllowedMethodReferences.isEmpty()) { - if (securitySchemeName == null) { - securitySchemeName = config.securitySchemeName; - } - return new AutoRolesAllowedFilter(securitySchemeName, rolesAllowedMethodReferences); + + List authenticatedMethodReferences = getAuthenticatedMethodReferences( + apiFilteredIndexViewBuildItem); + + if ((rolesAllowedMethodReferences != null && !rolesAllowedMethodReferences.isEmpty()) + || (authenticatedMethodReferences != null && !authenticatedMethodReferences.isEmpty())) { + + return new AutoRolesAllowedFilter(securitySchemeName, rolesAllowedMethodReferences, + authenticatedMethodReferences); } } return null; @@ -460,6 +469,36 @@ private Map> getRolesAllowedMethodReferences( return methodReferences; } + private List getAuthenticatedMethodReferences( + OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem) { + List authenticatedAnnotations = new ArrayList<>(); + authenticatedAnnotations.addAll( + apiFilteredIndexViewBuildItem.getIndex().getAnnotations(DotName.createSimple(Authenticated.class.getName()))); + + List methodReferences = new ArrayList<>(); + DotName securityRequirement = DotName.createSimple(SecurityRequirement.class.getName()); + for (AnnotationInstance ai : authenticatedAnnotations) { + if (ai.target().kind().equals(AnnotationTarget.Kind.METHOD)) { + MethodInfo method = ai.target().asMethod(); + if (isValidOpenAPIMethodForAutoAdd(method, securityRequirement)) { + String ref = JandexUtil.createUniqueMethodReference(method.declaringClass(), method); + methodReferences.add(ref); + } + } + if (ai.target().kind().equals(AnnotationTarget.Kind.CLASS)) { + ClassInfo classInfo = ai.target().asClass(); + List methods = classInfo.methods(); + for (MethodInfo method : methods) { + if (isValidOpenAPIMethodForAutoAdd(method, securityRequirement)) { + String ref = JandexUtil.createUniqueMethodReference(classInfo, method); + methodReferences.add(ref); + } + } + } + } + return methodReferences; + } + private Map getClassNamesMethodReferences(OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem) { List openapiAnnotations = new ArrayList<>(); Set allOpenAPIEndpoints = getAllOpenAPIEndpoints(); diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoRolesAllowedFilter.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoRolesAllowedFilter.java old mode 100644 new mode 100755 index 80174a25316c3..aab73cd9abbf9 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoRolesAllowedFilter.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoRolesAllowedFilter.java @@ -13,7 +13,6 @@ import org.eclipse.microprofile.openapi.models.responses.APIResponses; import org.eclipse.microprofile.openapi.models.security.SecurityRequirement; import org.eclipse.microprofile.openapi.models.security.SecurityScheme; -import org.jboss.logging.Logger; import io.smallrye.openapi.api.models.OperationImpl; import io.smallrye.openapi.api.models.responses.APIResponseImpl; @@ -23,26 +22,43 @@ * Automatically add security requirement to RolesAllowed methods */ public class AutoRolesAllowedFilter implements OASFilter { - private static final Logger log = Logger.getLogger(AutoRolesAllowedFilter.class); - - private Map> methodReferences; + private Map> rolesAllowedMethodReferences; + private List authenticatedMethodReferences; private String defaultSecuritySchemeName; public AutoRolesAllowedFilter() { } - public AutoRolesAllowedFilter(String defaultSecuritySchemeName, Map> methodReferences) { + public AutoRolesAllowedFilter(String defaultSecuritySchemeName, Map> rolesAllowedMethodReferences, + List authenticatedMethodReferences) { this.defaultSecuritySchemeName = defaultSecuritySchemeName; - this.methodReferences = methodReferences; + this.rolesAllowedMethodReferences = rolesAllowedMethodReferences; + this.authenticatedMethodReferences = authenticatedMethodReferences; + } + + public Map> getRolesAllowedMethodReferences() { + return rolesAllowedMethodReferences; + } + + public void setRolesAllowedMethodReferences(Map> rolesAllowedMethodReferences) { + this.rolesAllowedMethodReferences = rolesAllowedMethodReferences; + } + + public boolean hasRolesAllowedMethodReferences() { + return this.rolesAllowedMethodReferences != null && !this.rolesAllowedMethodReferences.isEmpty(); } - public Map> getMethodReferences() { - return methodReferences; + public List getAuthenticatedMethodReferences() { + return authenticatedMethodReferences; } - public void setMethodReferences(Map> methodReferences) { - this.methodReferences = methodReferences; + public void setAuthenticatedMethodReferences(List authenticatedMethodReferences) { + this.authenticatedMethodReferences = authenticatedMethodReferences; + } + + public boolean hasAuthenticatedMethodReferences() { + return this.authenticatedMethodReferences != null && !this.authenticatedMethodReferences.isEmpty(); } public String getDefaultSecuritySchemeName() { @@ -56,7 +72,7 @@ public void setDefaultSecuritySchemeName(String defaultSecuritySchemeName) { @Override public void filterOpenAPI(OpenAPI openAPI) { - if (!methodReferences.isEmpty()) { + if (hasRolesAllowedMethodReferences() || hasAuthenticatedMethodReferences()) { String securitySchemeName = getSecuritySchemeName(openAPI); Paths paths = openAPI.getPaths(); if (paths != null) { @@ -71,14 +87,29 @@ public void filterOpenAPI(OpenAPI openAPI) { OperationImpl operationImpl = (OperationImpl) operation; - if (methodReferences.keySet().contains(operationImpl.getMethodRef())) { + if (hasRolesAllowedMethodReferences() + && rolesAllowedMethodReferences.keySet().contains(operationImpl.getMethodRef())) { SecurityRequirement securityRequirement = new SecurityRequirementImpl(); - List roles = methodReferences.get(operationImpl.getMethodRef()); + List roles = rolesAllowedMethodReferences.get(operationImpl.getMethodRef()); securityRequirement = securityRequirement.addScheme(securitySchemeName, roles); operation = operation.addSecurityRequirement(securityRequirement); APIResponses responses = operation.getResponses(); for (APIResponseImpl response : getSecurityResponses()) { - responses.addAPIResponse(response.getResponseCode(), response); + if (!responses.hasAPIResponse(response.getResponseCode())) { + responses.addAPIResponse(response.getResponseCode(), response); + } + } + operation = operation.responses(responses); + } else if (hasAuthenticatedMethodReferences() + && authenticatedMethodReferences.contains(operationImpl.getMethodRef())) { + SecurityRequirement securityRequirement = new SecurityRequirementImpl(); + securityRequirement = securityRequirement.addScheme(securitySchemeName); + operation = operation.addSecurityRequirement(securityRequirement); + APIResponses responses = operation.getResponses(); + for (APIResponseImpl response : getSecurityResponses()) { + if (!responses.hasAPIResponse(response.getResponseCode())) { + responses.addAPIResponse(response.getResponseCode(), response); + } } operation = operation.responses(responses); } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRequirementTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityAuthenticateTestCase.java similarity index 90% rename from extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRequirementTestCase.java rename to extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityAuthenticateTestCase.java index 20f1506af4100..ff27f0614a282 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRequirementTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityAuthenticateTestCase.java @@ -8,12 +8,12 @@ import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; -public class AutoSecurityRequirementTestCase { +public class AutoSecurityAuthenticateTestCase { @RegisterExtension static QuarkusUnitTest runner = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar - .addClasses(ResourceBean.class, OpenApiResourceSecuredAtClassLevel.class, - OpenApiResourceSecuredAtMethodLevel.class, OpenApiResourceSecuredAtMethodLevel2.class) + .addClasses(ResourceBean2.class, OpenApiResourceAuthenticatedAtClassLevel.class, + OpenApiResourceAuthenticatedAtMethodLevel.class, OpenApiResourceAuthenticatedAtMethodLevel2.class) .addAsResource( new StringAsset("quarkus.smallrye-openapi.security-scheme=jwt\n" + "quarkus.smallrye-openapi.security-scheme-name=JWTCompanyAuthentication\n" diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedTestCase.java new file mode 100644 index 0000000000000..a9d4b7971708d --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedTestCase.java @@ -0,0 +1,132 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +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.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class AutoSecurityRolesAllowedTestCase { + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(ResourceBean.class, OpenApiResourceSecuredAtClassLevel.class, + OpenApiResourceSecuredAtMethodLevel.class, OpenApiResourceSecuredAtMethodLevel2.class) + .addAsResource( + new StringAsset("quarkus.smallrye-openapi.security-scheme=jwt\n" + + "quarkus.smallrye-openapi.security-scheme-name=JWTCompanyAuthentication\n" + + "quarkus.smallrye-openapi.security-scheme-description=JWT Authentication"), + + "application.properties")); + + @Test + public void testAutoSecurityRequirement() { + RestAssured.given().header("Accept", "application/json") + .when().get("/q/openapi") + .then() + .log().body() + .and() + .body("components.securitySchemes.JWTCompanyAuthentication", Matchers.hasEntry("type", "http")) + .and() + .body("components.securitySchemes.JWTCompanyAuthentication", + Matchers.hasEntry("description", "JWT Authentication")) + .and() + .body("components.securitySchemes.JWTCompanyAuthentication", Matchers.hasEntry("scheme", "bearer")) + .and() + .body("components.securitySchemes.JWTCompanyAuthentication", Matchers.hasEntry("bearerFormat", "JWT")) + .and() + .body("paths.'/resource2/test-security/annotated'.get.security.JWTCompanyAuthentication", + Matchers.notNullValue()) + .and() + .body("paths.'/resource2/test-security/naked'.get.security.JWTCompanyAuthentication", Matchers.notNullValue()) + .and() + .body("paths.'/resource2/test-security/classLevel/1'.get.security.JWTCompanyAuthentication", + Matchers.notNullValue()) + .and() + .body("paths.'/resource2/test-security/classLevel/2'.get.security.JWTCompanyAuthentication", + Matchers.notNullValue()) + .and() + .body("paths.'/resource2/test-security/classLevel/3'.get.security.MyOwnName", + Matchers.notNullValue()) + .and() + .body("paths.'/resource3/test-security/annotated'.get.security.AtClassLevel", Matchers.notNullValue()); + + } + + @Test + public void testOpenAPIAnnotations() { + RestAssured.given().header("Accept", "application/json") + .when().get("/q/openapi") + .then() + .log().body() + .and() + .body("paths.'/resource2/test-security/classLevel/1'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/classLevel/1'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource2/test-security/classLevel/2'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/classLevel/2'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource2/test-security/classLevel/3'.get.responses.401.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/classLevel/3'.get.responses.403.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/classLevel/4'.get.responses.401.description", + Matchers.equalTo("Who are you?")) + .and() + .body("paths.'/resource2/test-security/classLevel/4'.get.responses.403.description", + Matchers.equalTo("You cannot do that.")) + .and() + .body("paths.'/resource2/test-security/naked'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/naked'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource2/test-security/annotated'.get.responses.401.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/annotated'.get.responses.403.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/methodLevel/1'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/methodLevel/1'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource2/test-security/methodLevel/2'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/methodLevel/2'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource2/test-security/methodLevel/public'.get.responses.401.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/methodLevel/public'.get.responses.403.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/annotated/documented'.get.responses.401.description", + Matchers.equalTo("Who are you?")) + .and() + .body("paths.'/resource2/test-security/annotated/documented'.get.responses.403.description", + Matchers.equalTo("You cannot do that.")) + .and() + .body("paths.'/resource2/test-security/methodLevel/3'.get.responses.401.description", + Matchers.equalTo("Who are you?")) + .and() + .body("paths.'/resource2/test-security/methodLevel/3'.get.responses.403.description", + Matchers.equalTo("You cannot do that.")); + } + +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiDefaultPathTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiDefaultPathTestCase.java index d3e7b87978a2e..b15d78db8ba31 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiDefaultPathTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiDefaultPathTestCase.java @@ -34,7 +34,7 @@ public void testOpenApiPathAccessResource() { .then() .header("Content-Type", "application/json;charset=UTF-8") .body("openapi", Matchers.startsWith("3.0")) - .body("info.title", Matchers.equalTo("Generated API")) + .body("info.title", Matchers.equalTo("quarkus-smallrye-openapi-deployment API")) .body("tags.name[0]", Matchers.equalTo("test")) .body("paths.'/resource'.get.servers[0]", Matchers.hasKey("url")) .body("paths.'/resource'.get.security[0]", Matchers.hasKey("securityRequirement")) diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiHttpRootDefaultPathTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiHttpRootDefaultPathTestCase.java index b2b896eddd90f..1ccb3a7991fb1 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiHttpRootDefaultPathTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiHttpRootDefaultPathTestCase.java @@ -33,7 +33,7 @@ public void testOpenApiPathAccessResource() { .then() .header("Content-Type", "application/json;charset=UTF-8") .body("openapi", Matchers.startsWith("3.0")) - .body("info.title", Matchers.equalTo("Generated API")) + .body("info.title", Matchers.equalTo("quarkus-smallrye-openapi-deployment API")) .body("paths", Matchers.hasKey("/foo/resource")); } } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceAuthenticatedAtClassLevel.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceAuthenticatedAtClassLevel.java new file mode 100644 index 0000000000000..164ea97c27954 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceAuthenticatedAtClassLevel.java @@ -0,0 +1,39 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement; +import org.eclipse.microprofile.openapi.annotations.servers.Server; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import io.quarkus.security.Authenticated; + +@Path("/resource2") +@Tag(name = "test") +@Server(url = "serverUrl") +@Authenticated +public class OpenApiResourceAuthenticatedAtClassLevel { + + private ResourceBean2 resourceBean; + + @GET + @Path("/test-security/classLevel/1") + public String secureEndpoint1() { + return "secret"; + } + + @GET + @Path("/test-security/classLevel/2") + public String secureEndpoint2() { + return "secret"; + } + + @GET + @Path("/test-security/classLevel/3") + @SecurityRequirement(name = "MyOwnName") + public String secureEndpoint3() { + return "secret"; + } + +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceAuthenticatedAtMethodLevel.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceAuthenticatedAtMethodLevel.java new file mode 100644 index 0000000000000..6a3dc43cadfab --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceAuthenticatedAtMethodLevel.java @@ -0,0 +1,54 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement; +import org.eclipse.microprofile.openapi.annotations.servers.Server; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import io.quarkus.security.Authenticated; + +@Path("/resource2") +@Tag(name = "test") +@Server(url = "serverUrl") +public class OpenApiResourceAuthenticatedAtMethodLevel { + + private ResourceBean2 resourceBean; + + @GET + @Path("/test-security/naked") + @Authenticated + public String secureEndpointWithoutSecurityAnnotation() { + return "secret"; + } + + @GET + @Path("/test-security/annotated") + @Authenticated + @SecurityRequirement(name = "JWTCompanyAuthentication") + public String secureEndpointWithSecurityAnnotation() { + return "secret"; + } + + @GET + @Path("/test-security/methodLevel/1") + @Authenticated + public String secureEndpoint1() { + return "secret"; + } + + @GET + @Path("/test-security/methodLevel/2") + @Authenticated + public String secureEndpoint2() { + return "secret"; + } + + @GET + @Path("/test-security/methodLevel/public") + public String publicEndpoint() { + return "boo"; + } + +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceAuthenticatedAtMethodLevel2.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceAuthenticatedAtMethodLevel2.java new file mode 100644 index 0000000000000..98bef92c50ebf --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceAuthenticatedAtMethodLevel2.java @@ -0,0 +1,27 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement; +import org.eclipse.microprofile.openapi.annotations.servers.Server; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import io.quarkus.security.Authenticated; + +@Path("/resource3") +@Tag(name = "test") +@Server(url = "serverUrl") +@SecurityRequirement(name = "AtClassLevel") +public class OpenApiResourceAuthenticatedAtMethodLevel2 { + + private ResourceBean2 resourceBean; + + @GET + @Path("/test-security/annotated") + @Authenticated + public String secureEndpointWithSecurityAnnotation() { + return "secret"; + } + +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceSecuredAtClassLevel.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceSecuredAtClassLevel.java old mode 100644 new mode 100755 index fe0e2d7b1aa1a..2d6749566c6e1 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceSecuredAtClassLevel.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceSecuredAtClassLevel.java @@ -4,6 +4,8 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement; import org.eclipse.microprofile.openapi.annotations.servers.Server; import org.eclipse.microprofile.openapi.annotations.tags.Tag; @@ -35,4 +37,14 @@ public String secureEndpoint3() { return "secret"; } + @APIResponses({ + @APIResponse(responseCode = "401", description = "Who are you?"), + @APIResponse(responseCode = "403", description = "You cannot do that.") + }) + @GET + @Path("/test-security/classLevel/4") + public String secureEndpoint4() { + return "secret"; + } + } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceSecuredAtMethodLevel.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceSecuredAtMethodLevel.java old mode 100644 new mode 100755 index af31dad4c858e..ff3e17182fa0e --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceSecuredAtMethodLevel.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceSecuredAtMethodLevel.java @@ -4,6 +4,8 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement; import org.eclipse.microprofile.openapi.annotations.servers.Server; import org.eclipse.microprofile.openapi.annotations.tags.Tag; @@ -50,4 +52,27 @@ public String publicEndpoint() { return "boo"; } + @APIResponses({ + @APIResponse(responseCode = "401", description = "Who are you?"), + @APIResponse(responseCode = "403", description = "You cannot do that.") + }) + @GET + @Path("/test-security/annotated/documented") + @RolesAllowed("admin") + @SecurityRequirement(name = "JWTCompanyAuthentication") + public String secureEndpointWithSecurityAnnotationAndDocument() { + return "secret"; + } + + @APIResponses({ + @APIResponse(responseCode = "401", description = "Who are you?"), + @APIResponse(responseCode = "403", description = "You cannot do that.") + }) + @GET + @Path("/test-security/methodLevel/3") + @RolesAllowed("admin") + public String secureEndpoint3() { + return "secret"; + } + } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathHttpRootDefaultPathTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathHttpRootDefaultPathTestCase.java index 30aaf20833815..324811c1ac319 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathHttpRootDefaultPathTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathHttpRootDefaultPathTestCase.java @@ -26,7 +26,7 @@ public void testOpenApiResteasyPathHttpRootDefaultPath() { .then() .header("Content-Type", "application/json;charset=UTF-8") .body("openapi", Matchers.startsWith("3.0")) - .body("info.title", Matchers.equalTo("Generated API")) + .body("info.title", Matchers.equalTo("quarkus-smallrye-openapi-deployment API")) .body("paths", Matchers.hasKey("/http-root-path/resteasy-path/resource")); } } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathTestCase.java index a8b113c025c7f..6f530cc49ce25 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathTestCase.java @@ -25,7 +25,7 @@ public void testOpenApiResteasyPath() { .then() .header("Content-Type", "application/json;charset=UTF-8") .body("openapi", Matchers.startsWith("3.0")) - .body("info.title", Matchers.equalTo("Generated API")) + .body("info.title", Matchers.equalTo("quarkus-smallrye-openapi-deployment API")) .body("paths", Matchers.hasKey("/foo/bar/resource")); } } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/ResourceBean2.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/ResourceBean2.java new file mode 100644 index 0000000000000..134a04b5657c1 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/ResourceBean2.java @@ -0,0 +1,19 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import javax.enterprise.context.ApplicationScoped; + +import io.quarkus.security.Authenticated; + +@ApplicationScoped +public class ResourceBean2 { + @Override + public String toString() { + return "resource"; + } + + @Authenticated + public String anotherMethod() { + return "bla"; + } + +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiDefaultPathTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiDefaultPathTestCase.java index 258a9781194ea..1efed83abe595 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiDefaultPathTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiDefaultPathTestCase.java @@ -31,7 +31,7 @@ public void testOpenApiPathAccessResource() { .then() .header("Content-Type", "application/json;charset=UTF-8") .body("openapi", Matchers.startsWith("3.0")) - .body("info.title", Matchers.equalTo("Generated API")) + .body("info.title", Matchers.equalTo("quarkus-smallrye-openapi-deployment API")) .body("paths", Matchers.hasKey("/resource")); } } 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 337370cca6f79..1f9b5e59f651c 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 @@ -33,7 +33,7 @@ public void testOpenApiPathAccessResource() { .then() .header("Content-Type", "application/json;charset=UTF-8") .body("openapi", Matchers.startsWith("3.0")) - .body("info.title", Matchers.equalTo("Generated API")) + .body("info.title", Matchers.equalTo("quarkus-smallrye-openapi-deployment API")) .body("paths", Matchers.hasKey("/foo/resource")); } } diff --git a/extensions/smallrye-reactive-messaging-amqp/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/amqp/deployment/AmqpDevServicesProcessor.java b/extensions/smallrye-reactive-messaging-amqp/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/amqp/deployment/AmqpDevServicesProcessor.java index 2fcb397a450ba..3ba67d99a20d3 100644 --- a/extensions/smallrye-reactive-messaging-amqp/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/amqp/deployment/AmqpDevServicesProcessor.java +++ b/extensions/smallrye-reactive-messaging-amqp/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/amqp/deployment/AmqpDevServicesProcessor.java @@ -17,12 +17,12 @@ import org.testcontainers.utility.DockerImageName; import io.quarkus.deployment.Feature; -import io.quarkus.deployment.IsDockerWorking; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.console.ConsoleInstalledBuildItem; import io.quarkus.deployment.console.StartupLogCompressor; @@ -62,10 +62,9 @@ public class AmqpDevServicesProcessor { static volatile AmqpDevServiceCfg cfg; static volatile boolean first = true; - private final IsDockerWorking isDockerWorking = new IsDockerWorking(true); - @BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) public DevServicesResultBuildItem startAmqpDevService( + DockerStatusBuildItem dockerStatusBuildItem, LaunchModeBuildItem launchMode, AmqpBuildTimeConfig amqpClientBuildTimeConfig, Optional consoleInstalledBuildItem, @@ -88,7 +87,8 @@ public DevServicesResultBuildItem startAmqpDevService( (launchMode.isTest() ? "(test) " : "") + "AMQP Dev Services Starting:", consoleInstalledBuildItem, loggingSetupBuildItem); try { - RunningDevService newDevService = startAmqpBroker(configuration, launchMode, devServicesConfig.timeout); + RunningDevService newDevService = startAmqpBroker(dockerStatusBuildItem, configuration, launchMode, + devServicesConfig.timeout); if (newDevService != null) { devService = newDevService; Map config = devService.getConfig(); @@ -101,7 +101,11 @@ public DevServicesResultBuildItem startAmqpDevService( config.get(AMQP_PASSWORD_PROP)); } } - compressor.close(); + if (devService == null) { + compressor.closeAndDumpCaptured(); + } else { + compressor.close(); + } } catch (Throwable t) { compressor.closeAndDumpCaptured(); throw new RuntimeException(t); @@ -142,8 +146,8 @@ private void shutdownBroker() { } } - private RunningDevService startAmqpBroker(AmqpDevServiceCfg config, LaunchModeBuildItem launchMode, - Optional timeout) { + private RunningDevService startAmqpBroker(DockerStatusBuildItem dockerStatusBuildItem, AmqpDevServiceCfg config, + LaunchModeBuildItem launchMode, Optional timeout) { if (!config.devServicesEnabled) { // explicitly disabled log.debug("Not starting Dev Services for AMQP, as it has been disabled in the config."); @@ -162,7 +166,7 @@ private RunningDevService startAmqpBroker(AmqpDevServiceCfg config, LaunchModeBu return null; } - if (!isDockerWorking.getAsBoolean()) { + if (!dockerStatusBuildItem.isDockerAvailable()) { log.warn("Docker isn't working, please configure the AMQP broker location."); return null; } diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/pom.xml b/extensions/smallrye-reactive-messaging-kafka/deployment/pom.xml index 2b4f1d74e145a..d474778d76ddd 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/pom.xml +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/pom.xml @@ -73,6 +73,11 @@ quarkus-apicurio-registry-avro-deployment test
+ + io.quarkus + quarkus-confluent-registry-avro-deployment + test + io.rest-assured rest-assured diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java index 8501d99eedcfb..66366a7095eb7 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java @@ -10,6 +10,7 @@ final class DotNames { static final DotName EMITTER = DotName.createSimple(org.eclipse.microprofile.reactive.messaging.Emitter.class.getName()); static final DotName MUTINY_EMITTER = DotName.createSimple(io.smallrye.reactive.messaging.MutinyEmitter.class.getName()); + static final DotName KAFKA_EMITTER = DotName.createSimple(io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions.class.getName()); static final DotName MESSAGE = DotName.createSimple(org.eclipse.microprofile.reactive.messaging.Message.class.getName()); static final DotName KAFKA_RECORD = DotName.createSimple(io.smallrye.reactive.messaging.kafka.KafkaRecord.class.getName()); 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 b6fa969d50584..2d4081420b350 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 @@ -167,6 +167,8 @@ void discoverDefaultSerdeConfig(DefaultSerdeDiscoveryState discovery, processIncomingType(discovery, config, incomingType, channelName, generatedClass, reflection, alreadyGeneratedDeserializers); + processKafkaTransactions(discovery, config, channelName, injectionPointType); + Type outgoingType = getOutgoingTypeFromChannelInjectionPoint(injectionPointType); processOutgoingType(discovery, outgoingType, (keySerializer, valueSerializer) -> { produceRuntimeConfigurationDefaultBuildItem(discovery, config, @@ -180,6 +182,23 @@ void discoverDefaultSerdeConfig(DefaultSerdeDiscoveryState discovery, } } + private void processKafkaTransactions(DefaultSerdeDiscoveryState discovery, + BuildProducer config, String channelName, Type injectionPointType) { + if (injectionPointType != null && isKafkaEmitter(injectionPointType)) { + LOGGER.infof("Transactional producer detected for channel '%s', setting following default config values: " + + "'mp.messaging.outgoing.%s.transactional.id=${quarkus.application.name}-${channelName}', " + + "'mp.messaging.outgoing.%s.enable.idempotence=true', " + + "'mp.messaging.outgoing.%s.acks=all'", channelName, channelName, channelName, channelName); + produceRuntimeConfigurationDefaultBuildItem(discovery, config, + "mp.messaging.outgoing." + channelName + ".transactional.id", + "${quarkus.application.name}-" + channelName); + produceRuntimeConfigurationDefaultBuildItem(discovery, config, + "mp.messaging.outgoing." + channelName + ".enable.idempotence", "true"); + produceRuntimeConfigurationDefaultBuildItem(discovery, config, + "mp.messaging.outgoing." + channelName + ".acks", "all"); + } + } + private void processIncomingType(DefaultSerdeDiscoveryState discovery, BuildProducer config, Type incomingType, String channelName, BuildProducer generatedClass, BuildProducer reflection, @@ -352,7 +371,7 @@ private Type getOutgoingTypeFromChannelInjectionPoint(Type injectionPointType) { return null; } - if (isEmitter(injectionPointType) || isMutinyEmitter(injectionPointType)) { + if (isEmitter(injectionPointType) || isMutinyEmitter(injectionPointType) || isKafkaEmitter(injectionPointType)) { return injectionPointType.asParameterizedType().arguments().get(0); } else { return null; @@ -482,6 +501,13 @@ private static boolean isMutinyEmitter(Type type) { && type.asParameterizedType().arguments().size() == 1; } + private static boolean isKafkaEmitter(Type type) { + // raw type KafkaTransactions is wrong, must be KafkaTransactions + return DotNames.KAFKA_EMITTER.equals(type.name()) + && type.kind() == Type.Kind.PARAMETERIZED_TYPE + && type.asParameterizedType().arguments().size() == 1; + } + // --- private static boolean isMessage(Type type) { @@ -627,11 +653,11 @@ private Result deserializerFor(DefaultSerdeDiscoveryState discovery, Type type, if (clazz == null) { clazz = JacksonSerdeGenerator.generateDeserializer(generatedClass, type); LOGGER.infof("Generating Jackson deserializer for type %s", type.name().toString()); - result = Result.of(clazz); // Deserializers are access by reflection. reflection.produce(new ReflectiveClassBuildItem(true, true, false, clazz)); alreadyGeneratedSerializers.put(type.toString(), clazz); } + result = Result.of(clazz); } return result; } @@ -653,11 +679,11 @@ private Result serializerFor(DefaultSerdeDiscoveryState discovery, Type type, if (clazz == null) { clazz = JacksonSerdeGenerator.generateSerializer(generatedClass, type); LOGGER.infof("Generating Jackson serializer for type %s", type.name().toString()); - result = Result.of(clazz); // Serializers are access by reflection. reflection.produce(new ReflectiveClassBuildItem(true, true, false, clazz)); alreadyGeneratedSerializers.put(type.toString(), clazz); } + result = Result.of(clazz); } return result; diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java index 3f21351a981f6..0b1c82670c545 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java @@ -51,6 +51,7 @@ import io.smallrye.reactive.messaging.kafka.KafkaRecord; import io.smallrye.reactive.messaging.kafka.KafkaRecordBatch; import io.smallrye.reactive.messaging.kafka.Record; +import io.smallrye.reactive.messaging.kafka.transactions.KafkaTransactions; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; @@ -2675,4 +2676,24 @@ void method3(CustomDto payload) { } } + @Test + void kafkaTransactions() { + // @formatter:off + Tuple[] expectations = { + tuple("mp.messaging.outgoing.tx.value.serializer", "org.apache.kafka.common.serialization.StringSerializer"), + tuple("mp.messaging.outgoing.tx.transactional.id", "${quarkus.application.name}-tx"), + tuple("mp.messaging.outgoing.tx.enable.idempotence", "true"), + tuple("mp.messaging.outgoing.tx.acks", "all"), + }; + doTest(expectations, TransactionalProducer.class); + + } + + private static class TransactionalProducer { + + @Channel("tx") + KafkaTransactions kafkaTransactions; + + } + } 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 7502113b40c51..887a01b0f04e5 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 @@ -28,7 +28,8 @@ public class KafkaDevServicesDevModeTestCase { "mp.messaging.outgoing.generated-price.topic=prices\n" + "mp.messaging.incoming.prices.connector=smallrye-kafka\n" + "mp.messaging.incoming.prices.health-readiness-enabled=false\n" + - "mp.messaging.incoming.prices.topic=prices\n"; + "mp.messaging.incoming.prices.topic=prices\n" + + "quarkus.application.name=\n"; @RegisterExtension public static QuarkusDevModeTest test = new QuarkusDevModeTest() diff --git a/extensions/smallrye-reactive-messaging-rabbitmq/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/rabbitmq/deployment/RabbitMQDevServicesProcessor.java b/extensions/smallrye-reactive-messaging-rabbitmq/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/rabbitmq/deployment/RabbitMQDevServicesProcessor.java index d26beffffd4b1..e2b73a49ceb09 100644 --- a/extensions/smallrye-reactive-messaging-rabbitmq/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/rabbitmq/deployment/RabbitMQDevServicesProcessor.java +++ b/extensions/smallrye-reactive-messaging-rabbitmq/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/rabbitmq/deployment/RabbitMQDevServicesProcessor.java @@ -21,11 +21,11 @@ import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.Feature; -import io.quarkus.deployment.IsDockerWorking; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesResultBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.console.ConsoleInstalledBuildItem; import io.quarkus.deployment.console.StartupLogCompressor; @@ -60,10 +60,9 @@ public class RabbitMQDevServicesProcessor { static volatile RabbitMQDevServiceCfg cfg; static volatile boolean first = true; - private final IsDockerWorking isDockerWorking = new IsDockerWorking(true); - @BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) public DevServicesResultBuildItem startRabbitMQDevService( + DockerStatusBuildItem dockerStatusBuildItem, LaunchModeBuildItem launchMode, RabbitMQBuildTimeConfig rabbitmqClientBuildTimeConfig, Optional consoleInstalledBuildItem, @@ -85,7 +84,8 @@ public DevServicesResultBuildItem startRabbitMQDevService( (launchMode.isTest() ? "(test) " : "") + "RabbitMQ Dev Services Starting:", consoleInstalledBuildItem, loggingSetupBuildItem); try { - RunningDevService newDevService = startRabbitMQBroker(configuration, launchMode, devServicesConfig.timeout); + RunningDevService newDevService = startRabbitMQBroker(dockerStatusBuildItem, configuration, launchMode, + devServicesConfig.timeout); if (newDevService != null) { devService = newDevService; @@ -99,7 +99,11 @@ public DevServicesResultBuildItem startRabbitMQDevService( config.get(RABBITMQ_USERNAME_PROP), config.get(RABBITMQ_PASSWORD_PROP)); } } - compressor.close(); + if (devService == null) { + compressor.closeAndDumpCaptured(); + } else { + compressor.close(); + } } catch (Throwable t) { compressor.closeAndDumpCaptured(); throw new RuntimeException(t); @@ -141,7 +145,8 @@ private void shutdownBroker() { } } - private RunningDevService startRabbitMQBroker(RabbitMQDevServiceCfg config, LaunchModeBuildItem launchMode, + private RunningDevService startRabbitMQBroker(DockerStatusBuildItem dockerStatusBuildItem, + RabbitMQDevServiceCfg config, LaunchModeBuildItem launchMode, Optional timeout) { if (!config.devServicesEnabled) { // explicitly disabled @@ -161,7 +166,7 @@ private RunningDevService startRabbitMQBroker(RabbitMQDevServiceCfg config, Laun return null; } - if (!isDockerWorking.getAsBoolean()) { + if (!dockerStatusBuildItem.isDockerAvailable()) { log.warn("Docker isn't working, please configure the RabbitMQ broker location."); return null; } 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 6634a6b2bc2bb..19c664f2c09be 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 @@ -17,6 +17,7 @@ import io.smallrye.reactive.messaging.annotations.ConnectorAttribute; import io.smallrye.reactive.messaging.annotations.ConnectorAttributes; import io.smallrye.reactive.messaging.annotations.Emitter; +import io.smallrye.reactive.messaging.annotations.EmitterFactoryFor; import io.smallrye.reactive.messaging.annotations.Incomings; import io.smallrye.reactive.messaging.annotations.Merge; import io.smallrye.reactive.messaging.annotations.OnOverflow; @@ -49,6 +50,7 @@ public final class ReactiveMessagingDotNames { static final DotName ACKNOWLEDGMENT = DotName.createSimple(Acknowledgment.class.getName()); static final DotName MERGE = DotName.createSimple(Merge.class.getName()); static final DotName BROADCAST = DotName.createSimple(Broadcast.class.getName()); + static final DotName EMITTER_FACTORY_FOR = DotName.createSimple(EmitterFactoryFor.class.getName()); static final DotName INCOMING_CONNECTOR_FACTORY = DotName.createSimple(IncomingConnectorFactory.class.getName()); static final DotName OUTGOING_CONNECTOR_FACTORY = DotName.createSimple(OutgoingConnectorFactory.class.getName()); 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 d1fbfe7407b22..87e9596456d51 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 @@ -37,6 +37,7 @@ import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.DotNames; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.builder.item.SimpleBuildItem; import io.quarkus.deployment.Feature; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; @@ -73,13 +74,13 @@ import io.quarkus.smallrye.reactivemessaging.runtime.WorkerConfiguration; import io.quarkus.smallrye.reactivemessaging.runtime.devmode.DevModeSupportConnectorFactory; import io.quarkus.smallrye.reactivemessaging.runtime.devmode.DevModeSupportConnectorFactoryInterceptor; +import io.smallrye.reactive.messaging.EmitterConfiguration; import io.smallrye.reactive.messaging.Invoker; import io.smallrye.reactive.messaging.annotations.Blocking; import io.smallrye.reactive.messaging.health.SmallRyeReactiveMessagingLivenessCheck; import io.smallrye.reactive.messaging.health.SmallRyeReactiveMessagingReadinessCheck; import io.smallrye.reactive.messaging.health.SmallRyeReactiveMessagingStartupCheck; import io.smallrye.reactive.messaging.providers.extension.ChannelConfiguration; -import io.smallrye.reactive.messaging.providers.extension.EmitterConfiguration; public class SmallRyeReactiveMessagingProcessor { @@ -448,10 +449,9 @@ private boolean doesImplement(ClassInfo clazz, DotName iface, IndexView index) { @BuildStep CoroutineConfigurationBuildItem producesCoroutineConfiguration() { - try { - Class.forName("kotlinx.coroutines.future.FutureKt", false, getClass().getClassLoader()); + if (QuarkusClassLoader.isClassPresentAtRuntime("kotlinx.coroutines.future.FutureKt")) { return new CoroutineConfigurationBuildItem(true); - } catch (ClassNotFoundException e) { + } else { return new CoroutineConfigurationBuildItem(false); } } 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 76d2ffef6e594..47e744385dd41 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 @@ -49,24 +49,28 @@ static String getConnectorName(BeanInfo bi) { } static void produceIncomingChannel(BuildProducer producer, String name) { - Optional managingConnector = getManagingConnector(ChannelDirection.INCOMING, name); + String channelName = normalizeChannelName(name); + + Optional managingConnector = getManagingConnector(ChannelDirection.INCOMING, channelName); if (managingConnector.isPresent()) { - if (isChannelEnabled(ChannelDirection.INCOMING, name)) { - producer.produce(ChannelBuildItem.incoming(name, managingConnector.get())); + if (isChannelEnabled(ChannelDirection.INCOMING, channelName)) { + producer.produce(ChannelBuildItem.incoming(channelName, managingConnector.get())); } } else { - producer.produce(ChannelBuildItem.incoming(name, null)); + producer.produce(ChannelBuildItem.incoming(channelName, null)); } } static void produceOutgoingChannel(BuildProducer producer, String name) { - Optional managingConnector = getManagingConnector(ChannelDirection.OUTGOING, name); + String channelName = normalizeChannelName(name); + + Optional managingConnector = getManagingConnector(ChannelDirection.OUTGOING, channelName); if (managingConnector.isPresent()) { - if (isChannelEnabled(ChannelDirection.OUTGOING, name)) { - producer.produce(ChannelBuildItem.outgoing(name, managingConnector.get())); + if (isChannelEnabled(ChannelDirection.OUTGOING, channelName)) { + producer.produce(ChannelBuildItem.outgoing(channelName, managingConnector.get())); } } else { - producer.produce(ChannelBuildItem.outgoing(name, null)); + producer.produce(ChannelBuildItem.outgoing(channelName, null)); } } @@ -80,7 +84,7 @@ static void produceOutgoingChannel(BuildProducer producer, Str */ static Optional getManagingConnector(ChannelDirection direction, String channel) { return ConfigProvider.getConfig().getOptionalValue( - "mp.messaging." + direction.name().toLowerCase() + "." + channel + ".connector", + "mp.messaging." + direction.name().toLowerCase() + "." + normalizeChannelName(channel) + ".connector", String.class); } @@ -93,7 +97,8 @@ static Optional getManagingConnector(ChannelDirection direction, String */ static boolean isChannelEnabled(ChannelDirection direction, String channel) { return ConfigProvider.getConfig() - .getOptionalValue("mp.messaging." + direction.name().toLowerCase() + "." + channel + ".enabled", + .getOptionalValue( + "mp.messaging." + direction.name().toLowerCase() + "." + normalizeChannelName(channel) + ".enabled", Boolean.class) .orElse(true); } @@ -192,6 +197,21 @@ private static boolean getBooleanValueOrDefault(AnnotationInstance instance, Str return false; } + /** + * Normalize the name of a given channel. + * + * Concatenate the channel name with double quotes when it contains dots. + *

+ * Otherwise, the SmallRye Reactive Messaging only considers the + * text up to the first occurrence of a dot as the channel name. + * + * @param name the channel name. + * @return normalized channel name. + */ + private static String normalizeChannelName(String name) { + return name != null && !name.startsWith("\"") && name.contains(".") ? "\"" + name + "\"" : name; + } + /** * Finds a connector by name and direction in the given list. * diff --git a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/WiringProcessor.java b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/WiringProcessor.java index 64d3e0d1f9a9e..ce4c563f749b9 100644 --- a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/WiringProcessor.java +++ b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/WiringProcessor.java @@ -88,10 +88,17 @@ void extractComponents(BeanDiscoveryFinishedBuildItem beanDiscoveryFinished, BuildProducer validationErrors, BuildProducer configDescriptionBuildItemBuildProducer) { + Map emitterFactories = new HashMap<>(); // We need to collect all business methods annotated with @Incoming/@Outgoing first for (BeanInfo bean : beanDiscoveryFinished.beanStream().classBeans()) { // TODO: add support for inherited business methods //noinspection OptionalGetWithoutIsPresent + AnnotationInstance emitterFactory = transformedAnnotations.getAnnotation(bean.getTarget().get(), + ReactiveMessagingDotNames.EMITTER_FACTORY_FOR); + if (emitterFactory != null) { + emitterFactories.put(emitterFactory.value().asClass().name().toString(), emitterFactory); + } + for (MethodInfo method : bean.getTarget().get().asClass().methods()) { // @Incoming is repeatable AnnotationInstance incoming = transformedAnnotations.getAnnotation(method, @@ -131,31 +138,33 @@ void extractComponents(BeanDiscoveryFinishedBuildItem beanDiscoveryFinished, ReactiveMessagingDotNames.CHANNEL); Optional legacyChannel = WiringHelper.getAnnotation(transformedAnnotations, injectionPoint, ReactiveMessagingDotNames.LEGACY_CHANNEL); - boolean isEmitter = injectionPoint.getRequiredType().name().equals(ReactiveMessagingDotNames.EMITTER); - boolean isMutinyEmitter = injectionPoint.getRequiredType().name() - .equals(ReactiveMessagingDotNames.MUTINY_EMITTER); + + String injectionType = injectionPoint.getRequiredType().name().toString(); + AnnotationInstance emitterType = emitterFactories.get(injectionType); + boolean isLegacyEmitter = injectionPoint.getRequiredType().name() .equals(ReactiveMessagingDotNames.LEGACY_EMITTER); - if (isEmitter || isMutinyEmitter) { - // New emitter from the spec, or Mutiny emitter - handleEmitter(transformedAnnotations, appChannels, emitters, validationErrors, injectionPoint, broadcast, - channel, ReactiveMessagingDotNames.ON_OVERFLOW); - } - - if (isLegacyEmitter) { - // Deprecated Emitter from SmallRye (emitter, channel and on overflow have been added to the spec) - handleEmitter(transformedAnnotations, appChannels, emitters, validationErrors, injectionPoint, broadcast, - legacyChannel, ReactiveMessagingDotNames.LEGACY_ON_OVERFLOW); - } + if (emitterType != null) { + if (isLegacyEmitter) { + // Deprecated Emitter from SmallRye (emitter, channel and on overflow have been added to the spec) + handleEmitter(transformedAnnotations, appChannels, emitters, validationErrors, injectionPoint, + emitterType, broadcast, legacyChannel, ReactiveMessagingDotNames.LEGACY_ON_OVERFLOW); + } else { + // New emitter from the spec, or Mutiny emitter + handleEmitter(transformedAnnotations, appChannels, emitters, validationErrors, injectionPoint, + emitterType, broadcast, channel, ReactiveMessagingDotNames.ON_OVERFLOW); + } + } else { + if (channel.isPresent()) { + handleChannelInjection(appChannels, channels, channel.get()); + } - if (channel.isPresent() && !(isEmitter || isMutinyEmitter)) { - handleChannelInjection(appChannels, channels, channel.get()); + if (legacyChannel.isPresent()) { + handleChannelInjection(appChannels, channels, legacyChannel.get()); + } } - if (legacyChannel.isPresent() && !isLegacyEmitter) { - handleChannelInjection(appChannels, channels, legacyChannel.get()); - } } } @@ -175,6 +184,7 @@ private void handleEmitter(TransformedAnnotationsBuildItem transformedAnnotation BuildProducer emitters, BuildProducer validationErrors, InjectionPointInfo injectionPoint, + AnnotationInstance emitterType, Optional broadcast, Optional annotation, DotName onOverflowAnnotation) { @@ -187,7 +197,7 @@ private void handleEmitter(TransformedAnnotationsBuildItem transformedAnnotation String channelName = annotation.get().value().asString(); Optional overflow = WiringHelper.getAnnotation(transformedAnnotations, injectionPoint, onOverflowAnnotation); - createEmitter(appChannels, emitters, injectionPoint, channelName, overflow, broadcast); + createEmitter(appChannels, emitters, injectionPoint, channelName, emitterType, overflow, broadcast); } } @@ -365,14 +375,18 @@ public void autoConfigureConnectorForOrphansAndProduceManagedChannels( } @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private void createEmitter(BuildProducer appChannels, BuildProducer emitters, + private void createEmitter(BuildProducer appChannels, + BuildProducer emitters, InjectionPointInfo injectionPoint, String channelName, + AnnotationInstance emitter, Optional overflow, Optional broadcast) { LOGGER.debugf("Emitter injection point '%s' detected, channel name: '%s'", injectionPoint.getTargetInfo(), channelName); + String emitterTypeName = emitter.value().asClass().name().toString(); + boolean hasBroadcast = false; int awaitSubscribers = -1; int bufferSize = -1; @@ -390,12 +404,10 @@ private void createEmitter(BuildProducer appChannels, BuildPro strategy = annotation.value().asString(); } - boolean isMutinyEmitter = injectionPoint.getRequiredType().name() - .equals(ReactiveMessagingDotNames.MUTINY_EMITTER); produceOutgoingChannel(appChannels, channelName); emitters.produce( InjectedEmitterBuildItem - .of(channelName, isMutinyEmitter, strategy, bufferSize, hasBroadcast, awaitSubscribers)); + .of(channelName, emitterTypeName, strategy, bufferSize, hasBroadcast, awaitSubscribers)); } } diff --git a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/items/InjectedEmitterBuildItem.java b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/items/InjectedEmitterBuildItem.java index c50e22c60dec5..7d534c7e41494 100644 --- a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/items/InjectedEmitterBuildItem.java +++ b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/items/InjectedEmitterBuildItem.java @@ -3,7 +3,9 @@ import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.smallrye.reactivemessaging.deployment.BroadcastLiteral; import io.quarkus.smallrye.reactivemessaging.deployment.OnOverflowLiteral; -import io.smallrye.reactive.messaging.providers.extension.EmitterConfiguration; +import io.quarkus.smallrye.reactivemessaging.runtime.EmitterFactoryForLiteral; +import io.quarkus.smallrye.reactivemessaging.runtime.QuarkusEmitterConfiguration; +import io.smallrye.reactive.messaging.EmitterConfiguration; /** * Represents an emitter injection. @@ -14,15 +16,15 @@ public final class InjectedEmitterBuildItem extends MultiBuildItem { * Creates a new instance of {@link InjectedEmitterBuildItem} setting the overflow strategy. * * @param name the name of the stream - * @param isMutinyEmitter if the emitter is a {@link io.smallrye.reactive.messaging.MutinyEmitter} + * @param emitterType emitterType * @param overflow the overflow strategy * @param bufferSize the buffer size, if overflow is set to {@code BUFFER} * @return the new {@link InjectedEmitterBuildItem} */ - public static InjectedEmitterBuildItem of(String name, boolean isMutinyEmitter, String overflow, int bufferSize, + public static InjectedEmitterBuildItem of(String name, String emitterType, String overflow, int bufferSize, boolean hasBroadcast, int awaitSubscribers) { - return new InjectedEmitterBuildItem(name, isMutinyEmitter, overflow, bufferSize, hasBroadcast, awaitSubscribers); + return new InjectedEmitterBuildItem(name, emitterType, overflow, bufferSize, hasBroadcast, awaitSubscribers); } /** @@ -48,9 +50,9 @@ public static InjectedEmitterBuildItem of(String name, boolean isMutinyEmitter, private final boolean hasBroadcast; /** - * Whether the emitter is a {@link io.smallrye.reactive.messaging.MutinyEmitter} or a regular (non-mutiny) emitter. + * The emitter type */ - private final boolean isMutinyEmitter; + private final String emitterType; /** * If the emitter uses the {@link io.smallrye.reactive.messaging.annotations.Broadcast} annotation, indicates the @@ -58,19 +60,30 @@ public static InjectedEmitterBuildItem of(String name, boolean isMutinyEmitter, */ private final int awaitSubscribers; - public InjectedEmitterBuildItem(String name, boolean isMutinyEmitter, String overflow, int bufferSize, boolean hasBroadcast, + public InjectedEmitterBuildItem(String name, String emitterType, String overflow, int bufferSize, + boolean hasBroadcast, int awaitSubscribers) { this.name = name; this.overflow = overflow; - this.isMutinyEmitter = isMutinyEmitter; + this.emitterType = emitterType; this.bufferSize = bufferSize; this.hasBroadcast = hasBroadcast; this.awaitSubscribers = hasBroadcast ? awaitSubscribers : -1; } public EmitterConfiguration getEmitterConfig() { - return new EmitterConfiguration(name, isMutinyEmitter, OnOverflowLiteral.create(overflow, bufferSize), + return new QuarkusEmitterConfiguration(name, EmitterFactoryForLiteral.of(loadEmitterClass()), + OnOverflowLiteral.create(overflow, bufferSize), hasBroadcast ? new BroadcastLiteral(awaitSubscribers) : null); } + private Class loadEmitterClass() { + try { + return Class.forName(emitterType, false, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + // should not happen + throw new RuntimeException(e); + } + } + } diff --git a/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/config/ConnectorDoubleQuoteConfigTest.java b/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/config/ConnectorDoubleQuoteConfigTest.java new file mode 100644 index 0000000000000..46b6350d0bfed --- /dev/null +++ b/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/config/ConnectorDoubleQuoteConfigTest.java @@ -0,0 +1,51 @@ +package io.quarkus.smallrye.reactivemessaging.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class ConnectorDoubleQuoteConfigTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(DumbConnector.class, BeanUsingDummyConnector.class)) + .overrideConfigKey("mp.messaging.incoming.\"a.b\".values", "bonjour") + .overrideConfigKey("mp.messaging.incoming.\"a.b\".connector", "dummy"); + + @Inject + BeanUsingDummyConnector bean; + + @Test + public void test() { + await().until(() -> bean.getList().size() == 2); + assertThat(bean.getList()).containsExactly("bonjour", "BONJOUR"); + } + + @ApplicationScoped + public static class BeanUsingDummyConnector { + + private List list = new CopyOnWriteArrayList<>(); + + @Incoming("a.b") + public void consume(String s) { + list.add(s); + } + + public List getList() { + return list; + } + + } +} diff --git a/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/wiring/ConnectorAttachmentCustomEmitterTest.java b/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/wiring/ConnectorAttachmentCustomEmitterTest.java new file mode 100644 index 0000000000000..652321db30178 --- /dev/null +++ b/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/wiring/ConnectorAttachmentCustomEmitterTest.java @@ -0,0 +1,117 @@ +package io.quarkus.smallrye.reactivemessaging.wiring; + +import static org.awaitility.Awaitility.await; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.Typed; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.inject.Inject; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.spi.Connector; +import org.eclipse.microprofile.reactive.messaging.spi.OutgoingConnectorFactory; +import org.eclipse.microprofile.reactive.streams.operators.ReactiveStreams; +import org.eclipse.microprofile.reactive.streams.operators.SubscriberBuilder; +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 org.reactivestreams.Publisher; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.ChannelRegistry; +import io.smallrye.reactive.messaging.EmitterConfiguration; +import io.smallrye.reactive.messaging.EmitterFactory; +import io.smallrye.reactive.messaging.EmitterType; +import io.smallrye.reactive.messaging.MessagePublisherProvider; +import io.smallrye.reactive.messaging.annotations.EmitterFactoryFor; +import io.smallrye.reactive.messaging.providers.extension.ChannelProducer; + +public class ConnectorAttachmentCustomEmitterTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(MyDummyConnector.class, MySink.class, + CustomEmitter.class, CustomEmitterImpl.class, CustomEmitterFactory.class)); + + @Inject + @Connector("dummy") + MyDummyConnector connector; + + @Test + public void testAutoAttachmentOfOutgoingChannel() { + await().until(() -> connector.getList().size() == 5); + } + + @ApplicationScoped + static class MySink { + + @Channel("sink") + CustomEmitter channel; + + @PostConstruct + public void init() { + assert Objects.nonNull(channel); + } + } + + @ApplicationScoped + @Connector("dummy") + static class MyDummyConnector implements OutgoingConnectorFactory { + + private final List> list = new CopyOnWriteArrayList<>(); + + @Override + public SubscriberBuilder, Void> getSubscriberBuilder(Config config) { + return ReactiveStreams.> builder().forEach(list::add); + } + + public List> getList() { + return list; + } + + } + + interface CustomEmitter extends EmitterType { + + } + + static class CustomEmitterImpl implements MessagePublisherProvider, CustomEmitter { + + @Override + public Publisher> getPublisher() { + return Multi.createFrom().range(0, 5).map(Message::of).map(m -> (Message) m); + } + } + + @ApplicationScoped + @EmitterFactoryFor(CustomEmitter.class) + static class CustomEmitterFactory implements EmitterFactory> { + @Inject + ChannelRegistry channelRegistry; + + @Override + public CustomEmitterImpl createEmitter(EmitterConfiguration configuration, long defaultBufferSize) { + return new CustomEmitterImpl<>(); + } + + @Produces + @Typed(CustomEmitter.class) + @Channel("") // Stream name is ignored during type-safe resolution + CustomEmitter produceEmitter(InjectionPoint injectionPoint) { + String channelName = ChannelProducer.getChannelName(injectionPoint); + return channelRegistry.getEmitter(channelName, CustomEmitter.class); + } + + } +} diff --git a/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/wiring/IncomingChannelWithDotsTest.java b/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/wiring/IncomingChannelWithDotsTest.java new file mode 100644 index 0000000000000..33ea46cae0249 --- /dev/null +++ b/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/wiring/IncomingChannelWithDotsTest.java @@ -0,0 +1,65 @@ +package io.quarkus.smallrye.reactivemessaging.wiring; + +import static org.awaitility.Awaitility.await; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.spi.Connector; +import org.eclipse.microprofile.reactive.messaging.spi.IncomingConnectorFactory; +import org.eclipse.microprofile.reactive.streams.operators.PublisherBuilder; +import org.eclipse.microprofile.reactive.streams.operators.ReactiveStreams; +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.smallrye.mutiny.Multi; + +public class IncomingChannelWithDotsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(MyDummyConnector.class, MySink.class)); + + @Inject + MySink sink; + + @Test + public void testAutoAttachmentOfIncomingChannel() { + await().until(() -> sink.items().size() == 5); + } + + @ApplicationScoped + @Connector("dummy") + static class MyDummyConnector implements IncomingConnectorFactory { + + @Override + public PublisherBuilder> getPublisherBuilder(Config config) { + return ReactiveStreams.fromPublisher(Multi.createFrom().range(0, 5).map(Message::of)); + } + } + + @ApplicationScoped + static class MySink { + private final List items = new CopyOnWriteArrayList<>(); + + @Incoming("a.b") + public void sink(int l) { + items.add(l); + } + + public List items() { + return items; + } + } + +} diff --git a/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/wiring/OutgoingWithDotsTest.java b/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/wiring/OutgoingWithDotsTest.java new file mode 100644 index 0000000000000..f57eaca16438b --- /dev/null +++ b/extensions/smallrye-reactive-messaging/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/wiring/OutgoingWithDotsTest.java @@ -0,0 +1,66 @@ +package io.quarkus.smallrye.reactivemessaging.wiring; + +import static org.awaitility.Awaitility.await; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; +import org.eclipse.microprofile.reactive.messaging.spi.Connector; +import org.eclipse.microprofile.reactive.messaging.spi.OutgoingConnectorFactory; +import org.eclipse.microprofile.reactive.streams.operators.ReactiveStreams; +import org.eclipse.microprofile.reactive.streams.operators.SubscriberBuilder; +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.smallrye.mutiny.Multi; + +public class OutgoingWithDotsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(MyDummyConnector.class, MySource.class)); + + @Inject + @Connector("dummy") + MyDummyConnector connector; + + @Test + public void testAutoAttachmentOfOutgoingChannel() { + await().until(() -> connector.getList().size() == 5); + } + + @ApplicationScoped + @Connector("dummy") + static class MyDummyConnector implements OutgoingConnectorFactory { + + private final List> list = new CopyOnWriteArrayList<>(); + + @Override + public SubscriberBuilder, Void> getSubscriberBuilder(Config config) { + return ReactiveStreams.> builder().forEach(list::add); + } + + public List> getList() { + return list; + } + } + + @ApplicationScoped + static class MySource { + @Outgoing("c.d") + public Multi generate() { + return Multi.createFrom().range(0, 5); + } + } + +} diff --git a/extensions/smallrye-reactive-messaging/kotlin/src/main/kotlin/io/quarkus/smallrye/reactivemessaging/runtime/kotlin/AbstractSubscribingCoroutineInvoker.kt b/extensions/smallrye-reactive-messaging/kotlin/src/main/kotlin/io/quarkus/smallrye/reactivemessaging/runtime/kotlin/AbstractSubscribingCoroutineInvoker.kt index 1fb7a369463b0..27df4ef209158 100644 --- a/extensions/smallrye-reactive-messaging/kotlin/src/main/kotlin/io/quarkus/smallrye/reactivemessaging/runtime/kotlin/AbstractSubscribingCoroutineInvoker.kt +++ b/extensions/smallrye-reactive-messaging/kotlin/src/main/kotlin/io/quarkus/smallrye/reactivemessaging/runtime/kotlin/AbstractSubscribingCoroutineInvoker.kt @@ -13,7 +13,7 @@ abstract class AbstractSubscribingCoroutineInvoker(private val beanInstance: Any override fun invoke(vararg args: Any?): CompletableFuture { val coroutineScope = Arc.container().instance(ApplicationCoroutineScope::class.java).get() val dispatcher: CoroutineDispatcher = Vertx.currentContext()?.let(::VertxDispatcher) - ?: throw IllegalStateException("No Vertx context found") + ?: throw IllegalStateException("No Vertx context found. Consider using @NonBlocking on the caller method, or make sure the upstream emits items on the Vert.x context") return coroutineScope.async(context = dispatcher) { invokeBean(beanInstance, args) diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/EmitterFactoryForLiteral.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/EmitterFactoryForLiteral.java new file mode 100644 index 0000000000000..49ab75718faa6 --- /dev/null +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/EmitterFactoryForLiteral.java @@ -0,0 +1,39 @@ +package io.quarkus.smallrye.reactivemessaging.runtime; + +import java.lang.annotation.Annotation; + +import io.smallrye.reactive.messaging.annotations.EmitterFactoryFor; + +public class EmitterFactoryForLiteral implements EmitterFactoryFor { + + private Class value; + + public static EmitterFactoryFor of(Class type) { + return new io.quarkus.smallrye.reactivemessaging.runtime.EmitterFactoryForLiteral(type); + } + + public EmitterFactoryForLiteral() { + } + + public EmitterFactoryForLiteral(Class type) { + this.value = type; + } + + public Class getValue() { + return value; + } + + public void setValue(Class value) { + this.value = value; + } + + @Override + public Class value() { + return value; + } + + @Override + public Class annotationType() { + return EmitterFactoryFor.class; + } +} diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/QuarkusEmitterConfiguration.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/QuarkusEmitterConfiguration.java new file mode 100644 index 0000000000000..2b33e70c7f3b9 --- /dev/null +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/QuarkusEmitterConfiguration.java @@ -0,0 +1,118 @@ +package io.quarkus.smallrye.reactivemessaging.runtime; + +import org.eclipse.microprofile.reactive.messaging.OnOverflow; + +import io.smallrye.reactive.messaging.EmitterConfiguration; +import io.smallrye.reactive.messaging.annotations.Broadcast; +import io.smallrye.reactive.messaging.annotations.EmitterFactoryFor; + +public class QuarkusEmitterConfiguration implements EmitterConfiguration { + + private String name; + private EmitterFactoryFor emitterType; + private OnOverflow.Strategy overflowBufferStrategy; + private long overflowBufferSize; + private boolean broadcast; + private int numberOfSubscriberBeforeConnecting; + + public QuarkusEmitterConfiguration() { + } + + public QuarkusEmitterConfiguration(String name, EmitterFactoryFor emitterType, OnOverflow onOverflow, Broadcast broadcast) { + this.name = name; + this.emitterType = emitterType; + + if (onOverflow != null) { + this.overflowBufferStrategy = onOverflow.value(); + this.overflowBufferSize = onOverflow.bufferSize(); + } else { + this.overflowBufferStrategy = null; + this.overflowBufferSize = -1; + } + + if (broadcast != null) { + this.broadcast = Boolean.TRUE; + this.numberOfSubscriberBeforeConnecting = broadcast.value(); + } else { + this.numberOfSubscriberBeforeConnecting = -1; + } + } + + @Override + public String name() { + return name; + } + + @Override + public EmitterFactoryFor emitterType() { + return emitterType; + } + + @Override + public OnOverflow.Strategy overflowBufferStrategy() { + return overflowBufferStrategy; + } + + @Override + public long overflowBufferSize() { + return overflowBufferSize; + } + + @Override + public boolean broadcast() { + return broadcast; + } + + @Override + public int numberOfSubscriberBeforeConnecting() { + return numberOfSubscriberBeforeConnecting; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public EmitterFactoryFor getEmitterType() { + return emitterType; + } + + public void setEmitterType(EmitterFactoryFor emitterType) { + this.emitterType = emitterType; + } + + public OnOverflow.Strategy getOverflowBufferStrategy() { + return overflowBufferStrategy; + } + + public void setOverflowBufferStrategy(OnOverflow.Strategy overflowBufferStrategy) { + this.overflowBufferStrategy = overflowBufferStrategy; + } + + public long getOverflowBufferSize() { + return overflowBufferSize; + } + + public void setOverflowBufferSize(long overflowBufferSize) { + this.overflowBufferSize = overflowBufferSize; + } + + public boolean isBroadcast() { + return broadcast; + } + + public void setBroadcast(boolean broadcast) { + this.broadcast = broadcast; + } + + public int getNumberOfSubscriberBeforeConnecting() { + return numberOfSubscriberBeforeConnecting; + } + + public void setNumberOfSubscriberBeforeConnecting(int numberOfSubscriberBeforeConnecting) { + this.numberOfSubscriberBeforeConnecting = numberOfSubscriberBeforeConnecting; + } +} diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/SmallRyeReactiveMessagingLifecycle.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/SmallRyeReactiveMessagingLifecycle.java index 6a6fcc4a33f0e..b42cd10fda0bb 100644 --- a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/SmallRyeReactiveMessagingLifecycle.java +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/SmallRyeReactiveMessagingLifecycle.java @@ -11,8 +11,8 @@ import javax.interceptor.Interceptor; import io.quarkus.runtime.StartupEvent; +import io.smallrye.reactive.messaging.EmitterConfiguration; import io.smallrye.reactive.messaging.providers.extension.ChannelConfiguration; -import io.smallrye.reactive.messaging.providers.extension.EmitterConfiguration; import io.smallrye.reactive.messaging.providers.extension.MediatorManager; @Dependent diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/SmallRyeReactiveMessagingRecorder.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/SmallRyeReactiveMessagingRecorder.java index 9d2f0a2a9cae3..c64d59a38b5ff 100644 --- a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/SmallRyeReactiveMessagingRecorder.java +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/SmallRyeReactiveMessagingRecorder.java @@ -4,8 +4,8 @@ import java.util.function.Supplier; import io.quarkus.runtime.annotations.Recorder; +import io.smallrye.reactive.messaging.EmitterConfiguration; import io.smallrye.reactive.messaging.providers.extension.ChannelConfiguration; -import io.smallrye.reactive.messaging.providers.extension.EmitterConfiguration; @Recorder public class SmallRyeReactiveMessagingRecorder { diff --git a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devconsole/DevReactiveMessagingInfos.java b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devconsole/DevReactiveMessagingInfos.java index 032acdf52dd89..4d0446a5e5e7a 100644 --- a/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devconsole/DevReactiveMessagingInfos.java +++ b/extensions/smallrye-reactive-messaging/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/runtime/devconsole/DevReactiveMessagingInfos.java @@ -15,9 +15,9 @@ import io.quarkus.arc.ArcContainer; import io.quarkus.arc.impl.LazyValue; import io.quarkus.smallrye.reactivemessaging.runtime.SmallRyeReactiveMessagingRecorder.SmallRyeReactiveMessagingContext; +import io.smallrye.reactive.messaging.EmitterConfiguration; import io.smallrye.reactive.messaging.MediatorConfiguration; import io.smallrye.reactive.messaging.providers.extension.ChannelConfiguration; -import io.smallrye.reactive.messaging.providers.extension.EmitterConfiguration; public class DevReactiveMessagingInfos { @@ -47,10 +47,10 @@ public List get() { } for (EmitterConfiguration emitter : context.getEmitterConfigurations()) { - publishers.put(emitter.name, + publishers.put(emitter.name(), new Component(ComponentType.EMITTER, - emitter.broadcast ? "@Broadcast " - : "" + asCode(DevConsoleRecorder.EMITTERS.get(emitter.name)))); + emitter.broadcast() ? "@Broadcast " + : "" + asCode(DevConsoleRecorder.EMITTERS.get(emitter.name())))); } for (ChannelConfiguration channel : context.getChannelConfigurations()) { consumers.computeIfAbsent(channel.channelName, fun) diff --git a/extensions/smallrye-stork/deployment/src/main/java/io/quarkus/stork/deployment/SmallRyeStorkProcessor.java b/extensions/smallrye-stork/deployment/src/main/java/io/quarkus/stork/deployment/SmallRyeStorkProcessor.java index 45d96371a0f69..4fea62e416a07 100644 --- a/extensions/smallrye-stork/deployment/src/main/java/io/quarkus/stork/deployment/SmallRyeStorkProcessor.java +++ b/extensions/smallrye-stork/deployment/src/main/java/io/quarkus/stork/deployment/SmallRyeStorkProcessor.java @@ -2,11 +2,18 @@ import static java.util.Arrays.asList; +import org.jboss.logging.Logger; + import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.builder.item.EmptyBuildItem; +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.Consume; import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Produce; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; @@ -20,18 +27,46 @@ public class SmallRyeStorkProcessor { + private static final String KUBERNETES_SERVICE_DISCOVERY_PROVIDER = "io.smallrye.stork.servicediscovery.kubernetes.KubernetesServiceDiscoveryProvider"; + private static final Logger LOGGER = Logger.getLogger(SmallRyeStorkProcessor.class.getName()); + @BuildStep - void registerServiceProviders(BuildProducer services) { + void registerServiceProviders(BuildProducer services, Capabilities capabilities) { services.produce(new ServiceProviderBuildItem(io.smallrye.stork.spi.config.ConfigProvider.class.getName(), StorkConfigProvider.class.getName())); - for (Class providerClass : asList(LoadBalancerLoader.class, ServiceDiscoveryLoader.class)) { services.produce(ServiceProviderBuildItem.allProvidersFromClassPath(providerClass.getName())); } } + /** + * This build step is the fix for https://github.com/quarkusio/quarkus/issues/24444. + * Because Stork itself cannot depend on Quarkus, and we do not want to have extensions for all the service + * discovery and load-balancer providers, we work around the issue by detecting when the kubernetes service + * discovery is used and if the kubernetes extension is used. + */ + @BuildStep + @Produce(AlwaysBuildItem.class) + void checkThatTheKubernetesExtensionIsUsedWhenKubernetesServiceDiscoveryInOnTheClasspath(Capabilities capabilities) { + if (QuarkusClassLoader.isClassPresentAtRuntime(KUBERNETES_SERVICE_DISCOVERY_PROVIDER)) { + if (!capabilities.isPresent(Capability.KUBERNETES_CLIENT)) { + LOGGER.warn( + "The application is using the Stork Kubernetes Service Discovery provider but does not depend on the `quarkus-kubernetes-client` extension. " + + + "It is highly recommended to use the `io.quarkus:quarkus-kubernetes-client` extension with the Kubernetes service discovery. \n" + + + "To add this extension:" + + "\n - with the quarkus CLI, run: `quarkus ext add io.quarkus:quarkus-kubernetes-client`" + + "\n - with Apache Maven, run: `./mvnw quarkus:add-extension -Dextensions=\"io.quarkus:quarkus-kubernetes-client\"`" + + + "\n - or just add the `io.quarkus:quarkus-kubernetes-client` dependency to the project"); + } + } + } + @BuildStep @Record(ExecutionTime.RUNTIME_INIT) + @Consume(AlwaysBuildItem.class) @Consume(RuntimeConfigSetupCompleteBuildItem.class) @Consume(SyntheticBeansRuntimeInitBuildItem.class) void initializeStork(SmallRyeStorkRecorder storkRecorder, ShutdownContextBuildItem shutdown, VertxBuildItem vertx, @@ -39,4 +74,8 @@ void initializeStork(SmallRyeStorkRecorder storkRecorder, ShutdownContextBuildIt storkRecorder.initialize(shutdown, vertx.getVertx(), configuration); } + private static final class AlwaysBuildItem extends EmptyBuildItem { + // Just here to be sure we run the `checkThatTheKubernetesExtensionIsUsedWhenKubernetesServiceDiscoveryInOnTheClasspath` build step. + } + } diff --git a/extensions/spring-cloud-config-client/runtime/pom.xml b/extensions/spring-cloud-config-client/runtime/pom.xml index 6f28616ac0bdb..b1dfd7c14250f 100644 --- a/extensions/spring-cloud-config-client/runtime/pom.xml +++ b/extensions/spring-cloud-config-client/runtime/pom.xml @@ -108,5 +108,28 @@ + + + jakarta-rewrite + + + jakarta-rewrite + + + + + + org.openrewrite.maven + rewrite-maven-plugin + + + io.quarkus.jakarta-jaxrs-switch + + + + + + + - \ No newline at end of file + diff --git a/extensions/spring-data-jpa/deployment/pom.xml b/extensions/spring-data-jpa/deployment/pom.xml index 26b6db74c2db5..c9d9f16913e7c 100644 --- a/extensions/spring-data-jpa/deployment/pom.xml +++ b/extensions/spring-data-jpa/deployment/pom.xml @@ -11,7 +11,7 @@ 4.0.0 quarkus-spring-data-jpa-deployment - Quarkus - Spring - Data JPA - Deployment + Quarkus - Spring Data JPA - Deployment diff --git a/extensions/spring-data-jpa/runtime/pom.xml b/extensions/spring-data-jpa/runtime/pom.xml index d5b34f91444a0..43b864f510ad5 100644 --- a/extensions/spring-data-jpa/runtime/pom.xml +++ b/extensions/spring-data-jpa/runtime/pom.xml @@ -11,7 +11,7 @@ 4.0.0 quarkus-spring-data-jpa - Quarkus - Spring - Data JPA - Runtime + Quarkus - Spring Data JPA - Runtime Use Spring Data JPA annotations to create your data access layer diff --git a/extensions/spring-data-rest/deployment/pom.xml b/extensions/spring-data-rest/deployment/pom.xml index e3abeaec08689..9a7f1082a4298 100644 --- a/extensions/spring-data-rest/deployment/pom.xml +++ b/extensions/spring-data-rest/deployment/pom.xml @@ -11,7 +11,7 @@ 4.0.0 quarkus-spring-data-rest-deployment - Quarkus - Spring - Data REST - Deployment + Quarkus - Spring Data - REST - Deployment @@ -41,6 +41,11 @@ quarkus-resteasy-jsonb-deployment test + + io.quarkus + quarkus-resteasy-links-deployment + test + io.rest-assured rest-assured diff --git a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java index e3b67da1513f7..a54899eeb4fef 100644 --- a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java +++ b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java @@ -21,6 +21,7 @@ import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; @@ -29,6 +30,7 @@ import io.quarkus.rest.data.panache.RestDataPanacheException; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.RestDataResourceBuildItem; +import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; import io.quarkus.rest.data.panache.deployment.properties.ResourcePropertiesBuildItem; import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; import io.quarkus.resteasy.reactive.spi.ExceptionMapperBuildItem; @@ -75,27 +77,27 @@ AdditionalBeanBuildItem registerTransactionalExecutor() { } @BuildStep - void registerCrudRepositories(CombinedIndexBuildItem indexBuildItem, + void registerCrudRepositories(CombinedIndexBuildItem indexBuildItem, Capabilities capabilities, BuildProducer implementationsProducer, BuildProducer restDataResourceProducer, BuildProducer resourcePropertiesProducer, BuildProducer unremovableBeansProducer) { IndexView index = indexBuildItem.getIndex(); - implementResources(implementationsProducer, restDataResourceProducer, resourcePropertiesProducer, + implementResources(capabilities, implementationsProducer, restDataResourceProducer, resourcePropertiesProducer, unremovableBeansProducer, new CrudMethodsImplementor(index), new CrudPropertiesProvider(index), getRepositoriesToImplement(index, CRUD_REPOSITORY_INTERFACE)); } @BuildStep - void registerPagingAndSortingRepositories(CombinedIndexBuildItem indexBuildItem, + void registerPagingAndSortingRepositories(CombinedIndexBuildItem indexBuildItem, Capabilities capabilities, BuildProducer implementationsProducer, BuildProducer restDataResourceProducer, BuildProducer resourcePropertiesProducer, BuildProducer unremovableBeansProducer) { IndexView index = indexBuildItem.getIndex(); - implementResources(implementationsProducer, restDataResourceProducer, resourcePropertiesProducer, + implementResources(capabilities, implementationsProducer, restDataResourceProducer, resourcePropertiesProducer, unremovableBeansProducer, new PagingAndSortingMethodsImplementor(index), new PagingAndSortingPropertiesProvider(index), getRepositoriesToImplement(index, PAGING_AND_SORTING_REPOSITORY_INTERFACE, JPA_REPOSITORY_INTERFACE)); @@ -105,7 +107,8 @@ unremovableBeansProducer, new PagingAndSortingMethodsImplementor(index), * Implement the {@link io.quarkus.rest.data.panache.RestDataResource} interface for each given Spring Data * repository and register its metadata and properties to be later picked up by the `rest-data-panache` extension. */ - private void implementResources(BuildProducer implementationsProducer, + private void implementResources(Capabilities capabilities, + BuildProducer implementationsProducer, BuildProducer restDataResourceProducer, BuildProducer resourcePropertiesProducer, BuildProducer unremovableBeansProducer, @@ -127,8 +130,8 @@ private void implementResources(BuildProducer implementa new ResourceMetadata(resourceClass, repositoryInterface, entityType, idType))); // Spring Data repositories use different annotations for configuration and we translate them for // the rest-data-panache here. - resourcePropertiesProducer.produce(new ResourcePropertiesBuildItem(resourceClass, - propertiesProvider.getResourceProperties(repositoryInterface))); + ResourceProperties resourceProperties = propertiesProvider.getResourceProperties(repositoryInterface); + resourcePropertiesProducer.produce(new ResourcePropertiesBuildItem(resourceClass, resourceProperties)); // Make sure that repository bean is not removed and will be injected to the generated resource unremovableBeansProducer.produce(new UnremovableBeanBuildItem( new UnremovableBeanBuildItem.BeanTypeExclusion(DotName.createSimple(repositoryInterface)))); diff --git a/extensions/spring-data-rest/pom.xml b/extensions/spring-data-rest/pom.xml index 0901971381fed..9b5d3233be078 100644 --- a/extensions/spring-data-rest/pom.xml +++ b/extensions/spring-data-rest/pom.xml @@ -11,7 +11,7 @@ 4.0.0 quarkus-spring-data-rest-parent - Quarkus - Spring Data REST + Quarkus - Spring Data - REST pom diff --git a/extensions/spring-data-rest/runtime/pom.xml b/extensions/spring-data-rest/runtime/pom.xml index f1c159c2fb303..9bf3fb41e3ee7 100644 --- a/extensions/spring-data-rest/runtime/pom.xml +++ b/extensions/spring-data-rest/runtime/pom.xml @@ -11,7 +11,7 @@ 4.0.0 quarkus-spring-data-rest - Quarkus - Spring - Data REST - Runtime + Quarkus - Spring Data - REST - Runtime Generate JAX-RS resources for a Spring Data application diff --git a/extensions/spring-web/core/common-runtime/pom.xml b/extensions/spring-web/core/common-runtime/pom.xml index 11e227f56b883..b390650332744 100644 --- a/extensions/spring-web/core/common-runtime/pom.xml +++ b/extensions/spring-web/core/common-runtime/pom.xml @@ -39,4 +39,28 @@ quarkus-spring-context-api + + + + jakarta-rewrite + + + jakarta-rewrite + + + + + + org.openrewrite.maven + rewrite-maven-plugin + + + io.quarkus.jakarta-jaxrs-switch + + + + + + + diff --git a/extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java b/extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java index 3333e7501c34f..620e25691a3b9 100644 --- a/extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java +++ b/extensions/spring-web/core/deployment/src/main/java/io/quarkus/spring/web/deployment/AbstractExceptionMapperGenerator.java @@ -44,7 +44,7 @@ String generate() { try (ClassCreator cc = ClassCreator.builder() .classOutput(classOutput).className(generatedClassName) .interfaces(ExceptionMapper.class) - .signature(String.format("Ljava/lang/Object;Ljavax/ws/rs/ext/ExceptionMapper;", + .signature(String.format("Ljava/lang/Object;L" + ExceptionMapper.class.getName().replace(".", "/") + ";", exceptionClassName.replace('.', '/'))) .build()) { diff --git a/extensions/spring-web/resteasy-classic/runtime/pom.xml b/extensions/spring-web/resteasy-classic/runtime/pom.xml index e5d8688143837..42062defd3d62 100644 --- a/extensions/spring-web/resteasy-classic/runtime/pom.xml +++ b/extensions/spring-web/resteasy-classic/runtime/pom.xml @@ -59,4 +59,28 @@ + + + + jakarta-rewrite + + + jakarta-rewrite + + + + + + org.openrewrite.maven + rewrite-maven-plugin + + + io.quarkus.resteasy-spring-web + + + + + + + diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AllowBothForwardedHeadersTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AllowBothForwardedHeadersTest.java new file mode 100644 index 0000000000000..3427a18e3c667 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AllowBothForwardedHeadersTest.java @@ -0,0 +1,40 @@ +package io.quarkus.vertx.http; + +import static org.assertj.core.api.Assertions.assertThat; + +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.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class AllowBothForwardedHeadersTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(ForwardedHandlerInitializer.class) + .addAsResource(new StringAsset("quarkus.http.proxy.proxy-address-forwarding=true\n" + + "quarkus.http.proxy.allow-forwarded=true\n" + + "quarkus.http.proxy.allow-x-forwarded=true\n" + + "quarkus.http.proxy.enable-forwarded-host=true\n" + + "quarkus.http.proxy.enable-forwarded-prefix=true\n" + + "quarkus.http.proxy.forwarded-host-header=X-Forwarded-Server"), + "application.properties")); + + @Test + public void test() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("Forwarded", "proto=http;for=backend2:5555;host=somehost2") + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Server", "somehost") + .get("/path") + .then() + .body(Matchers.equalTo("http|somehost2|backend2:5555|/path|http://somehost2/path")); + } +} \ No newline at end of file diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AllowOnlyForwardedHeaderTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AllowOnlyForwardedHeaderTest.java new file mode 100644 index 0000000000000..739d47fa2552e --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AllowOnlyForwardedHeaderTest.java @@ -0,0 +1,68 @@ +package io.quarkus.vertx.http; + +import static org.assertj.core.api.Assertions.assertThat; + +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.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class AllowOnlyForwardedHeaderTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(ForwardedHandlerInitializer.class) + .addAsResource(new StringAsset("quarkus.http.proxy.proxy-address-forwarding=true\n" + + "quarkus.http.proxy.allow-forwarded=true\n" + + "quarkus.http.proxy.enable-forwarded-host=true\n" + + "quarkus.http.proxy.enable-forwarded-prefix=true\n" + + "quarkus.http.proxy.forwarded-host-header=X-Forwarded-Server"), + "application.properties")); + + @Test + public void testWithXForwardedSslAndXForwardedProto() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("Forwarded", "proto=http;for=backend2:5555;host=somehost2") + .header("X-Forwarded-Ssl", "on") + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Server", "somehost") + .get("/path") + .then() + .body(Matchers.equalTo("http|somehost2|backend2:5555|/path|http://somehost2/path")); + } + + @Test + public void testWithXForwardedProto() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("Forwarded", "proto=http;for=backend2:5555;host=somehost2") + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Server", "somehost") + .get("/path") + .then() + .body(Matchers.equalTo("http|somehost2|backend2:5555|/path|http://somehost2/path")); + } + + @Test + public void testWithXForwardedSsl() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("Forwarded", "proto=http;for=backend2:5555;host=somehost2") + .header("X-Forwarded-Ssl", "on") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Server", "somehost") + .get("/path") + .then() + .body(Matchers.equalTo("http|somehost2|backend2:5555|/path|http://somehost2/path")); + } +} \ No newline at end of file diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AllowOnlyXForwardedHeaderTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AllowOnlyXForwardedHeaderTest.java new file mode 100644 index 0000000000000..5799802a6e113 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AllowOnlyXForwardedHeaderTest.java @@ -0,0 +1,68 @@ +package io.quarkus.vertx.http; + +import static org.assertj.core.api.Assertions.assertThat; + +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.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class AllowOnlyXForwardedHeaderTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(ForwardedHandlerInitializer.class) + .addAsResource(new StringAsset("quarkus.http.proxy.proxy-address-forwarding=true\n" + + "quarkus.http.proxy.allow-x-forwarded=true\n" + + "quarkus.http.proxy.enable-forwarded-host=true\n" + + "quarkus.http.proxy.enable-forwarded-prefix=true\n" + + "quarkus.http.proxy.forwarded-host-header=X-Forwarded-Server"), + "application.properties")); + + @Test + public void testWithXForwardedSslOn() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("Forwarded", "proto=http;for=backend2:5555;host=somehost2") + .header("X-Forwarded-Ssl", "on") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Server", "somehost") + .get("/path") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|/path|https://somehost/path")); + } + + @Test + public void testWithXForwardedProto() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("Forwarded", "proto=http;for=backend2:5555;host=somehost2") + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Server", "somehost") + .get("/path") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|/path|https://somehost/path")); + } + + @Test + public void testWithXForwardedProtoAndXForwardedSsl() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("Forwarded", "proto=http;for=backend2:5555;host=somehost2") + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-Ssl", "on") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Server", "somehost") + .get("/path") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|/path|https://somehost/path")); + } +} \ No newline at end of file diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AllowOnlyXForwardedHeaderUsingDefaultConfigTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AllowOnlyXForwardedHeaderUsingDefaultConfigTest.java new file mode 100644 index 0000000000000..076d672d8542a --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/AllowOnlyXForwardedHeaderUsingDefaultConfigTest.java @@ -0,0 +1,67 @@ +package io.quarkus.vertx.http; + +import static org.assertj.core.api.Assertions.assertThat; + +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.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class AllowOnlyXForwardedHeaderUsingDefaultConfigTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(ForwardedHandlerInitializer.class) + .addAsResource(new StringAsset("quarkus.http.proxy.proxy-address-forwarding=true\n" + + "quarkus.http.proxy.enable-forwarded-host=true\n" + + "quarkus.http.proxy.enable-forwarded-prefix=true\n" + + "quarkus.http.proxy.forwarded-host-header=X-Forwarded-Server"), + "application.properties")); + + @Test + public void testWithXForwardedSsl() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("Forwarded", "proto=http;for=backend2:5555;host=somehost2") + .header("X-Forwarded-Ssl", "on") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Server", "somehost") + .get("/path") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|/path|https://somehost/path")); + } + + @Test + public void testWithXForwardedProto() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("Forwarded", "proto=http;for=backend2:5555;host=somehost2") + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Server", "somehost") + .get("/path") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|/path|https://somehost/path")); + } + + @Test + public void testWithXForwardedProtoAndXForwardedSsl() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("Forwarded", "proto=http;for=backend2:5555;host=somehost2") + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-Ssl", "on") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Server", "somehost") + .get("/path") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|/path|https://somehost/path")); + } +} \ No newline at end of file diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHeaderTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHeaderTest.java index 41b7eaa6e9f26..9fc8a8b841d73 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHeaderTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHeaderTest.java @@ -23,7 +23,6 @@ public class ForwardedHeaderTest { @Test public void test() { assertThat(RestAssured.get("/forward").asString()).startsWith("http|"); - RestAssured.given() .header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https") .get("/forward") @@ -31,4 +30,36 @@ public void test() { .body(Matchers.equalTo("https|somehost|backend:4444")); } + @Test + public void testForwardedForWithSequenceOfProxies() { + assertThat(RestAssured.get("/forward").asString()).startsWith("http|"); + + RestAssured.given() + .header("Forwarded", "by=proxy;for=backend:4444,for=backend2:5555;host=somehost;proto=https") + .get("/forward") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444")); + } + + @Test + public void testForwardedWithSequenceOfProxiesIncludingIpv6Address() { + assertThat(RestAssured.get("/forward").asString()).startsWith("http|"); + + RestAssured.given() + .header("Forwarded", "by=proxy;for=\"[2001:db8:cafe::17]:47011\",for=backend:4444;host=somehost;proto=https") + .get("/forward") + .then() + .body(Matchers.equalTo("https|somehost|[2001:db8:cafe::17]:47011")); + } + + @Test + public void testForwardedForWithIpv6Address2() { + assertThat(RestAssured.get("/forward").asString()).startsWith("http|"); + + RestAssured.given() + .header("Forwarded", "by=proxy;for=\"[2001:db8:cafe::17]:47011\",for=backend:4444;host=somehost;proto=https") + .get("/forward") + .then() + .body(Matchers.equalTo("https|somehost|[2001:db8:cafe::17]:47011")); + } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathWithHttpRootTestCase.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathWithHttpRootTestCase.java index 347e4714052fe..4672f66bfe074 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathWithHttpRootTestCase.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathWithHttpRootTestCase.java @@ -25,7 +25,7 @@ public static void setup() { private static final String APP_PROPS = "" + "# Add your application.properties here, if applicable.\n" + "quarkus.http.root-path=/root\n" + - "quarkus.http.auth.permission.authenticated.paths=/admin\n" + + "quarkus.http.auth.permission.authenticated.paths=admin\n" + "quarkus.http.auth.permission.authenticated.policy=authenticated\n"; @RegisterExtension diff --git a/extensions/vertx-http/runtime/pom.xml b/extensions/vertx-http/runtime/pom.xml index a5326ef98b710..1d27bba131af9 100644 --- a/extensions/vertx-http/runtime/pom.xml +++ b/extensions/vertx-http/runtime/pom.xml @@ -66,6 +66,10 @@ svm provided + + io.github.crac + org-crac + org.junit.jupiter diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FilterConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FilterConfig.java new file mode 100644 index 0000000000000..285a581d7759f --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FilterConfig.java @@ -0,0 +1,36 @@ +package io.quarkus.vertx.http.runtime; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class FilterConfig { + + /** + * A regular expression for the paths matching this configuration + */ + @ConfigItem + public String matches; + + /** + * Additional HTTP Headers always sent in the response + */ + @ConfigItem + public Map header; + + /** + * The HTTP methods for this path configuration + */ + @ConfigItem + public Optional> methods; + + /** + * Order in which this path config is applied. Higher priority takes precedence + */ + public OptionalInt order; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java index 437e0e6002965..3755c61871531 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java @@ -108,36 +108,33 @@ private void calculate() { setHostAndPort(delegate.host(), port); uri = delegate.uri(); - String forwardedSsl = delegate.getHeader(X_FORWARDED_SSL); - boolean isForwardedSslOn = forwardedSsl != null && forwardedSsl.equalsIgnoreCase("on"); - String forwarded = delegate.getHeader(FORWARDED); if (forwardingProxyOptions.allowForwarded && forwarded != null) { - String forwardedToUse = forwarded.split(",")[0]; - Matcher matcher = FORWARDED_PROTO_PATTERN.matcher(forwardedToUse); + Matcher matcher = FORWARDED_PROTO_PATTERN.matcher(forwarded); if (matcher.find()) { scheme = (matcher.group(1).trim()); port = -1; - } else if (isForwardedSslOn) { - scheme = HTTPS_SCHEME; - port = -1; } - matcher = FORWARDED_HOST_PATTERN.matcher(forwardedToUse); + matcher = FORWARDED_HOST_PATTERN.matcher(forwarded); if (matcher.find()) { setHostAndPort(matcher.group(1).trim(), port); } - matcher = FORWARDED_FOR_PATTERN.matcher(forwardedToUse); + matcher = FORWARDED_FOR_PATTERN.matcher(forwarded); if (matcher.find()) { remoteAddress = parseFor(matcher.group(1).trim(), remoteAddress.port()); } - } else if (!forwardingProxyOptions.allowForwarded) { + } else if (forwardingProxyOptions.allowXForwarded) { String protocolHeader = delegate.getHeader(X_FORWARDED_PROTO); if (protocolHeader != null) { - scheme = protocolHeader.split(",")[0]; + scheme = getFirstElement(protocolHeader); port = -1; - } else if (isForwardedSslOn) { + } + + String forwardedSsl = delegate.getHeader(X_FORWARDED_SSL); + boolean isForwardedSslOn = forwardedSsl != null && forwardedSsl.equalsIgnoreCase("on"); + if (isForwardedSslOn) { scheme = HTTPS_SCHEME; port = -1; } @@ -145,7 +142,7 @@ private void calculate() { if (forwardingProxyOptions.enableForwardedHost) { String hostHeader = delegate.getHeader(forwardingProxyOptions.forwardedHostHeader); if (hostHeader != null) { - setHostAndPort(hostHeader.split(",")[0], port); + setHostAndPort(getFirstElement(hostHeader), port); } } @@ -158,12 +155,12 @@ private void calculate() { String portHeader = delegate.getHeader(X_FORWARDED_PORT); if (portHeader != null) { - port = parsePort(portHeader.split(",")[0], port); + port = parsePort(getFirstElement(portHeader), port); } String forHeader = delegate.getHeader(X_FORWARDED_FOR); if (forHeader != null) { - remoteAddress = parseFor(forHeader.split(",")[0], remoteAddress.port()); + remoteAddress = parseFor(getFirstElement(forHeader), remoteAddress.port()); } } @@ -194,6 +191,11 @@ private SocketAddress parseFor(String forToParse, int defaultPort) { return new SocketAddressImpl(port, host); } + private String getFirstElement(String value) { + int index = value.indexOf(','); + return index == -1 ? value : value.substring(0, index); + } + /** * Returns a String[] of 2 elements, with the first being the host and the second the port */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java index 90ec58978c830..0a380bc32ede7 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java @@ -5,6 +5,7 @@ public class ForwardingProxyOptions { boolean proxyAddressForwarding; boolean allowForwarded; + boolean allowXForwarded; boolean enableForwardedHost; boolean enableForwardedPrefix; AsciiString forwardedHostHeader; @@ -12,12 +13,14 @@ public class ForwardingProxyOptions { public ForwardingProxyOptions(final boolean proxyAddressForwarding, final boolean allowForwarded, + final boolean allowXForwarded, final boolean enableForwardedHost, final AsciiString forwardedHostHeader, final boolean enableForwardedPrefix, final AsciiString forwardedPrefixHeader) { this.proxyAddressForwarding = proxyAddressForwarding; this.allowForwarded = allowForwarded; + this.allowXForwarded = allowXForwarded; this.enableForwardedHost = enableForwardedHost; this.enableForwardedPrefix = enableForwardedPrefix; this.forwardedHostHeader = forwardedHostHeader; @@ -25,17 +28,16 @@ public ForwardingProxyOptions(final boolean proxyAddressForwarding, } public static ForwardingProxyOptions from(HttpConfiguration httpConfiguration) { - final boolean proxyAddressForwarding = httpConfiguration.proxyAddressForwarding - .orElse(httpConfiguration.proxy.proxyAddressForwarding); - final boolean allowForwarded = httpConfiguration.allowForwarded - .orElse(httpConfiguration.proxy.allowForwarded); + final boolean proxyAddressForwarding = httpConfiguration.proxy.proxyAddressForwarding; + final boolean allowForwarded = httpConfiguration.proxy.allowForwarded; + final boolean allowXForwarded = httpConfiguration.proxy.allowXForwarded.orElse(!allowForwarded); final boolean enableForwardedHost = httpConfiguration.proxy.enableForwardedHost; final boolean enableForwardedPrefix = httpConfiguration.proxy.enableForwardedPrefix; final AsciiString forwardedPrefixHeader = AsciiString.cached(httpConfiguration.proxy.forwardedPrefixHeader); final AsciiString forwardedHostHeader = AsciiString.cached(httpConfiguration.proxy.forwardedHostHeader); - return new ForwardingProxyOptions(proxyAddressForwarding, allowForwarded, enableForwardedHost, forwardedHostHeader, - enableForwardedPrefix, forwardedPrefixHeader); + return new ForwardingProxyOptions(proxyAddressForwarding, allowForwarded, allowXForwarded, enableForwardedHost, + forwardedHostHeader, enableForwardedPrefix, forwardedPrefixHeader); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java index dbae58949e924..220204a6fe115 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java @@ -1,12 +1,17 @@ package io.quarkus.vertx.http.runtime; import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.quarkus.runtime.annotations.ConvertWith; import io.quarkus.runtime.configuration.NormalizeRootHttpPathConverter; +import io.quarkus.vertx.http.Compressed; +import io.quarkus.vertx.http.Uncompressed; import io.vertx.core.http.ClientAuth; @ConfigRoot(name = "http", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) @@ -57,4 +62,39 @@ public class HttpBuildTimeConfig { */ @ConfigItem(defaultValue = "30s") public Duration testTimeout; + + /** + * If responses should be compressed. + * + * Note that this will attempt to compress all responses, to avoid compressing + * already compressed content (such as images) you need to set the following header: + * + * Content-Encoding: identity + * + * Which will tell vert.x not to compress the response. + */ + @ConfigItem + public boolean enableCompression; + + /** + * When enabled, vert.x will decompress the request's body if it's compressed. + * + * Note that the compression format (e.g., gzip) must be specified in the Content-Encoding header + * in the request. + */ + @ConfigItem + public boolean enableDecompression; + + /** + * List of media types for which the compression should be enabled automatically, unless declared explicitly via + * {@link Compressed} or {@link Uncompressed}. + */ + @ConfigItem(defaultValue = "text/html,text/plain,text/xml,text/css,text/javascript,application/javascript") + public Optional> compressMediaTypes; + + /** + * The compression level used when compression support is enabled. + */ + @ConfigItem + public OptionalInt compressionLevel; } 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 1ce4d25e4f193..390a998fba1b1 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 @@ -1,7 +1,6 @@ package io.quarkus.vertx.http.runtime; import java.time.Duration; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalInt; @@ -10,8 +9,6 @@ import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; -import io.quarkus.vertx.http.Compressed; -import io.quarkus.vertx.http.Uncompressed; import io.quarkus.vertx.http.runtime.cors.CORSConfig; @ConfigRoot(phase = ConfigPhase.RUN_TIME) @@ -65,26 +62,6 @@ public class HttpConfiguration { @ConfigItem(defaultValue = "8444") public int testSslPort; - /** - * If this is true then the address, scheme etc will be set from headers forwarded by the proxy server, such as - * {@code X-Forwarded-For}. This should only be set if you are behind a proxy that sets these headers. - * - * @deprecated use quarkus.http.proxy.proxy-address-forwarding instead. - */ - @Deprecated - @ConfigItem - public Optional proxyAddressForwarding; - - /** - * If this is true and proxy address forwarding is enabled then the standard {@code Forwarded} header will be used, - * rather than the more common but not standard {@code X-Forwarded-For}. - * - * @deprecated use quarkus.http.proxy.allow-forwarded instead. - */ - @Deprecated - @ConfigItem - public Optional allowForwarded; - /** * If insecure (i.e. http rather than https) requests are allowed. If this is {@code enabled} * then http works as normal. {@code redirect} will still open the http port, but @@ -187,7 +164,7 @@ public class HttpConfiguration { /** * The accept backlog, this is how many connections can be waiting to be accepted before connections start being rejected */ - @ConfigItem + @ConfigItem(defaultValue = "-1") public int acceptBacklog; /** @@ -218,41 +195,6 @@ public class HttpConfiguration { @ConfigItem public Map sameSiteCookie; - /** - * If responses should be compressed. - * - * Note that this will attempt to compress all responses, to avoid compressing - * already compressed content (such as images) you need to set the following header: - * - * Content-Encoding: identity - * - * Which will tell vert.x not to compress the response. - */ - @ConfigItem - public boolean enableCompression; - - /** - * When enabled, vert.x will decompress the request's body if it's compressed. - * - * Note that the compression format (e.g., gzip) must be specified in the Content-Encoding header - * in the request. - */ - @ConfigItem - public boolean enableDecompression; - - /** - * List of media types for which the compression should be enabled automatically, unless declared explicitly via - * {@link Compressed} or {@link Uncompressed}. - */ - @ConfigItem(defaultValue = "text/html,text/plain,text/xml,text/css,text/javascript,application/javascript") - public Optional> compressMediaTypes; - - /** - * The compression level used when compression support is enabled. - */ - @ConfigItem - public OptionalInt compressionLevel; - /** * Provides a hint (optional) for the default content type of responses generated for * the errors not handled by the application. @@ -273,6 +215,12 @@ public class HttpConfiguration { @ConfigItem public Map header; + /** + * Additional HTTP configuration per path + */ + @ConfigItem + public Map filter; + public ProxyConfig proxy; public int determinePort(LaunchMode launchMode) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ProxyConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ProxyConfig.java index 2d255f93e5ab5..0d2cd57addf0e 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ProxyConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ProxyConfig.java @@ -1,5 +1,7 @@ package io.quarkus.vertx.http.runtime; +import java.util.Optional; + import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -16,12 +18,28 @@ public class ProxyConfig { public boolean proxyAddressForwarding; /** - * If this is true and proxy address forwarding is enabled then the standard {@code Forwarded} header will be used, - * rather than the more common but not standard {@code X-Forwarded-For}. + * If this is true and proxy address forwarding is enabled then the standard {@code Forwarded} header will be used. + * In case the not standard {@code X-Forwarded-For} header is enabled and detected on HTTP requests, the standard header has + * the precedence. + * Activating this together with {@code quarkus.http.proxy.allow-x-forwarded} has security implications as clients can forge + * requests with a forwarded header that is not overwritten by the proxy. Therefore proxies should strip unexpected + * `X-Forwarded` or `X-Forwarded-*` headers from the client. */ @ConfigItem public boolean allowForwarded; + /** + * If either this or {@code allow-forwarded} are true and proxy address forwarding is enabled then the not standard + * {@code Forwarded} header will be used. + * In case the standard {@code Forwarded} header is enabled and detected on HTTP requests, the standard header has the + * precedence. + * Activating this together with {@code quarkus.http.proxy.allow-x-forwarded} has security implications as clients can forge + * requests with a forwarded header that is not overwritten by the proxy. Therefore proxies should strip unexpected + * `X-Forwarded` or `X-Forwarded-*` headers from the client. + */ + @ConfigItem + public Optional allowXForwarded; + /** * Enable override the received request's host through a forwarded host header. */ 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 851b552945ae8..f3f5f0ab4a692 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 @@ -23,9 +23,12 @@ public class StaticResourcesRecorder { private static volatile List hotDeploymentResourcePaths; final RuntimeValue httpConfiguration; + final HttpBuildTimeConfig httpBuildTimeConfig; - public StaticResourcesRecorder(RuntimeValue httpConfiguration) { + public StaticResourcesRecorder(RuntimeValue httpConfiguration, + HttpBuildTimeConfig httpBuildTimeConfig) { this.httpConfiguration = httpConfiguration; + this.httpBuildTimeConfig = httpBuildTimeConfig; } public static void setHotDeploymentResources(List resources) { @@ -70,7 +73,7 @@ public void handle(RoutingContext ctx) { ctx.mountPoint().endsWith("/") ? ctx.mountPoint().length() - 1 : ctx.mountPoint().length()); if (knownPaths.contains(rel)) { staticHandler.handle(ctx); - if (httpConfiguration.getValue().enableCompression && isCompressed(rel)) { + if (httpBuildTimeConfig.enableCompression && isCompressed(rel)) { // Remove the "Content-Encoding: identity" header and enable compression ctx.response().headers().remove(HttpHeaders.CONTENT_ENCODING); } @@ -103,7 +106,7 @@ private boolean isCompressed(String path) { suffix = null; } String contentType = MimeMapping.getMimeTypeForExtension(suffix); - return httpConfiguration.getValue().compressMediaTypes.orElse(List.of()).contains(contentType); + return httpBuildTimeConfig.compressMediaTypes.orElse(List.of()).contains(contentType); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index 8a043ce9bc046..c0a0dd6b8d8e2 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -37,6 +37,7 @@ import javax.enterprise.event.Event; +import org.crac.Resource; import org.jboss.logging.Logger; import org.wildfly.common.cpu.ProcessorInfo; @@ -141,15 +142,22 @@ public class VertxHttpRecorder { public static final String GET = "GET"; private static final Handler ACTUAL_ROOT = new Handler() { + + /** JVM system property that disables URI validation, don't use this in production. */ + private static final String DISABLE_URI_VALIDATION_PROP_NAME = "vertx.disableURIValidation"; + + /** + * Disables HTTP headers validation, so we can save some processing and save some allocations. + */ + private final boolean DISABLE_URI_VALIDATION = Boolean.getBoolean(DISABLE_URI_VALIDATION_PROP_NAME); + @Override public void handle(HttpServerRequest httpServerRequest) { - try { - // we simply need to know if the URI is valid - new URI(httpServerRequest.uri()); - } catch (URISyntaxException e) { + if (!uriValid(httpServerRequest)) { httpServerRequest.response().setStatusCode(400).end(); return; } + //we need to pause the request to make sure that data does //not arrive before handlers have a chance to install a read handler //as it is possible filters such as the auth filter can do blocking tasks @@ -165,6 +173,19 @@ public void handle(HttpServerRequest httpServerRequest) { httpServerRequest.response().setStatusCode(503).end(); } } + + private boolean uriValid(HttpServerRequest httpServerRequest) { + if (DISABLE_URI_VALIDATION) { + return true; + } + try { + // we simply need to know if the URI is valid + new URI(httpServerRequest.uri()); + return true; + } catch (URISyntaxException e) { + return false; + } + } }; final HttpBuildTimeConfig httpBuildTimeConfig; final RuntimeValue httpConfiguration; @@ -337,7 +358,7 @@ public void finalizeRouter(BeanContainer container, Consumer defaultRoute defaultRouteHandler.accept(httpRouteRouter.route().order(DEFAULT_ROUTE_ORDER)); } - if (httpConfiguration.enableCompression) { + if (httpBuildTimeConfig.enableCompression) { httpRouteRouter.route().order(0).handler(new Handler() { @Override public void handle(RoutingContext ctx) { @@ -393,6 +414,40 @@ public void handle(Void e) { } }); } + // Filter Configuration per path + var filtersInConfig = httpConfiguration.filter; + if (!filtersInConfig.isEmpty()) { + for (var entry : filtersInConfig.entrySet()) { + var filterConfig = entry.getValue(); + var matches = filterConfig.matches; + var order = filterConfig.order.orElse(Integer.MIN_VALUE); + var methods = filterConfig.methods; + var headers = filterConfig.header; + if (methods.isEmpty()) { + httpRouteRouter.routeWithRegex(matches) + .order(order) + .handler(new Handler() { + @Override + public void handle(RoutingContext event) { + event.response().headers().setAll(headers); + event.next(); + } + }); + } else { + for (var method : methods.get()) { + httpRouteRouter.routeWithRegex(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)), matches) + .order(order) + .handler(new Handler() { + @Override + public void handle(RoutingContext event) { + event.response().headers().setAll(headers); + event.next(); + } + }); + } + } + } + } // Headers sent on any request, regardless of the response Map headers = httpConfiguration.header; if (!headers.isEmpty()) { @@ -406,7 +461,7 @@ public void handle(Void e) { .handler(new Handler() { @Override public void handle(RoutingContext event) { - event.response().headers().add(name, config.value); + event.response().headers().set(name, config.value); event.next(); } }); @@ -457,7 +512,7 @@ public void handle(RoutingContext event) { root = mainRouter; } - warnIfDeprecatedHttpConfigPropertiesPresent(httpConfiguration); + warnIfProxyAddressForwardingAllowedWithMultipleHeaders(httpConfiguration); ForwardingProxyOptions forwardingProxyOptions = ForwardingProxyOptions.from(httpConfiguration); if (forwardingProxyOptions.proxyAddressForwarding) { Handler delegate = root; @@ -547,17 +602,18 @@ public void handle(RoutingContext event) { rootHandler = root; } - private void warnIfDeprecatedHttpConfigPropertiesPresent(HttpConfiguration httpConfiguration) { - if (httpConfiguration.proxyAddressForwarding.isPresent()) { - LOGGER.warn( - "`quarkus.http.proxy-address-forwarding` is deprecated and will be removed in a future version - it is " - + "recommended to switch to `quarkus.http.proxy.proxy-address-forwarding`"); - } + private void warnIfProxyAddressForwardingAllowedWithMultipleHeaders(HttpConfiguration httpConfiguration) { + ProxyConfig proxyConfig = httpConfiguration.proxy; + boolean proxyAddressForwardingActivated = proxyConfig.proxyAddressForwarding; + boolean forwardedActivated = proxyConfig.allowForwarded; + boolean xForwardedActivated = httpConfiguration.proxy.allowXForwarded.orElse(!forwardedActivated); - if (httpConfiguration.allowForwarded.isPresent()) { + if (proxyAddressForwardingActivated && forwardedActivated && xForwardedActivated) { LOGGER.warn( - "`quarkus.http.allow-forwarded` is deprecated and will be removed in a future version - it is " - + "recommended to switch to `quarkus.http.proxy.allow-forwarded`"); + "The X-Forwarded-* and Forwarded headers will be considered when determining the proxy address. " + + "This configuration can cause a security issue as clients can forge requests and send a " + + "forwarded header that is not overwritten by the proxy. " + + "Please consider use one of these headers just to forward the proxy address in requests."); } } @@ -565,8 +621,10 @@ private static void doServerStart(Vertx vertx, HttpBuildTimeConfig httpBuildTime HttpConfiguration httpConfiguration, LaunchMode launchMode, Supplier eventLoops, List websocketSubProtocols, boolean auxiliaryApplication) throws IOException { // Http server configuration - HttpServerOptions httpServerOptions = createHttpServerOptions(httpConfiguration, launchMode, websocketSubProtocols); - HttpServerOptions domainSocketOptions = createDomainSocketOptions(httpConfiguration, websocketSubProtocols); + HttpServerOptions httpServerOptions = createHttpServerOptions(httpBuildTimeConfig, httpConfiguration, launchMode, + websocketSubProtocols); + HttpServerOptions domainSocketOptions = createDomainSocketOptions(httpBuildTimeConfig, httpConfiguration, + websocketSubProtocols); HttpServerOptions sslConfig = createSslOptions(httpBuildTimeConfig, httpConfiguration, launchMode, websocketSubProtocols); @@ -774,12 +832,14 @@ private static HttpServerOptions createSslOptions(HttpBuildTimeConfig buildTimeC serverOptions.setPort(sslPort == 0 ? -2 : sslPort); serverOptions.setClientAuth(buildTimeConfig.tlsClientAuth); - applyCommonOptions(serverOptions, httpConfiguration, websocketSubProtocols); + applyCommonOptions(serverOptions, buildTimeConfig, httpConfiguration, websocketSubProtocols); return serverOptions; } - private static void applyCommonOptions(HttpServerOptions httpServerOptions, HttpConfiguration httpConfiguration, + private static void applyCommonOptions(HttpServerOptions httpServerOptions, + HttpBuildTimeConfig buildTimeConfig, + HttpConfiguration httpConfiguration, List websocketSubProtocols) { httpServerOptions.setHost(httpConfiguration.host); setIdleTimeout(httpConfiguration, httpServerOptions); @@ -792,11 +852,11 @@ private static void applyCommonOptions(HttpServerOptions httpServerOptions, Http httpServerOptions.setTcpCork(httpConfiguration.tcpCork); httpServerOptions.setAcceptBacklog(httpConfiguration.acceptBacklog); httpServerOptions.setTcpFastOpen(httpConfiguration.tcpFastOpen); - httpServerOptions.setCompressionSupported(httpConfiguration.enableCompression); - if (httpConfiguration.compressionLevel.isPresent()) { - httpServerOptions.setCompressionLevel(httpConfiguration.compressionLevel.getAsInt()); + httpServerOptions.setCompressionSupported(buildTimeConfig.enableCompression); + if (buildTimeConfig.compressionLevel.isPresent()) { + httpServerOptions.setCompressionLevel(buildTimeConfig.compressionLevel.getAsInt()); } - httpServerOptions.setDecompressionSupported(httpConfiguration.enableDecompression); + httpServerOptions.setDecompressionSupported(buildTimeConfig.enableDecompression); httpServerOptions.setMaxInitialLineLength(httpConfiguration.limits.maxInitialLineLength); } @@ -883,7 +943,8 @@ private static byte[] doRead(InputStream is) throws IOException { return out.toByteArray(); } - private static HttpServerOptions createHttpServerOptions(HttpConfiguration httpConfiguration, + private static HttpServerOptions createHttpServerOptions( + HttpBuildTimeConfig buildTimeConfig, HttpConfiguration httpConfiguration, LaunchMode launchMode, List websocketSubProtocols) { if (!httpConfiguration.hostEnabled) { return null; @@ -893,19 +954,20 @@ private static HttpServerOptions createHttpServerOptions(HttpConfiguration httpC int port = httpConfiguration.determinePort(launchMode); options.setPort(port == 0 ? -1 : port); - applyCommonOptions(options, httpConfiguration, websocketSubProtocols); + applyCommonOptions(options, buildTimeConfig, httpConfiguration, websocketSubProtocols); return options; } - private static HttpServerOptions createDomainSocketOptions(HttpConfiguration httpConfiguration, + private static HttpServerOptions createDomainSocketOptions( + HttpBuildTimeConfig buildTimeConfig, HttpConfiguration httpConfiguration, List websocketSubProtocols) { if (!httpConfiguration.domainSocketEnabled) { return null; } HttpServerOptions options = new HttpServerOptions(); - applyCommonOptions(options, httpConfiguration, websocketSubProtocols); + applyCommonOptions(options, buildTimeConfig, httpConfiguration, websocketSubProtocols); // Override the host (0.0.0.0 by default) with the configured domain socket. options.setHost(httpConfiguration.domainSocket); @@ -962,7 +1024,7 @@ public GracefulShutdownFilter createGracefulShutdownHandler() { return new GracefulShutdownFilter(); } - private static class WebDeploymentVerticle extends AbstractVerticle { + private static class WebDeploymentVerticle extends AbstractVerticle implements Resource { private HttpServer httpServer; private HttpServer httpsServer; @@ -988,6 +1050,7 @@ public WebDeploymentVerticle(HttpServerOptions httpOptions, HttpServerOptions ht this.insecureRequests = insecureRequests; this.quarkusConfig = quarkusConfig; this.connectionCount = connectionCount; + org.crac.Core.getGlobalContext().register(this); } @Override @@ -1097,60 +1160,63 @@ public void handle(Void event) { } }); } - httpServer.listen(options.getPort(), options.getHost(), event -> { - if (event.cause() != null) { - startFuture.fail(event.cause()); - } else { - // Port may be random, so set the actual port - int actualPort = event.result().actualPort(); - - if (https) { - actualHttpsPort = actualPort; + httpServer.listen(options.getPort(), options.getHost(), new Handler<>() { + @Override + public void handle(AsyncResult event) { + if (event.cause() != null) { + startFuture.fail(event.cause()); } else { - actualHttpPort = actualPort; - } - if (remainingCount.decrementAndGet() == 0) { - //make sure we only set the properties once - if (actualPort != options.getPort()) { - // Override quarkus.http(s)?.(test-)?port - String schema; - if (https) { - clearHttpsProperty = true; - schema = "https"; - } else { - clearHttpProperty = true; - actualHttpPort = actualPort; - schema = "http"; - } - portPropertiesToRestore = new HashMap<>(); - String portPropertyValue = String.valueOf(actualPort); - //we always set the .port property, even if we are in test mode, so this will always - //reflect the current port - String portPropertyName = "quarkus." + schema + ".port"; - String prevPortPropertyValue = System.setProperty(portPropertyName, portPropertyValue); - if (!Objects.equals(prevPortPropertyValue, portPropertyValue)) { - portPropertiesToRestore.put(portPropertyName, prevPortPropertyValue); - } - if (launchMode == LaunchMode.TEST) { - //we also set the test-port property in a test - String testPropName = "quarkus." + schema + ".test-port"; - String prevTestPropPrevValue = System.setProperty(testPropName, portPropertyValue); - if (!Objects.equals(prevTestPropPrevValue, portPropertyValue)) { - portPropertiesToRestore.put(testPropName, prevTestPropPrevValue); + // Port may be random, so set the actual port + int actualPort = event.result().actualPort(); + + if (https) { + actualHttpsPort = actualPort; + } else { + actualHttpPort = actualPort; + } + if (remainingCount.decrementAndGet() == 0) { + //make sure we only set the properties once + if (actualPort != options.getPort()) { + // Override quarkus.http(s)?.(test-)?port + String schema; + if (https) { + clearHttpsProperty = true; + schema = "https"; + } else { + clearHttpProperty = true; + actualHttpPort = actualPort; + schema = "http"; } - } - if (launchMode.isDevOrTest()) { - // set the profile property as well to make sure we don't have any inconsistencies - portPropertyName = propertyWithProfilePrefix(portPropertyName); - prevPortPropertyValue = System.setProperty(portPropertyName, portPropertyValue); + portPropertiesToRestore = new HashMap<>(); + String portPropertyValue = String.valueOf(actualPort); + //we always set the .port property, even if we are in test mode, so this will always + //reflect the current port + String portPropertyName = "quarkus." + schema + ".port"; + String prevPortPropertyValue = System.setProperty(portPropertyName, portPropertyValue); if (!Objects.equals(prevPortPropertyValue, portPropertyValue)) { portPropertiesToRestore.put(portPropertyName, prevPortPropertyValue); } + if (launchMode == LaunchMode.TEST) { + //we also set the test-port property in a test + String testPropName = "quarkus." + schema + ".test-port"; + String prevTestPropPrevValue = System.setProperty(testPropName, portPropertyValue); + if (!Objects.equals(prevTestPropPrevValue, portPropertyValue)) { + portPropertiesToRestore.put(testPropName, prevTestPropPrevValue); + } + } + if (launchMode.isDevOrTest()) { + // set the profile property as well to make sure we don't have any inconsistencies + portPropertyName = propertyWithProfilePrefix(portPropertyName); + prevPortPropertyValue = System.setProperty(portPropertyName, portPropertyValue); + if (!Objects.equals(prevPortPropertyValue, portPropertyValue)) { + portPropertiesToRestore.put(portPropertyName, prevPortPropertyValue); + } + } } + startFuture.complete(null); } - startFuture.complete(null); - } + } } }); } @@ -1217,6 +1283,24 @@ public void stop(Promise stopFuture) { private String propertyWithProfilePrefix(String portPropertyName) { return "%" + launchMode.getDefaultProfile() + "." + portPropertyName; } + + @Override + public void beforeCheckpoint(org.crac.Context context) throws Exception { + Promise p = Promise.promise(); + stop(p); + CountDownLatch latch = new CountDownLatch(1); + p.future().onComplete(event -> latch.countDown()); + latch.await(); + } + + @Override + public void afterRestore(org.crac.Context context) throws Exception { + Promise p = Promise.promise(); + start(p); + CountDownLatch latch = new CountDownLatch(1); + p.future().onComplete(event -> latch.countDown()); + latch.await(); + } } protected static ServerBootstrap virtualBootstrap; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/DevConsoleFilter.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/DevConsoleFilter.java index a565c2d1f7d7c..a16ab6330f01f 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/DevConsoleFilter.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/DevConsoleFilter.java @@ -34,13 +34,14 @@ public void handle(RoutingContext event) { for (Map.Entry entry : event.request().headers()) { headers.put(entry.getKey(), event.request().headers().getAll(entry.getKey())); } - event.request().resume(); if (event.getBody() != null) { + event.request().resume(); DevConsoleRequest request = new DevConsoleRequest(event.request().method().name(), event.request().uri(), headers, event.getBody().getBytes()); setupFuture(event, request.getResponse()); DevConsoleManager.sentRequest(request); } else if (event.request().isEnded()) { + event.request().resume(); DevConsoleRequest request = new DevConsoleRequest(event.request().method().name(), event.request().uri(), headers, new byte[0]); setupFuture(event, request.getResponse()); @@ -55,6 +56,7 @@ public void handle(Buffer body) { DevConsoleManager.sentRequest(request); } }); + event.request().resume(); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/logstream/WebSocketLogHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/logstream/WebSocketLogHandler.java index 21103a4bbf289..1e368c8a005b0 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/logstream/WebSocketLogHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/logstream/WebSocketLogHandler.java @@ -68,6 +68,7 @@ private void recordHistory(final ExtLogRecord record) { history.add(record); } catch (InterruptedException ex) { ex.printStackTrace(); + Thread.currentThread().interrupt(); } } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java index a2be2222f63f8..aad139cde70b2 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java @@ -99,9 +99,8 @@ void init(HttpBuildTimeConfig config, Map> if (entry.getValue().enabled.orElse(Boolean.TRUE)) { for (String path : entry.getValue().paths.orElse(Collections.emptyList())) { path = path.trim(); - if (!config.rootPath.equals("/")) { - path = (config.rootPath.endsWith("/") ? config.rootPath.substring(0, config.rootPath.length() - 1) - : config.rootPath) + path; + if (!path.startsWith("/")) { + path = config.rootPath + path; } if (tempMap.containsKey(path)) { HttpMatcher m = new HttpMatcher(entry.getValue().authMechanism.orElse(null), diff --git a/extensions/vertx/deployment/pom.xml b/extensions/vertx/deployment/pom.xml index 2a339d26a4230..8e926aeee9ed4 100644 --- a/extensions/vertx/deployment/pom.xml +++ b/extensions/vertx/deployment/pom.xml @@ -49,6 +49,11 @@ rest-assured test + + io.vertx + vertx-web-client + test + diff --git a/extensions/vertx/deployment/src/main/java/io/quarkus/vertx/core/deployment/VertxCoreProcessor.java b/extensions/vertx/deployment/src/main/java/io/quarkus/vertx/core/deployment/VertxCoreProcessor.java index 647e31f239b94..e8153f354884a 100644 --- a/extensions/vertx/deployment/src/main/java/io/quarkus/vertx/core/deployment/VertxCoreProcessor.java +++ b/extensions/vertx/deployment/src/main/java/io/quarkus/vertx/core/deployment/VertxCoreProcessor.java @@ -28,6 +28,7 @@ import org.objectweb.asm.Type; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.bootstrap.logging.LateBoundMDCProvider; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; @@ -44,6 +45,7 @@ import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.ThreadFactoryBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; import io.quarkus.gizmo.Gizmo; @@ -67,8 +69,11 @@ class VertxCoreProcessor { ); @BuildStep - NativeImageConfigBuildItem build(BuildProducer reflectiveClass) { + NativeImageConfigBuildItem build(BuildProducer reflectiveClass, + BuildProducer nativeImageResources) { reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, VertxLogDelegateFactory.class.getName())); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, LateBoundMDCProvider.class.getName())); + nativeImageResources.produce(new NativeImageResourceBuildItem("META-INF/services/org.jboss.logmanager.MDCProvider")); return NativeImageConfigBuildItem.builder() .addRuntimeInitializedClass("io.vertx.core.buffer.impl.VertxByteBufAllocator") .addRuntimeInitializedClass("io.vertx.core.buffer.impl.PartialPooledByteBufAllocator") diff --git a/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/mdc/InMemoryLogHandler.java b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/mdc/InMemoryLogHandler.java new file mode 100644 index 0000000000000..5ee148fe88c72 --- /dev/null +++ b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/mdc/InMemoryLogHandler.java @@ -0,0 +1,55 @@ +package io.quarkus.vertx.mdc; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.ErrorManager; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +import org.jboss.logmanager.formatters.PatternFormatter; + +public class InMemoryLogHandler extends Handler { + private static final PatternFormatter FORMATTER = new PatternFormatter("%X{requestId} ### %s"); + + private final List recordList = new CopyOnWriteArrayList<>(); + + public List logRecords() { + return Collections.unmodifiableList(recordList); + } + + @Override + public void publish(LogRecord record) { + String loggerName = record.getLoggerName(); + if (loggerName != null && (loggerName.endsWith("Verticle") || loggerName.endsWith("MDCTest"))) { + final String formatted; + final Formatter formatter = getFormatter(); + try { + formatted = formatter.format(record); + } catch (Exception ex) { + reportError("Formatting error", ex, ErrorManager.FORMAT_FAILURE); + return; + } + if (formatted.length() == 0) { + return; + } + recordList.add(formatted); + } + } + + @Override + public Formatter getFormatter() { + return FORMATTER; + } + + @Override + public void flush() { + + } + + @Override + public void close() throws SecurityException { + + } +} diff --git a/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/mdc/InMemoryLogHandlerProducer.java b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/mdc/InMemoryLogHandlerProducer.java new file mode 100644 index 0000000000000..84f25fe074b20 --- /dev/null +++ b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/mdc/InMemoryLogHandlerProducer.java @@ -0,0 +1,28 @@ +package io.quarkus.vertx.mdc; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.Produces; +import javax.inject.Singleton; + +import io.quarkus.bootstrap.logging.InitialConfigurator; +import io.quarkus.runtime.ShutdownEvent; +import io.quarkus.runtime.StartupEvent; + +@ApplicationScoped +public class InMemoryLogHandlerProducer { + + @Produces + @Singleton + public InMemoryLogHandler inMemoryLogHandler() { + return new InMemoryLogHandler(); + } + + void onStart(@Observes StartupEvent ev, InMemoryLogHandler inMemoryLogHandler) { + InitialConfigurator.DELAYED_HANDLER.addHandler(inMemoryLogHandler); + } + + void onStop(@Observes ShutdownEvent ev, InMemoryLogHandler inMemoryLogHandler) { + InitialConfigurator.DELAYED_HANDLER.removeHandler(inMemoryLogHandler); + } +} diff --git a/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/mdc/VerticleDeployer.java b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/mdc/VerticleDeployer.java new file mode 100644 index 0000000000000..0127b0871c3bb --- /dev/null +++ b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/mdc/VerticleDeployer.java @@ -0,0 +1,74 @@ +package io.quarkus.vertx.mdc; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.inject.Inject; + +import org.jboss.logging.Logger; +import org.jboss.logging.MDC; + +import io.quarkus.runtime.ShutdownEvent; +import io.quarkus.runtime.StartupEvent; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Promise; +import io.vertx.core.http.HttpServer; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.client.HttpRequest; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.codec.BodyCodec; +import io.vertx.mutiny.core.Vertx; + +@ApplicationScoped +public class VerticleDeployer { + public static final String REQUEST_ID_HEADER = "x-request-id"; + public static final int VERTICLE_PORT = 8097; + + @Inject + Vertx vertx; + + private volatile String deploymentId; + + void onStart(@Observes StartupEvent ev) { + deploymentId = vertx.deployVerticle(new TestVerticle()).await().indefinitely(); + } + + void onStop(@Observes ShutdownEvent ev) { + if (deploymentId != null) { + vertx.undeploy(deploymentId).await().indefinitely(); + } + } + + private static class TestVerticle extends AbstractVerticle { + private static final Logger LOGGER = Logger.getLogger(TestVerticle.class); + + private HttpRequest request; + + @Override + public void start(Promise startPromise) { + WebClient webClient = WebClient.create(vertx); + request = webClient.getAbs("http://worldclockapi.com/api/json/utc/now").as(BodyCodec.jsonObject()); + + Promise httpServerPromise = Promise.promise(); + httpServerPromise.future(). mapEmpty().onComplete(startPromise); + vertx.createHttpServer() + .requestHandler(req -> { + String requestId = req.getHeader(REQUEST_ID_HEADER); + + MDC.put("requestId", requestId); + LOGGER.info("Received HTTP request ### " + requestId); + + vertx.setTimer(50, l -> { + LOGGER.info("Timer fired ### " + requestId); + vertx.executeBlocking(fut -> { + LOGGER.info("Blocking task executed ### " + requestId); + fut.complete(); + }, false, bar -> request.send(rar -> { + LOGGER.info("Received Web Client response ### " + requestId); + req.response().end(); + })); + }); + }) + .listen(VERTICLE_PORT, httpServerPromise); + } + } +} diff --git a/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/mdc/VertxMDCTest.java b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/mdc/VertxMDCTest.java new file mode 100644 index 0000000000000..e7149236b0510 --- /dev/null +++ b/extensions/vertx/deployment/src/test/java/io/quarkus/vertx/mdc/VertxMDCTest.java @@ -0,0 +1,162 @@ +package io.quarkus.vertx.mdc; + +import static io.quarkus.vertx.mdc.VerticleDeployer.REQUEST_ID_HEADER; +import static io.quarkus.vertx.mdc.VerticleDeployer.VERTICLE_PORT; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.toList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import javax.inject.Inject; + +import org.jboss.logging.Logger; +import org.jboss.logging.MDC; +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.vertx.core.AsyncResult; +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.client.HttpRequest; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; +import io.vertx.ext.web.client.predicate.ResponsePredicate; + +/* +This test was mostly based on https://github.com/reactiverse/reactiverse-contextual-logging/blob/39e691d3a8fd78d19ee120cab8d8b38a4ef67813/src/test/java/io/reactiverse/contextual/logging/ContextualLoggingIT.java + */ + +public class VertxMDCTest { + private static final Logger LOGGER = Logger.getLogger(VertxMDCTest.class); + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addClass(VerticleDeployer.class) + .addClass(InMemoryLogHandler.class) + .addClass(InMemoryLogHandlerProducer.class)) + .overrideConfigKey("quarkus.log.console.format", "%d{HH:mm:ss} %-5p requestId=%X{requestId} [%c{2.}] (%t) %s%e%n"); + + @Inject + Vertx vertx; + + @Inject + InMemoryLogHandler inMemoryLogHandler; + + @Inject + VerticleDeployer verticleDeployer; + + static final CountDownLatch countDownLatch = new CountDownLatch(1); + static final AtomicReference errorDuringExecution = new AtomicReference<>(); + + @Test + void mdc() throws Throwable { + List requestIds = IntStream.range(0, 10) + .mapToObj(i -> UUID.randomUUID().toString()) + .collect(toList()); + + sendRequests(requestIds, onSuccess(v -> { + try { + Map> allMessagesById = inMemoryLogHandler.logRecords() + .stream() + .map(line -> line.split(" ### ")) + .peek(split -> assertEquals(split[0], split[2])) + .collect(groupingBy(split -> split[0], + mapping(split -> split[1], toList()))); + + assertEquals(requestIds.size(), allMessagesById.size()); + assertTrue(requestIds.containsAll(allMessagesById.keySet())); + + List expected = Stream. builder() + .add("Received HTTP request") + .add("Timer fired") + .add("Blocking task executed") + .add("Received Web Client response") + .build() + .collect(toList()); + + for (List messages : allMessagesById.values()) { + assertEquals(expected, messages); + } + } catch (Throwable t) { + errorDuringExecution.set(t); + } finally { + countDownLatch.countDown(); + } + })); + + countDownLatch.await(); + + Throwable throwable = errorDuringExecution.get(); + if (throwable != null) { + throw throwable; + } + } + + @Test + public void mdcNonVertxThreadTest() { + String mdcValue = "Test MDC value"; + MDC.put("requestId", mdcValue); + LOGGER.info("Test 1"); + + assertThat(inMemoryLogHandler.logRecords(), + hasItem(mdcValue + " ### Test 1")); + + MDC.remove("requestId"); + LOGGER.info("Test 2"); + + assertThat(inMemoryLogHandler.logRecords(), + hasItem(" ### Test 2")); + + mdcValue = "New test MDC value"; + MDC.put("requestId", mdcValue); + LOGGER.info("Test 3"); + + assertThat(inMemoryLogHandler.logRecords(), + hasItem(mdcValue + " ### Test 3")); + } + + protected Handler> onSuccess(Consumer consumer) { + return result -> { + if (result.failed()) { + errorDuringExecution.set(result.cause()); + countDownLatch.countDown(); + } else { + consumer.accept(result.result()); + } + }; + } + + @SuppressWarnings({ "rawtypes" }) + private void sendRequests(List ids, Handler> handler) { + WebClient webClient = WebClient.create(vertx, new WebClientOptions().setDefaultPort(VERTICLE_PORT)); + + HttpRequest request = webClient.get("/") + .expect(ResponsePredicate.SC_OK); + + List futures = ids.stream() + .map(id -> request.putHeader(REQUEST_ID_HEADER, id).send()) + .collect(toList()); + + CompositeFuture.all(futures). mapEmpty().onComplete(handler); + } +} diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/VertxCoreRecorder.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/VertxCoreRecorder.java index bbba0cd17f0eb..92f005e9f0b0f 100644 --- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/VertxCoreRecorder.java +++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/VertxCoreRecorder.java @@ -32,6 +32,7 @@ import io.netty.channel.EventLoopGroup; import io.netty.util.concurrent.FastThreadLocal; +import io.quarkus.bootstrap.logging.LateBoundMDCProvider; import io.quarkus.runtime.IOThreadDetector; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.ShutdownContext; @@ -242,6 +243,9 @@ public void handle(Throwable error) { LOGGER.error("Uncaught exception received by Vert.x", error); } }); + + LateBoundMDCProvider.setMDCProviderDelegate(VertxMDC.INSTANCE); + return logVertxInitialization(vertx); } @@ -421,7 +425,9 @@ private static void initializeClusterOptions(VertxConfiguration conf, VertxOptio if (cluster.port.isPresent()) { options.getEventBusOptions().setPort(cluster.port.getAsInt()); } - cluster.publicHost.ifPresent(options.getEventBusOptions()::setClusterPublicHost); + if (cluster.publicHost.isPresent()) { + options.getEventBusOptions().setClusterPublicHost(cluster.publicHost.get()); + } if (cluster.publicPort.isPresent()) { options.getEventBusOptions().setPort(cluster.publicPort.getAsInt()); } @@ -510,11 +516,14 @@ public Integer get() { public ThreadFactory createThreadFactory(LaunchMode launchMode) { Optional nonDevModeTccl = setupThreadFactoryTccl(launchMode); AtomicInteger threadCount = new AtomicInteger(0); - return runnable -> { - VertxThread thread = createVertxThread(runnable, - "executor-thread-" + threadCount.getAndIncrement(), true, 0, null, launchMode, nonDevModeTccl); - thread.setDaemon(true); - return thread; + return new ThreadFactory() { + @Override + public Thread newThread(Runnable runnable) { + VertxThread thread = createVertxThread(runnable, + "executor-thread-" + threadCount.getAndIncrement(), true, 0, null, launchMode, nonDevModeTccl); + thread.setDaemon(true); + return thread; + } }; } diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/VertxMDC.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/VertxMDC.java new file mode 100644 index 0000000000000..1f6eb1a800516 --- /dev/null +++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/VertxMDC.java @@ -0,0 +1,311 @@ +package io.quarkus.vertx.core.runtime; + +import static io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.setContextSafe; +import static io.smallrye.common.vertx.VertxContext.getOrCreateDuplicatedContext; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.jboss.logmanager.MDCProvider; + +import io.vertx.core.Context; +import io.vertx.core.Vertx; +import io.vertx.core.impl.ContextInternal; + +public enum VertxMDC implements MDCProvider { + INSTANCE; + + final InheritableThreadLocal> inheritableThreadLocalMap = new InheritableThreadLocal<>() { + @Override + protected Map childValue(Map parentValue) { + if (parentValue == null) { + return null; + } + return new HashMap<>(parentValue); + } + + @Override + protected Map initialValue() { + return new HashMap<>(); + } + }; + + /** + * Get the value for a key, or {@code null} if there is no mapping. + * + * Tries to use the current Vert.x Context, if the context is non-existent + * meaning that it was called out of a Vert.x thread it will fall back to + * the thread local context map. + * + * @param key the key + * @return the value + */ + @Override + public String get(String key) { + return get(key, getContext()); + } + + /** + * Get the value for a key, or {@code null} if there is no mapping. + * + * Tries to use the current Vert.x Context, if the context is non-existent + * meaning that it was called out of a Vert.x thread it will fall back to + * the thread local context map. + * + * @param key the key + * @return the value + */ + @Override + public Object getObject(String key) { + return getObject(key, getContext()); + } + + /** + * Get the value for a key the in specified Context, or {@code null} if there is no mapping. + * If the informed context is null it falls back to the thread local context map. + * + * @param key the key + * @param vertxContext the context + * @return the value + */ + public String get(String key, Context vertxContext) { + Object value = getObject(key, vertxContext); + return value != null ? value.toString() : null; + } + + /** + * Get the value for a key the in specified Context, or {@code null} if there is no mapping. + * If the context is null it falls back to the thread local context map. + * + * @param key the key + * @param vertxContext the context + * @return the value + */ + public Object getObject(String key, Context vertxContext) { + Objects.requireNonNull(key); + return contextualDataMap(vertxContext).get(key); + } + + /** + * Set the value of a key, returning the old value (if any) or {@code null} if there was none. + * + * Tries to use the current Vert.x Context, if the context is non-existent + * meaning that it was called out of a Vert.x thread it will fall back to + * the thread local context map. + * + * @param key the key + * @param value the new value + * @return the old value or {@code null} if there was none + */ + @Override + public String put(String key, String value) { + return put(key, value, getContext()); + } + + /** + * Set the value of a key, returning the old value (if any) or {@code null} if there was none. + * + * Tries to use the current Vert.x Context, if the context is non-existent + * meaning that it was called out of a Vert.x thread it will fall back to + * the thread local context map. + * + * @param key the key + * @param value the new value + * @return the old value or {@code null} if there was none + */ + @Override + public Object putObject(String key, Object value) { + return putObject(key, value, getContext()); + } + + /** + * Set the value of a key, returning the old value (if any) or {@code null} if there was none. + * If the informed context is null it falls back to the thread local context map. + * + * @param key the key + * @param value the new value + * @return the old value or {@code null} if there was none + */ + public String put(String key, String value, Context vertxContext) { + Object oldValue = putObject(key, value, vertxContext); + return oldValue != null ? oldValue.toString() : null; + } + + /** + * Set the value of a key, returning the old value (if any) or {@code null} if there was none. + * If the informed context is null it falls back to the thread local context map. + * + * @param key the key + * @param value the new value + * @return the old value or {@code null} if there was none + */ + public Object putObject(String key, Object value, Context vertxContext) { + Objects.requireNonNull(key); + Objects.requireNonNull(value); + return contextualDataMap(vertxContext).put(key, value); + } + + /** + * Removes a key. + * + * Tries to use the current Vert.x Context, if the context is non-existent + * meaning that it was called out of a Vert.x thread it will fall back to + * the thread local context map. + * + * @param key the key + * @return the old value or {@code null} if there was none + */ + @Override + public String remove(String key) { + return remove(key, getContext()); + } + + /** + * Removes a key. + * + * Tries to use the current Vert.x Context, if the context is non-existent + * meaning that it was called out of a Vert.x thread it will fall back to + * the thread local context map. + * + * @param key the key + * @return the old value or {@code null} if there was none + */ + @Override + public Object removeObject(String key) { + return removeObject(key, getContext()); + } + + /** + * Removes a key. + * If the informed context is null it falls back to the thread local context map. + * + * @param key the key + * @return the old value or {@code null} if there was none + */ + public String remove(String key, Context vertxContext) { + Object oldValue = removeObject(key, vertxContext); + return oldValue != null ? oldValue.toString() : null; + } + + /** + * Removes a key. + * If the informed context is null it falls back to the thread local context map. + * + * @param key the key + * @return the old value or {@code null} if there was none + */ + public Object removeObject(String key, Context vertxContext) { + Objects.requireNonNull(key); + return contextualDataMap(vertxContext).remove(key); + } + + /** + * Get a copy of the MDC map. This is a relatively expensive operation. + * + * Tries to use the current Vert.x Context, if the context is non-existent + * meaning that it was called out of a Vert.x thread it will fall back to + * the thread local context map. + * + * @return a copy of the map + */ + @Override + public Map copy() { + return copy(getContext()); + } + + /** + * Get a copy of the MDC map. This is a relatively expensive operation. + * + * Tries to use the current Vert.x Context, if the context is non-existent + * meaning that it was called out of a Vert.x thread it will fall back to + * the thread local context map. + * + * @return a copy of the map + */ + @Override + public Map copyObject() { + return copyObject(getContext()); + } + + /** + * Get a copy of the MDC map. This is a relatively expensive operation. + * If the informed context is null it falls back to the thread local context map. + * + * @return a copy of the map + */ + public Map copy(Context vertxContext) { + final HashMap result = new HashMap<>(); + Map contextualDataMap = contextualDataMap(vertxContext); + for (Map.Entry entry : contextualDataMap.entrySet()) { + result.put(entry.getKey(), entry.getValue().toString()); + } + return result; + } + + /** + * Get a copy of the MDC map. This is a relatively expensive operation. + * If the informed context is null it falls back to the thread local context map. + * + * @return a copy of the map + */ + public Map copyObject(Context vertxContext) { + return new HashMap<>(contextualDataMap(vertxContext)); + } + + /** + * Clear the current MDC map. + * Tries to use the current Vert.x Context, if the context is non-existent + * meaning that it was called out of a Vert.x thread it will fall back to + * the thread local context map. + */ + @Override + public void clear() { + clear(getContext()); + } + + /** + * Clear the current MDC map. + * If the informed context is null it falls back to the thread local context map. + */ + public void clear(Context vertxContext) { + contextualDataMap(vertxContext).clear(); + } + + /** + * Gets the current duplicated context or a new duplicated context if a Vert.x Context exists. Multiple invocations + * of this method may return the same or different context. If the current context is a duplicate one, multiple + * invocations always return the same context. If the current context is not duplicated, a new instance is returned + * with each method invocation. + * + * @return a duplicated Vert.x Context or null. + */ + private Context getContext() { + Context context = Vertx.currentContext(); + if (context != null) { + Context dc = getOrCreateDuplicatedContext(context); + setContextSafe(dc, true); + return dc; + } + return null; + } + + /** + * Gets the current Contextual Data Map from the current Vert.x Context if it is not null or the default + * ThreadLocal Data Map for use in non Vert.x Threads. + * + * @return the current Contextual Data Map. + */ + @SuppressWarnings({ "unchecked" }) + private Map contextualDataMap(Context ctx) { + if (ctx == null) { + return inheritableThreadLocalMap.get(); + } + + ConcurrentMap lcd = Objects.requireNonNull((ContextInternal) ctx).localContextData(); + return (ConcurrentMap) lcd.computeIfAbsent(VertxMDC.class.getName(), + k -> new ConcurrentHashMap()); + } +} 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 0826786c47be9..178d075f1f9dc 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 @@ -41,7 +41,14 @@ public final class VertxContextSafetyToggle { private static final Object ACCESS_TOGGLE_KEY = new Object(); public static final String UNRESTRICTED_BY_DEFAULT_PROPERTY = "io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.UNRESTRICTED_BY_DEFAULT"; + + /** + * This gets exposed for people who prefer fully disabling all safeguards, for example because they have tested it all + * carefully under load already and are preferring maximum efficiency over the safeguards introduced by this class. + */ + public static final String FULLY_DISABLE_PROPERTY = "io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.I_HAVE_CHECKED_EVERYTHING"; private static final boolean UNRESTRICTED_BY_DEFAULT = Boolean.getBoolean(UNRESTRICTED_BY_DEFAULT_PROPERTY); + private static final boolean FULLY_DISABLED = Boolean.getBoolean(FULLY_DISABLE_PROPERTY); /** * Verifies if the current Vert.x context was flagged as safe @@ -58,6 +65,8 @@ public final class VertxContextSafetyToggle { * @throws IllegalStateException if the context exists and it failed to be validated */ public static void validateContextIfExists(final String errorMessageOnVeto, final String errorMessageOnDoubt) { + if (FULLY_DISABLED) + return; final io.vertx.core.Context context = Vertx.currentContext(); if (context != null) { checkIsSafe(context, errorMessageOnVeto, errorMessageOnDoubt); @@ -90,6 +99,8 @@ private static void checkIsSafe(final Context context, final String errorMessage * @throws IllegalStateException if there is no current context, or if it's of the wrong type. */ public static void setCurrentContextSafe(final boolean safe) { + if (FULLY_DISABLED) + return; final io.vertx.core.Context context = Vertx.currentContext(); setContextSafe(context, safe); } @@ -101,6 +112,8 @@ public static void setCurrentContextSafe(final boolean safe) { * @throws IllegalStateException if context is null or not of the expected type. */ public static void setContextSafe(final Context context, final boolean safe) { + if (FULLY_DISABLED) + return; if (context == null) { throw new IllegalStateException("Can't set the context safety flag: no Vert.x context found"); } else if (!VertxContext.isDuplicatedContext(context)) { diff --git a/extensions/vertx/runtime/src/main/resources/META-INF/services/org.jboss.logmanager.MDCProvider b/extensions/vertx/runtime/src/main/resources/META-INF/services/org.jboss.logmanager.MDCProvider new file mode 100644 index 0000000000000..08e547a5085bd --- /dev/null +++ b/extensions/vertx/runtime/src/main/resources/META-INF/services/org.jboss.logmanager.MDCProvider @@ -0,0 +1 @@ +io.quarkus.bootstrap.logging.LateBoundMDCProvider \ No newline at end of file diff --git a/extensions/websockets/server/deployment/src/test/java/io/quarkus/websockets/test/WebsocketRootPathTestCase.java b/extensions/websockets/server/deployment/src/test/java/io/quarkus/websockets/test/WebsocketRootPathTestCase.java new file mode 100644 index 0000000000000..ff0ad1904607d --- /dev/null +++ b/extensions/websockets/server/deployment/src/test/java/io/quarkus/websockets/test/WebsocketRootPathTestCase.java @@ -0,0 +1,60 @@ +package io.quarkus.websockets.test; + +import java.net.URI; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +import javax.websocket.ClientEndpointConfig; +import javax.websocket.ContainerProvider; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.Session; + +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.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; + +/** + * smoke test that websockets work as expected in dev mode + */ +public class WebsocketRootPathTestCase { + + @TestHTTPResource("foo/echo") + URI echoUri; + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(a -> { + a.addClasses(EchoWebSocket.class, EchoService.class) + .add(new StringAsset("quarkus.http.root-path=/foo"), "application.properties"); + }); + + @Test + public void testHttpRootPath() throws Exception { + + LinkedBlockingDeque message = new LinkedBlockingDeque<>(); + Session session = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() { + @Override + public void onOpen(Session session, EndpointConfig endpointConfig) { + session.addMessageHandler(new MessageHandler.Whole() { + @Override + public void onMessage(String s) { + message.add(s); + } + }); + session.getAsyncRemote().sendText("hello"); + } + }, ClientEndpointConfig.Builder.create().build(), echoUri); + + try { + Assertions.assertEquals("hello", message.poll(20, TimeUnit.SECONDS)); + } finally { + session.close(); + } + } +} diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 453a45c87eb52..a7e28cd90648b 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -46,9 +46,9 @@ 5.8.2 3.8.4 3.22.0 - 3.4.3.Final + 3.5.0.Final 1.3.5 - 1.0.10.Final + 1.0.11.Final 2.2.3 3.0.0-M5 @@ -332,6 +332,22 @@ clean install + + quick-build-docs + + + quicklyDocs + + + + true + true + true + + + clean install + + quick-build-ci 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 89ea7bc77e229..046d0d90f0bdc 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 @@ -249,6 +249,7 @@ Collection generateSyntheticBean(BeanInfo bean) { implementGetKind(beanCreator, InjectableBean.Kind.SYNTHETIC); implementEquals(bean, beanCreator); implementHashCode(bean, beanCreator); + implementToString(beanCreator); beanCreator.close(); return classOutput.getResources(); @@ -342,6 +343,7 @@ Collection generateClassBean(BeanInfo bean, ClassInfo beanClass) { implementIsSuppressed(bean, beanCreator); implementEquals(bean, beanCreator); implementHashCode(bean, beanCreator); + implementToString(beanCreator); beanCreator.close(); return classOutput.getResources(); @@ -445,6 +447,7 @@ Collection generateProducerMethodBean(BeanInfo bean, MethodInfo produc implementIsSuppressed(bean, beanCreator); implementEquals(bean, beanCreator); implementHashCode(bean, beanCreator); + implementToString(beanCreator); beanCreator.close(); return classOutput.getResources(); @@ -533,6 +536,7 @@ Collection generateProducerFieldBean(BeanInfo bean, FieldInfo producer implementIsSuppressed(bean, beanCreator); implementEquals(bean, beanCreator); implementHashCode(bean, beanCreator); + implementToString(beanCreator); beanCreator.close(); return classOutput.getResources(); @@ -1593,17 +1597,8 @@ protected void implementGet(BeanInfo bean, ClassCreator beanCreator, ProviderTyp // We can optimize if: // 1) class bean - has no @PreDestroy interceptor and there is no @PreDestroy callback // 2) producer - there is no disposal method - boolean canBeOptimized = false; - if (bean.isClassBean()) { - canBeOptimized = bean.getLifecycleInterceptors(InterceptionType.PRE_DESTROY).isEmpty() - && Beans.getCallbacks(bean.getTarget().get().asClass(), - DotNames.PRE_DESTROY, - bean.getDeployment().getBeanArchiveIndex()).isEmpty(); - } else if (bean.isProducerMethod() || bean.isProducerField()) { - canBeOptimized = bean.getDisposer() == null; - } - - if (canBeOptimized) { + // 3) synthetic bean - has no destruction logic + if (!bean.hasDestroyLogic()) { // If there is no dependency in the creational context we don't have to store the instance in the CreationalContext ResultHandle creationalContext = get.checkCast(get.getMethodParam(0), CreationalContextImpl.class); get.ifNonZero( @@ -1706,6 +1701,11 @@ protected void implementHashCode(BeanInfo bean, ClassCreator beanCreator) { hashCode.returnValue(constantHashCodeResult); } + protected void implementToString(ClassCreator beanCreator) { + MethodCreator toString = beanCreator.getMethodCreator("toString", String.class).setModifiers(ACC_PUBLIC); + toString.returnValue(toString.invokeStaticMethod(MethodDescriptors.BEANS_TO_STRING, toString.getThis())); + } + /** * * @param bean diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java index b1f8dfad9edae..c0b52952f1476 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java @@ -272,6 +272,10 @@ public final class MethodDescriptors { ComponentsProvider.class, "unableToLoadRemovedBeanType", void.class, String.class, Throwable.class); + public static final MethodDescriptor BEANS_TO_STRING = MethodDescriptor.ofMethod(io.quarkus.arc.impl.Beans.class, + "toString", String.class, + InjectableBean.class); + private MethodDescriptors() { } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverInfo.java index 781377bafa4d4..6374e4c0b80c1 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverInfo.java @@ -8,9 +8,11 @@ import io.quarkus.arc.processor.ObserverTransformer.TransformationContext; import io.quarkus.gizmo.MethodCreator; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.ListIterator; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; import javax.enterprise.event.Reception; @@ -25,6 +27,7 @@ import org.jboss.jandex.MethodInfo; import org.jboss.jandex.MethodParameterInfo; import org.jboss.jandex.Type; +import org.jboss.jandex.TypeVariable; import org.jboss.logging.Logger; /** @@ -48,10 +51,20 @@ static ObserverInfo create(BeanInfo declaringBean, MethodInfo observerMethod, In } else { priority = ObserverMethod.DEFAULT_PRIORITY; } + + Type observedType = observerMethod.parameters().get(eventParameter.position()); + if (Types.containsTypeVariable(observedType)) { + Map resolvedTypeVariables = Types + .resolvedTypeVariables(declaringBean.getImplClazz(), declaringBean.getDeployment()) + .getOrDefault(observerMethod.declaringClass(), Collections.emptyMap()); + observedType = Types.resolveTypeParam(observedType, resolvedTypeVariables, + declaringBean.getDeployment().getBeanArchiveIndex()); + } + return create(null, declaringBean.getDeployment(), declaringBean.getTarget().get().asClass().name(), declaringBean, observerMethod, injection, eventParameter, - observerMethod.parameters().get(eventParameter.position()), + observedType, initQualifiers(declaringBean.getDeployment(), observerMethod, eventParameter), initReception(isAsync, declaringBean.getDeployment(), observerMethod), initTransactionPhase(isAsync, declaringBean.getDeployment(), observerMethod), isAsync, priority, transformers, 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 c188fc591c1a8..282bdfbe13c06 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 @@ -239,13 +239,18 @@ public static ResultHandle getParameterizedType(BytecodeCreator creator, ResultH } private static ResultHandle doLoadClass(BytecodeCreator creator, String className, ResultHandle tccl) { - //we need to use Class.forName as the class may be package private - if (tccl == null) { - ResultHandle currentThread = creator - .invokeStaticMethod(MethodDescriptors.THREAD_CURRENT_THREAD); - tccl = creator.invokeVirtualMethod(MethodDescriptors.THREAD_GET_TCCL, currentThread); + if (className.startsWith("java.")) { + return creator.loadClass(className); + } else { + //we need to use Class.forName as the class may be package private + if (tccl == null) { + ResultHandle currentThread = creator + .invokeStaticMethod(MethodDescriptors.THREAD_CURRENT_THREAD); + tccl = creator.invokeVirtualMethod(MethodDescriptors.THREAD_GET_TCCL, currentThread); + } + return creator.invokeStaticMethod(MethodDescriptors.CL_FOR_NAME, creator.load(className), creator.load(false), + tccl); } - return creator.invokeStaticMethod(MethodDescriptors.CL_FOR_NAME, creator.load(className), creator.load(false), tccl); } static Type getProviderType(ClassInfo classInfo) { diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/All.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/All.java index fc4e9bcec242a..03d26f220941a 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/All.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/All.java @@ -59,7 +59,7 @@ * } * * - * By default, the list of beans is sorted by {@link InjectableBean#getPriority()}. Higher priority goes first. + * The list is sorted by {@link InjectableBean#getPriority()}. Higher priority goes first. * * @see Priority */ diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java index 1a8192c6082e2..0079555f3a4d5 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java @@ -134,6 +134,36 @@ public interface ArcContainer { */ InjectableInstance select(TypeLiteral type, Annotation... qualifiers); + /** + * List all beans matching the required type and qualifiers. + *

+ * Instances of dependent scoped beans should be explicitly destroyed with {@link InstanceHandle#destroy()}. + *

+ * The list is sorted by {@link InjectableBean#getPriority()}. Higher priority goes first. + * + * @param + * @param type + * @param qualifiers + * @return the list of handles for the disambiguated beans + * @see All + */ + List> listAll(Class type, Annotation... qualifiers); + + /** + * List all beans matching the required type and qualifiers. + *

+ * Instances of dependent scoped beans should be explicitly destroyed with {@link InstanceHandle#destroy()}. + *

+ * The list of is sorted by {@link InjectableBean#getPriority()}. Higher priority goes first. + * + * @param + * @param type + * @param qualifiers + * @return the list of handles for the disambiguated beans + * @see All + */ + List> listAll(TypeLiteral type, Annotation... qualifiers); + /** * Returns true if Arc container is running. * This can be used as a quick check to determine CDI availability in Quarkus. diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableBean.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableBean.java index ca800d59a5f80..edb5bcc8ffd6b 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableBean.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableBean.java @@ -148,7 +148,9 @@ enum Kind { PRODUCER_METHOD, SYNTHETIC, INTERCEPTOR, - DECORATOR; + DECORATOR, + BUILTIN, + ; public static Kind from(String value) { for (Kind kind : values()) { diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java index 00386f409c3dd..53a49439e23cc 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java @@ -115,10 +115,10 @@ public ArcContainerImpl(CurrentContextFactory currentContextFactory) { applicationContext = new ApplicationContext(); singletonContext = new SingletonContext(); requestContext = new RequestContext(this.currentContextFactory.create(RequestScoped.class)); - contexts = new HashMap<>(); - putContext(requestContext); - putContext(applicationContext); - putContext(singletonContext); + Map, List> contexts = new HashMap<>(); + putContext(requestContext, contexts); + putContext(applicationContext, contexts); + putContext(singletonContext, contexts); for (ComponentsProvider componentsProvider : ServiceLoader.load(ComponentsProvider.class)) { Components components = componentsProvider.getComponents(); @@ -143,11 +143,14 @@ public ArcContainerImpl(CurrentContextFactory currentContextFactory) { throw new IllegalStateException( "Failed to register a context - built-in singleton context is always active: " + context); } - putContext(context); + putContext(context, contexts); } transitiveInterceptorBindings.putAll(components.getTransitiveInterceptorBindings()); qualifierNonbindingMembers.putAll(components.getQualifierNonbindingMembers()); } + + this.contexts = Map.copyOf(contexts); + // register built-in beans addBuiltInBeans(beans); @@ -173,7 +176,7 @@ public ArcContainerImpl(CurrentContextFactory currentContextFactory) { this.qualifierNonbindingMembers = Map.copyOf(qualifierNonbindingMembers); } - private void putContext(InjectableContext context) { + private void putContext(InjectableContext context, Map, List> contexts) { Collection values = contexts.get(context.getScope()); if (values == null) { contexts.put(context.getScope(), Collections.singletonList(context)); @@ -181,15 +184,16 @@ private void putContext(InjectableContext context) { List multi = new ArrayList<>(values.size() + 1); multi.addAll(values); multi.add(context); - contexts.put(context.getScope(), Collections.unmodifiableList(multi)); + contexts.put(context.getScope(), List.copyOf(multi)); } } private static void addBuiltInBeans(List> beans) { - // BeanManager, Event, Instance + // BeanManager, Event, Instance, InjectionPoint beans.add(new BeanManagerBean()); beans.add(new EventBean()); beans.add(InstanceBean.INSTANCE); + beans.add(new InjectionPointBean()); } public void init() { @@ -299,6 +303,18 @@ public InjectableInstance select(TypeLiteral type, Annotation... quali return instance.select(type, qualifiers); } + @Override + public List> listAll(Class type, Annotation... qualifiers) { + return Instances.listOfHandles(CurrentInjectionPointProvider.EMPTY_SUPPLIER, type, Set.of(qualifiers), + new CreationalContextImpl<>(null)); + } + + @Override + public List> listAll(TypeLiteral type, Annotation... qualifiers) { + return Instances.listOfHandles(CurrentInjectionPointProvider.EMPTY_SUPPLIER, type.getType(), Set.of(qualifiers), + new CreationalContextImpl<>(null)); + } + @Override public boolean isRunning() { return running.get(); @@ -394,7 +410,6 @@ public synchronized void shutdown() { // Clear caches Reflections.clearCaches(); - contexts.clear(); resolved.clear(); running.set(false); InterceptedStaticMethods.clear(); @@ -870,10 +885,11 @@ private static final class Resolvable { final Annotation[] qualifiers; Resolvable(Type requiredType, Annotation[] qualifiers) { - // if the type is Event or Instance (the built-in types), the resolution simplifies type to raw type and ignores qualifiers + // if the type is Event, Instance or InjectionPoint (the built-in types), the resolution simplifies + // type to raw type and ignores qualifiers // this is so that every injection point matches the bean we provide for that type Type rawType = Reflections.getRawType(requiredType); - if (Event.class.equals(rawType) || Instance.class.equals(rawType)) { + if (Event.class.equals(rawType) || Instance.class.equals(rawType) || InjectionPoint.class.equals(rawType)) { this.requiredType = rawType; this.qualifiers = ANY_QUALIFIER; } else { diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Beans.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Beans.java new file mode 100644 index 0000000000000..5c9d64ce75b68 --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Beans.java @@ -0,0 +1,21 @@ +package io.quarkus.arc.impl; + +import io.quarkus.arc.InjectableBean; + +public class Beans { + + private Beans() { + } + + public static String toString(InjectableBean bean) { + return new StringBuilder() + .append(bean.getKind()) + .append(" bean [class=") + .append(bean.getBeanClass().getName()) + .append(", id=") + .append(bean.getIdentifier()) + .append("]") + .toString(); + } + +} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BuiltInBean.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BuiltInBean.java index 1d0ee1f33bd41..fa68edf4560dd 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BuiltInBean.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BuiltInBean.java @@ -18,4 +18,15 @@ public String getIdentifier() { public T create(CreationalContext creationalContext) { return get(creationalContext); } + + @Override + public Kind getKind() { + return Kind.BUILTIN; + } + + @Override + public String toString() { + return Beans.toString(this); + } + } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ClientProxies.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ClientProxies.java index 9469bf21f4e1a..3b0a8109d540e 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ClientProxies.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ClientProxies.java @@ -5,6 +5,7 @@ import io.quarkus.arc.InjectableContext; import java.util.List; import javax.enterprise.context.ContextNotActiveException; +import javax.enterprise.context.RequestScoped; import javax.enterprise.context.spi.Contextual; public final class ClientProxies { @@ -43,7 +44,13 @@ public static T getDelegate(InjectableBean bean) { } } if (result == null) { - throw new ContextNotActiveException(); + String msg = String.format( + "%s context was not active when trying to obtain a bean instance for a client proxy of %s", + bean.getScope().getSimpleName(), bean); + if (bean.getScope().equals(RequestScoped.class)) { + msg += "\n\t- you can activate the request context for a specific method using the @ActivateRequestContext interceptor binding"; + } + throw new ContextNotActiveException(msg); } return result; } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CurrentInjectionPointProvider.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CurrentInjectionPointProvider.java index 90715ddc59d0c..f615c11ef92f0 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CurrentInjectionPointProvider.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CurrentInjectionPointProvider.java @@ -34,6 +34,14 @@ public class CurrentInjectionPointProvider implements InjectableReferenceProv static final InjectionPoint EMPTY = new InjectionPointImpl(Object.class, Object.class, Collections.emptySet(), null, null, null, -1); + static final Supplier EMPTY_SUPPLIER = new Supplier() { + + @Override + public InjectionPoint get() { + return CurrentInjectionPointProvider.EMPTY; + } + }; + private final Supplier> delegateSupplier; private final InjectionPoint injectionPoint; diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointBean.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointBean.java new file mode 100644 index 0000000000000..3920df70fd41c --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointBean.java @@ -0,0 +1,25 @@ +package io.quarkus.arc.impl; + +import java.lang.reflect.Type; +import java.util.Set; +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.spi.InjectionPoint; + +public class InjectionPointBean extends BuiltInBean { + private static final Set IP_TYPES = Set.of(InjectionPoint.class, Object.class); + + @Override + public Set getTypes() { + return IP_TYPES; + } + + @Override + public InjectionPoint get(CreationalContext creationalContext) { + return InjectionPointProvider.get(); + } + + @Override + public Class getBeanClass() { + return CurrentInjectionPointProvider.InjectionPointImpl.class; + } +} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java index 1a9dbd47e3dc2..6e75c4e3ed288 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java @@ -11,9 +11,7 @@ import java.util.Comparator; import java.util.List; import java.util.Set; -import java.util.function.Predicate; import java.util.function.Supplier; -import java.util.stream.Collectors; import javax.enterprise.context.Dependent; import javax.enterprise.inject.spi.InjectionPoint; @@ -21,8 +19,12 @@ public final class Instances { static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[] {}; - static final Comparator> PRIORITY_COMPARATOR = Collections - .reverseOrder(Comparator.comparingInt(InjectableBean::getPriority)); + static final Comparator> PRIORITY_COMPARATOR = new Comparator<>() { + @Override + public int compare(InjectableBean ib1, InjectableBean ib2) { + return Integer.compare(ib2.getPriority(), ib1.getPriority()); + } + }; private Instances() { } @@ -32,12 +34,16 @@ public static List> resolveBeans(Type requiredType, Set> resolveBeans(Type requiredType, Annotation... requiredQualifiers) { - return ArcContainerImpl.instance() - .getResolvedBeans(requiredType, requiredQualifiers) - .stream() - .filter(Predicate.not(InjectableBean::isSuppressed)) - .sorted(PRIORITY_COMPARATOR) - .collect(Collectors.toUnmodifiableList()); + Set> resolvedBeans = ArcContainerImpl.instance() + .getResolvedBeans(requiredType, requiredQualifiers); + List> nonSuppressed = new ArrayList<>(resolvedBeans.size()); + for (InjectableBean injectableBean : resolvedBeans) { + if (!injectableBean.isSuppressed()) { + nonSuppressed.add(injectableBean); + } + } + nonSuppressed.sort(PRIORITY_COMPARATOR); + return List.copyOf(nonSuppressed); } @SuppressWarnings("unchecked") @@ -63,15 +69,10 @@ public static List listOf(InjectableBean targetBean, Type injectionPoi return List.copyOf(list); } - @SuppressWarnings("unchecked") public static List> listOfHandles(InjectableBean targetBean, Type injectionPointType, Type requiredType, Set requiredQualifiers, CreationalContextImpl creationalContext, Set annotations, Member javaMember, int position) { - List> beans = resolveBeans(requiredType, requiredQualifiers); - if (beans.isEmpty()) { - return Collections.emptyList(); - } Supplier supplier = new Supplier() { @Override public InjectionPoint get() { @@ -79,9 +80,20 @@ public InjectionPoint get() { annotations, javaMember, position); } }; + return listOfHandles(supplier, requiredType, requiredQualifiers, creationalContext); + } + + @SuppressWarnings("unchecked") + public static List> listOfHandles(Supplier injectionPoint, Type requiredType, + Set requiredQualifiers, + CreationalContextImpl creationalContext) { + List> beans = resolveBeans(requiredType, requiredQualifiers); + if (beans.isEmpty()) { + return Collections.emptyList(); + } List> list = new ArrayList<>(beans.size()); for (InjectableBean bean : beans) { - list.add(getHandle((CreationalContextImpl) creationalContext, (InjectableBean) bean, supplier)); + list.add(getHandle((CreationalContextImpl) creationalContext, (InjectableBean) bean, injectionPoint)); } return List.copyOf(list); } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RequestContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RequestContext.java index c805d84593d61..c7b3e07684739 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RequestContext.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RequestContext.java @@ -12,7 +12,6 @@ import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.stream.Collectors; import javax.enterprise.context.BeforeDestroyed; @@ -82,7 +81,7 @@ public T get(Contextual contextual, CreationalContext creationalContex T result = getIfActive(contextual, CreationalContextImpl.unwrap(Objects.requireNonNull(creationalContext, "CreationalContext must not be null"))); if (result == null) { - throw new ContextNotActiveException(); + throw notActive(); } return result; } @@ -97,7 +96,7 @@ public T get(Contextual contextual) { } RequestContextState state = currentContext.get(); if (state == null) { - throw new ContextNotActiveException(); + throw notActive(); } ContextInstanceHandle instance = (ContextInstanceHandle) state.map.get(contextual); return instance == null ? null : instance.get(); @@ -113,7 +112,7 @@ public void destroy(Contextual contextual) { RequestContextState state = currentContext.get(); if (state == null) { // Context is not active - throw new ContextNotActiveException(); + throw notActive(); } ContextInstanceHandle instance = state.map.remove(contextual); if (instance != null) { @@ -141,7 +140,7 @@ public ContextState getState() { RequestContextState state = currentContext.get(); if (state == null) { // Thread local not set - context is not active! - throw new ContextNotActiveException(); + throw notActive(); } return state; } @@ -168,7 +167,7 @@ public void destroy(ContextState state) { } if (state instanceof RequestContextState) { RequestContextState reqState = ((RequestContextState) state); - reqState.isValid.set(false); + reqState.isValid = false; synchronized (state) { Map, ContextInstanceHandle> map = ((RequestContextState) state).map; // Fire an event with qualifier @BeforeDestroyed(RequestScoped.class) if there are any observers for it @@ -207,6 +206,11 @@ private void fireIfNotEmpty(LazyValue> value) { } } + private ContextNotActiveException notActive() { + String msg = "Request context is not active - you can activate the request context for a specific method using the @ActivateRequestContext interceptor binding"; + return new ContextNotActiveException(msg); + } + private static Notifier createInitializedNotifier() { return EventImpl.createNotifier(Object.class, Object.class, new HashSet<>(Arrays.asList(Initialized.Literal.REQUEST, Any.Literal.INSTANCE)), @@ -228,11 +232,12 @@ private static Notifier createDestroyedNotifier() { static class RequestContextState implements ContextState { private final Map, ContextInstanceHandle> map; - private final AtomicBoolean isValid; + + private volatile boolean isValid; RequestContextState(ConcurrentMap, ContextInstanceHandle> value) { this.map = Objects.requireNonNull(value); - this.isValid = new AtomicBoolean(true); + this.isValid = true; } @Override @@ -243,7 +248,7 @@ public Map, Object> getContextualInstances() { @Override public boolean isValid() { - return isValid.get(); + return isValid; } } diff --git a/independent-projects/arc/tests/pom.xml b/independent-projects/arc/tests/pom.xml index faf78e701fc2a..95aa555f84c7f 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.6.20 + 1.6.21 test diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/all/ListAllTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/all/ListAllTest.java new file mode 100644 index 0000000000000..a1578c6bcf307 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/all/ListAllTest.java @@ -0,0 +1,91 @@ +package io.quarkus.arc.test.all; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.test.ArcTestContainer; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.PreDestroy; +import javax.annotation.Priority; +import javax.enterprise.context.Dependent; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class ListAllTest { + + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(Service.class, ServiceAlpha.class, ServiceBravo.class); + + @Test + public void testSelectAll() { + List> services = Arc.container().listAll(Service.class); + assertEquals(2, services.size()); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> services.remove(0)); + // ServiceBravo has higher priority + InstanceHandle bravoHandle = services.get(0); + Service bravo = bravoHandle.get(); + assertEquals("bravo", bravo.ping()); + assertEquals("alpha", services.get(1).get().ping()); + assertEquals(Dependent.class, bravoHandle.getBean().getScope()); + assertTrue(bravo.getInjectionPoint().isPresent()); + // Empty injection point + assertEquals(Object.class, bravo.getInjectionPoint().get().getType()); + bravoHandle.destroy(); + assertEquals(true, ServiceBravo.DESTROYED.get()); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> bravoHandle.get()); + } + + interface Service { + + String ping(); + + default Optional getInjectionPoint() { + return Optional.empty(); + } + + } + + @Singleton + static class ServiceAlpha implements Service { + + public String ping() { + return "alpha"; + } + } + + @Priority(5) // this impl should go first + @Dependent + static class ServiceBravo implements Service { + + static final AtomicBoolean DESTROYED = new AtomicBoolean(); + + @Inject + InjectionPoint injectionPoint; + + public String ping() { + return "bravo"; + } + + @Override + public Optional getInjectionPoint() { + return Optional.of(injectionPoint); + } + + @PreDestroy + void destroy() { + DESTROYED.set(true); + } + + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/builtin/beans/InjectionPointBuiltInBeanTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/builtin/beans/InjectionPointBuiltInBeanTest.java new file mode 100644 index 0000000000000..f13b2f48d6f3d --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/builtin/beans/InjectionPointBuiltInBeanTest.java @@ -0,0 +1,76 @@ +package io.quarkus.arc.test.builtin.beans; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.test.ArcTestContainer; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import javax.enterprise.context.Dependent; +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.AnnotatedField; +import javax.enterprise.inject.spi.AnnotatedParameter; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class InjectionPointBuiltInBeanTest { + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(MyProducer.class, MyService.class) + .additionalClasses(MyPojo.class) + .build(); + + @Test + public void test() { + MyService bean = Arc.container().select(MyService.class).get(); + assertEquals("Hello MyService.pojo|MyProducer.produce(1)", bean.hello()); + } + + // --- + + @Singleton + static class MyProducer { + @Produces + @Dependent + public MyPojo produce(InjectionPoint injectionPoint, Instance lookup) { + Field field = ((AnnotatedField) injectionPoint.getAnnotated()).getJavaMember(); + String f = field.getDeclaringClass().getSimpleName() + "." + field.getName(); + + // producer method parameters are injection points, so looking up `InjectionPoint` from `lookup` + // must return the injection point corresponding to the `lookup` producer method parameter + InjectionPoint lookupInjectionPoint = lookup.select(InjectionPoint.class).get(); + AnnotatedParameter parameter = (AnnotatedParameter) lookupInjectionPoint.getAnnotated(); + Executable method = parameter.getJavaParameter().getDeclaringExecutable(); + String m = method.getDeclaringClass().getSimpleName() + "." + method.getName() + + "(" + parameter.getPosition() + ")"; + + return new MyPojo(f + "|" + m); + } + } + + @Singleton + static class MyService { + @Inject + MyPojo pojo; + + String hello() { + return pojo.hello(); + } + } + + static class MyPojo { + private final String hello; + + MyPojo(String hello) { + this.hello = hello; + } + + public String hello() { + return "Hello " + hello; + } + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/contextnotactive/ClientProxyContextNotActiveTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/contextnotactive/ClientProxyContextNotActiveTest.java new file mode 100644 index 0000000000000..cac1e4ad3c145 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/contextnotactive/ClientProxyContextNotActiveTest.java @@ -0,0 +1,34 @@ +package io.quarkus.arc.test.clientproxy.contextnotactive; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.test.ArcTestContainer; +import javax.enterprise.context.ContextNotActiveException; +import javax.enterprise.context.RequestScoped; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class ClientProxyContextNotActiveTest { + + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(RequestFoo.class); + + @Test + public void testToStringIsDelegated() { + RequestFoo foo = Arc.container().instance(RequestFoo.class).get(); + assertThatExceptionOfType(ContextNotActiveException.class).isThrownBy(() -> foo.ping()) + .withMessageContaining( + "RequestScoped context was not active when trying to obtain a bean instance for a client proxy of CLASS bean [class=io.quarkus.arc.test.clientproxy.contextnotactive.ClientProxyContextNotActiveTest$RequestFoo, id=3e5a77b35b0824bc957993f6db95a37e766e929e]") + .withMessageContaining( + "you can activate the request context for a specific method using the @ActivateRequestContext interceptor binding"); + } + + @RequestScoped + static class RequestFoo { + + void ping() { + } + + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/contexts/dependent/DependentCreationalContextTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/contexts/dependent/DependentCreationalContextTest.java index 00525509dff12..350dcae44f5a7 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/contexts/dependent/DependentCreationalContextTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/contexts/dependent/DependentCreationalContextTest.java @@ -5,10 +5,15 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import io.quarkus.arc.Arc; +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.BeanDestroyer; import io.quarkus.arc.impl.InstanceImpl; +import io.quarkus.arc.processor.BeanRegistrar; import io.quarkus.arc.test.ArcTestContainer; +import java.util.Map; import javax.annotation.PreDestroy; import javax.enterprise.context.Dependent; +import javax.enterprise.context.spi.CreationalContext; import javax.enterprise.inject.Disposes; import javax.enterprise.inject.Produces; import javax.inject.Inject; @@ -18,8 +23,19 @@ public class DependentCreationalContextTest { @RegisterExtension - ArcTestContainer container = new ArcTestContainer(NoPreDestroy.class, HasDestroy.class, HasDependency.class, - ProducerNoDisposer.class, ProducerWithDisposer.class, String.class, Boolean.class); + ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(NoPreDestroy.class, HasDestroy.class, HasDependency.class, + ProducerNoDisposer.class, ProducerWithDisposer.class, String.class, Boolean.class) + .beanRegistrars(new BeanRegistrar() { + + @Override + public void register(RegistrationContext context) { + context.configure(SyntheticOne.class).addType(SyntheticOne.class).creator(SyntheticOne.class).done(); + context.configure(SyntheticTwo.class).addType(SyntheticTwo.class).creator(SyntheticTwo.class) + .destroyer(SyntheticTwo.class).done(); + } + }) + .build(); @Test public void testCreationalContextOptimization() { @@ -31,6 +47,10 @@ public void testCreationalContextOptimization() { assertBeanType(instance, boolean.class, false); // ProducerWithDisposer assertBeanType(instance, String.class, true); + // Synthetic bean + assertBeanType(instance, SyntheticOne.class, false); + // Synthetic bean with destruction logic + assertBeanType(instance, SyntheticTwo.class, true); } void assertBeanType(InstanceImpl instance, Class beanType, boolean shouldBeStored) { @@ -88,4 +108,27 @@ void dispose(@Disposes String ping) { } } + + public static class SyntheticOne implements BeanCreator { + + @Override + public SyntheticOne create(CreationalContext creationalContext, Map params) { + return new SyntheticOne(); + } + + } + + public static class SyntheticTwo implements BeanCreator, BeanDestroyer { + + @Override + public SyntheticTwo create(CreationalContext creationalContext, Map params) { + return new SyntheticTwo(); + } + + @Override + public void destroy(SyntheticTwo instance, CreationalContext creationalContext, + Map params) { + } + + } } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/observers/inheritance/typevariable/ObserverInheritanceTypeVariableTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/observers/inheritance/typevariable/ObserverInheritanceTypeVariableTest.java new file mode 100644 index 0000000000000..42151cdc53ea2 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/observers/inheritance/typevariable/ObserverInheritanceTypeVariableTest.java @@ -0,0 +1,85 @@ +package io.quarkus.arc.test.observers.inheritance.typevariable; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.Unremovable; +import io.quarkus.arc.test.ArcTestContainer; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Dependent; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * https://github.com/quarkusio/quarkus/issues/25364 + */ +public class ObserverInheritanceTypeVariableTest { + + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(MyEvent.class, MyAEvent.class, MyBEvent.class, MyAService.class, + MyBService.class, EventSource.class); + + @Test + public void testNotification() { + Arc.container().instance(EventSource.class).get().sendA(); + assertNotNull(MyAService.event); + assertNull(MyBService.event); + } + + static class MyEvent { + } + + static class MyAEvent extends MyEvent { + } + + static class MyBEvent extends MyEvent { + } + + static abstract class AbstractService { + + void onEvent(@Observes E myEvent) { + doSomething(myEvent); + } + + abstract void doSomething(E myEvent); + } + + @ApplicationScoped + static class MyAService extends AbstractService { + + static volatile MyAEvent event; + + @Override + protected void doSomething(MyAEvent myEvent) { + MyAService.event = myEvent; + } + } + + @ApplicationScoped + static class MyBService extends AbstractService { + + static volatile MyBEvent event; + + @Override + void doSomething(MyBEvent myEvent) { + MyBService.event = myEvent; + } + } + + @Unremovable + @Dependent + static class EventSource { + + @Inject + Event event; + + void sendA() { + event.fire(new MyAEvent()); + } + } + +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java index 9832c1db598a5..70474377eebe6 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java @@ -29,6 +29,7 @@ public interface BootstrapConstants { String DESCRIPTOR_PATH = META_INF + '/' + DESCRIPTOR_FILE_NAME; String BUILD_STEPS_PATH = META_INF + "/quarkus-build-steps.list"; + String EXTENSION_METADATA_PATH = META_INF + '/' + QUARKUS_EXTENSION_FILE_NAME; String PROP_DEPLOYMENT_ARTIFACT = "deployment-artifact"; String PROP_PROVIDES_CAPABILITIES = "provides-capabilities"; diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/workspace/DefaultWorkspaceModule.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/workspace/DefaultWorkspaceModule.java index 3138d84247f1a..853eba5134974 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/workspace/DefaultWorkspaceModule.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/workspace/DefaultWorkspaceModule.java @@ -154,18 +154,6 @@ public Collection getDirectDependencyConstraints() { public Collection getDirectDependencies() { return DefaultWorkspaceModule.this.getDirectDependencies(); } - - @Override - public boolean hasNonTestSources() { - final int srcTotal = DefaultWorkspaceModule.this.sourcesSets.size(); - if (srcTotal == 0) { - return false; - } - if (srcTotal > 1) { - return true; - } - return !hasTestSources(); - } } private WorkspaceModuleId id; diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/workspace/WorkspaceModule.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/workspace/WorkspaceModule.java index 0b979289d01b4..4e7ebda086f0d 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/workspace/WorkspaceModule.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/workspace/WorkspaceModule.java @@ -77,8 +77,6 @@ interface Mutable extends WorkspaceModule { Mutable addArtifactSources(ArtifactSources sources); - boolean hasNonTestSources(); - WorkspaceModule build(); default Mutable mutable() { diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ArtifactCoords.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ArtifactCoords.java index 1ba8bd4f7edfa..917fec186d921 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ArtifactCoords.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ArtifactCoords.java @@ -2,6 +2,18 @@ public interface ArtifactCoords { + static ArtifactCoords fromString(String str) { + return new GACTV(GACTV.split(str, new String[5])); + } + + static ArtifactCoords pom(String groupId, String artifactId, String version) { + return new GACTV(groupId, artifactId, null, TYPE_POM, version); + } + + static ArtifactCoords jar(String groupId, String artifactId, String version) { + return new GACTV(groupId, artifactId, null, TYPE_JAR, version); + } + String TYPE_JAR = "jar"; String TYPE_POM = "pom"; String DEFAULT_CLASSIFIER = ""; diff --git a/independent-projects/bootstrap/bom/pom.xml b/independent-projects/bootstrap/bom/pom.xml index 75f4de0e7347b..e27069a5e7e5b 100644 --- a/independent-projects/bootstrap/bom/pom.xml +++ b/independent-projects/bootstrap/bom/pom.xml @@ -343,6 +343,11 @@ jsoup ${jsoup.version} + + io.github.crac + org-crac + ${org-crac.version} + io.smallrye.common diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java index f4e5ab0a46a2a..32e55ddb209d7 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java @@ -102,7 +102,7 @@ private QuarkusBootstrap(Builder builder, ConfiguredClassLoading classLoadingCon this.additionalApplicationArchives = new ArrayList<>(builder.additionalApplicationArchives); this.excludeFromClassPath = new ArrayList<>(builder.excludeFromClassPath); this.projectRoot = builder.projectRoot != null ? builder.projectRoot.normalize() : null; - this.buildSystemProperties = builder.buildSystemProperties; + this.buildSystemProperties = builder.buildSystemProperties != null ? builder.buildSystemProperties : new Properties(); this.mode = builder.mode; this.offline = builder.offline; this.test = builder.test; @@ -159,7 +159,19 @@ public CuratedApplication bootstrap() throws BootstrapException { appModelFactory.setEnableClasspathCache(true); } } - return new CuratedApplication(this, appModelFactory.resolveAppModel(), classLoadingConfig); + CurationResult curationResult = appModelFactory.resolveAppModel(); + if (curationResult.getApplicationModel().getAppArtifact() != null) { + if (curationResult.getApplicationModel().getAppArtifact().getArtifactId() != null) { + buildSystemProperties.putIfAbsent("quarkus.application.name", + curationResult.getApplicationModel().getAppArtifact().getArtifactId()); + } + if (curationResult.getApplicationModel().getAppArtifact().getVersion() != null) { + buildSystemProperties.putIfAbsent("quarkus.application.version", + curationResult.getApplicationModel().getAppArtifact().getVersion()); + } + } + + return new CuratedApplication(this, curationResult, classLoadingConfig); } public static ConfiguredClassLoading createClassLoadingConfig(PathCollection applicationRoot, Mode mode, 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 55961a70b3f70..74fb4c0009f6a 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 @@ -47,6 +47,33 @@ public static List getElements(String resourceName, boolean lo return ((QuarkusClassLoader) ccl).getElementsWithResource(resourceName, localOnly); } + /** + * Indicates if a given class is present at runtime. + * + * @param resourceName the path of the resource, for instance {@code path/to/my-resources.properties} for a properties file + * or {@code my/package/MyClass.class} for a class. + */ + public static boolean isClassPresentAtRuntime(String className) { + return isResourcePresentAtRuntime(className.replace('.', '/') + ".class"); + } + + /** + * Indicates if a given resource is present at runtime. + * Can also be used to check if a class is present as a class is just a regular resource. + * + * @param resourceName the path of the resource, for instance {@code path/to/my-resources.properties} for a properties file + * or {@code my/package/MyClass.class} for a class. + */ + public static boolean isResourcePresentAtRuntime(String resourcePath) { + for (ClassPathElement cpe : QuarkusClassLoader.getElements(resourcePath, false)) { + if (cpe.isRuntime()) { + return true; + } + } + + return false; + } + private final String name; private final List elements; private final ConcurrentMap protectionDomains = new ConcurrentHashMap<>(); diff --git a/independent-projects/bootstrap/maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java b/independent-projects/bootstrap/maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java index 8d92b88b7465b..179b4671fd03a 100644 --- a/independent-projects/bootstrap/maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java +++ b/independent-projects/bootstrap/maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java @@ -25,6 +25,7 @@ import io.quarkus.maven.capabilities.CapabilitiesConfig; import io.quarkus.maven.capabilities.CapabilityConfig; import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.GACTV; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; @@ -317,8 +318,7 @@ public void execute() throws MojoExecutionException { // extension.json if (extensionFile == null) { - extensionFile = new File(outputDirectory, - "META-INF" + File.separator + BootstrapConstants.QUARKUS_EXTENSION_FILE_NAME); + extensionFile = output.resolve(BootstrapConstants.QUARKUS_EXTENSION_FILE_NAME).toFile(); } ObjectNode extObject; @@ -457,18 +457,28 @@ private void completeCodestartArtifact(ObjectMapper mapper, ObjectNode extObject } String codestartArtifact = getCodestartArtifact(mvalue.asText(), project.getVersion()); - final AppArtifactCoords codestartArtifactCoords = AppArtifactCoords.fromString(codestartArtifact); + final ArtifactCoords codestartArtifactCoords = GACTV.fromString(codestartArtifact); codestartObject.put("artifact", codestartArtifactCoords.toString()); if (!skipCodestartValidation) { // first we look for it in the workspace, if it's in there we don't need to actually resolve the artifact, because it might not have been built yet if (workspaceProvider.getProject(codestartArtifactCoords.getGroupId(), - codestartArtifactCoords.getArtifactId()) == null) { - try { - resolve(new DefaultArtifact(codestartArtifact)); - } catch (MojoExecutionException e) { - throw new MojoExecutionException("Failed to resolve codestart artifact " + codestartArtifactCoords, e); + codestartArtifactCoords.getArtifactId()) != null) { + return; + } + for (Artifact attached : project.getAttachedArtifacts()) { + if (codestartArtifactCoords.getArtifactId().equals(attached.getArtifactId()) && + codestartArtifactCoords.getClassifier().equals(attached.getClassifier()) && + codestartArtifactCoords.getType().equals(attached.getType()) && + codestartArtifactCoords.getVersion().equals(attached.getVersion()) && + codestartArtifactCoords.getGroupId().equals(attached.getGroupId())) { + return; } } + try { + resolve(new DefaultArtifact(codestartArtifact)); + } catch (MojoExecutionException e) { + throw new MojoExecutionException("Failed to resolve codestart artifact " + codestartArtifactCoords, e); + } } } diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java index 2291675ec2966..fd0475a08117a 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java @@ -179,20 +179,22 @@ public ApplicationModel resolveManagedModel(ArtifactCoords appArtifact, */ public ApplicationModel resolveModel(WorkspaceModule module) throws AppModelResolverException { - if (!module.getMainSources().isOutputAvailable()) { - throw new AppModelResolverException(""); - } final PathList.Builder resolvedPaths = PathList.builder(); - module.getMainSources().getSourceDirs().forEach(s -> { - if (!resolvedPaths.contains(s.getOutputDir())) { - resolvedPaths.add(s.getOutputDir()); - } - }); - module.getMainSources().getResourceDirs().forEach(s -> { - if (!resolvedPaths.contains(s.getOutputDir())) { - resolvedPaths.add(s.getOutputDir()); + if (module.hasMainSources()) { + if (!module.getMainSources().isOutputAvailable()) { + throw new AppModelResolverException("The application module hasn't been built yet"); } - }); + module.getMainSources().getSourceDirs().forEach(s -> { + if (!resolvedPaths.contains(s.getOutputDir())) { + resolvedPaths.add(s.getOutputDir()); + } + }); + module.getMainSources().getResourceDirs().forEach(s -> { + if (!resolvedPaths.contains(s.getOutputDir())) { + resolvedPaths.add(s.getOutputDir()); + } + }); + } final Artifact mainArtifact = new DefaultArtifact(module.getId().getGroupId(), module.getId().getArtifactId(), null, ArtifactCoords.TYPE_JAR, module.getId().getVersion()); @@ -257,8 +259,8 @@ private ApplicationModel doResolveModel(ArtifactCoords coords, } final Artifact mvnArtifact = toAetherArtifact(coords); - List managedDeps = Collections.emptyList(); - List managedRepos = Collections.emptyList(); + List managedDeps = List.of(); + List managedRepos = List.of(); if (managingProject != null) { final ArtifactDescriptorResult managingDescr = mvn.resolveDescriptor(toAetherArtifact(managingProject)); managedDeps = managingDescr.getManagedDependencies(); @@ -269,14 +271,14 @@ private ApplicationModel doResolveModel(ArtifactCoords coords, final List excludedScopes; if (test) { - excludedScopes = Collections.emptyList(); + excludedScopes = List.of(); } else if (devmode) { - excludedScopes = Collections.singletonList("test"); + excludedScopes = List.of("test"); } else { excludedScopes = List.of("provided", "test"); } - DependencyNode resolvedDeps = mvn.resolveManagedDependencies(mvnArtifact, + final DependencyNode resolvedDeps = mvn.resolveManagedDependencies(mvnArtifact, directMvnDeps, managedDeps, managedRepos, excludedScopes.toArray(new String[0])).getRoot(); ArtifactDescriptorResult appArtifactDescr = mvn.resolveDescriptor(toAetherArtifact(appArtifact)); diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContext.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContext.java index 0c3e028f8af78..cd301fd31e531 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContext.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContext.java @@ -204,15 +204,17 @@ public BootstrapMavenOptions getCliOptions() { } public File getUserSettings() { - return userSettings == null - ? userSettings = resolveSettingsFile( - getCliOptions().getOptionValue(BootstrapMavenOptions.ALTERNATE_USER_SETTINGS), - () -> { - final String quarkusMavenSettings = getProperty(MAVEN_SETTINGS); - return quarkusMavenSettings == null ? new File(getUserMavenConfigurationHome(), SETTINGS_XML) - : new File(quarkusMavenSettings); - }) - : userSettings; + if (userSettings == null) { + final String quarkusMavenSettings = getProperty(MAVEN_SETTINGS); + if (quarkusMavenSettings != null) { + var f = new File(quarkusMavenSettings); + return userSettings = f.exists() ? f : null; + } + return userSettings = resolveSettingsFile( + getCliOptions().getOptionValue(BootstrapMavenOptions.ALTERNATE_USER_SETTINGS), + () -> new File(getUserMavenConfigurationHome(), SETTINGS_XML)); + } + return userSettings; } private static File getUserMavenConfigurationHome() { @@ -506,7 +508,6 @@ private DefaultRepositorySystemSession newRepositorySystemSession() throws Boots } private List resolveRemoteRepos() throws BootstrapMavenException { - final List rawRepos = new ArrayList<>(); readMavenReposFromEnv(rawRepos, System.getenv()); diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapModelResolver.java index 979ad84c4160c..086c8e5a278a9 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapModelResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapModelResolver.java @@ -1,6 +1,7 @@ package io.quarkus.bootstrap.resolver.maven; import io.quarkus.bootstrap.resolver.maven.workspace.LocalWorkspace; +import io.quarkus.maven.dependency.ArtifactCoords; import java.io.File; import java.util.ArrayList; import java.util.Collection; @@ -203,7 +204,7 @@ public ModelSource resolveModel(final Dependency dependency) throws UnresolvableModelException { try { final Artifact artifact = new DefaultArtifact(dependency.getGroupId(), dependency.getArtifactId(), "", - "pom", dependency.getVersion()); + ArtifactCoords.TYPE_POM, dependency.getVersion()); final VersionRangeRequest versionRangeRequest = new VersionRangeRequest(artifact, repositories, context); versionRangeRequest.setTrace(trace); diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/MavenModelBuilder.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/MavenModelBuilder.java index 3a3d39bc40d88..2b25e09de8b66 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/MavenModelBuilder.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/MavenModelBuilder.java @@ -3,6 +3,7 @@ import io.quarkus.bootstrap.resolver.maven.options.BootstrapMavenOptions; import io.quarkus.bootstrap.resolver.maven.workspace.LocalWorkspace; import io.quarkus.bootstrap.resolver.maven.workspace.ModelUtils; +import io.quarkus.maven.dependency.ArtifactCoords; import java.io.File; import java.io.IOException; import java.util.HashSet; @@ -40,7 +41,7 @@ public ModelBuildingResult build(ModelBuildingRequest request) throws ModelBuild final Model requestModel = getModel(request); if (requestModel != null) { final Artifact artifact = new DefaultArtifact(ModelUtils.getGroupId(requestModel), requestModel.getArtifactId(), - null, "pom", + null, ArtifactCoords.TYPE_POM, ModelUtils.getVersion(requestModel)); if (workspace.findArtifact(artifact) != null) { final ModelBuildingResult result = workspace diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java index 6702c8a642834..86da66217f991 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java @@ -339,6 +339,7 @@ public WorkspaceModule toWorkspaceModule() { .setBuildDir(getOutputDir()); final Build build = (modelBuildingResult == null ? getRawModel() : modelBuildingResult.getEffectiveModel()).getBuild(); + boolean addDefaultSourceSet = true; if (build != null && !build.getPlugins().isEmpty()) { for (Plugin plugin : build.getPlugins()) { if (!plugin.getArtifactId().equals("maven-jar-plugin")) { @@ -347,6 +348,7 @@ public WorkspaceModule toWorkspaceModule() { if (plugin.getExecutions().isEmpty()) { final DefaultArtifactSources src = processJarPluginExecutionConfig(plugin.getConfiguration(), false); if (src != null) { + addDefaultSourceSet = false; moduleBuilder.addArtifactSources(src); } } else { @@ -354,6 +356,7 @@ public WorkspaceModule toWorkspaceModule() { DefaultArtifactSources src = null; if (e.getGoals().contains(ArtifactCoords.TYPE_JAR)) { src = processJarPluginExecutionConfig(e.getConfiguration(), false); + addDefaultSourceSet &= !e.getId().equals("default-jar"); } else if (e.getGoals().contains("test-jar")) { src = processJarPluginExecutionConfig(e.getConfiguration(), true); } @@ -362,10 +365,11 @@ public WorkspaceModule toWorkspaceModule() { } } } + break; } } - if (!moduleBuilder.hasNonTestSources()) { + if (addDefaultSourceSet) { moduleBuilder.addArtifactSources(new DefaultArtifactSources(ArtifactSources.MAIN, Collections.singletonList(new DefaultSourceDir(getSourcesSourcesDir(), getClassesDir())), collectMainResources(null))); @@ -405,7 +409,9 @@ private String resolveElementValue(String elementValue) { if (elementValue == null || elementValue.isEmpty() || !(elementValue.startsWith("${") && elementValue.endsWith("}"))) { return elementValue; } - return rawModel.getProperties().getProperty(elementValue.substring(2, elementValue.length() - 1), elementValue); + final String propName = elementValue.substring(2, elementValue.length() - 1); + String v = System.getProperty(propName); + return v == null ? rawModel.getProperties().getProperty(propName, elementValue) : v; } private DefaultArtifactSources processJarPluginExecutionConfig(Object config, boolean test) { diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalWorkspace.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalWorkspace.java index 3b38e304b43f6..945229794138f 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalWorkspace.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalWorkspace.java @@ -109,7 +109,7 @@ public File findArtifact(Artifact artifact) { if (ArtifactCoords.TYPE_POM.equals(artifact.getExtension())) { final File pom = lp.getRawModel().getPomFile(); // if the pom exists we should also check whether the main artifact can also be resolved from the workspace - if (pom.exists() && ("pom".equals(lp.getRawModel().getPackaging()) + if (pom.exists() && (ArtifactCoords.TYPE_POM.equals(lp.getRawModel().getPackaging()) || mvnCtx != null && mvnCtx.isPreferPomsFromWorkspace() || Files.exists(lp.getOutputDir()) || emptyJarOutput(lp, artifact) != null)) { diff --git a/independent-projects/bootstrap/pom.xml b/independent-projects/bootstrap/pom.xml index 0882ef631e788..9f2e67b83fa15 100644 --- a/independent-projects/bootstrap/pom.xml +++ b/independent-projects/bootstrap/pom.xml @@ -40,7 +40,7 @@ 3.22.0 0.9.5 - 3.4.3.Final + 3.5.0.Final 1.14.2 5.8.2 3.8.4 @@ -51,7 +51,7 @@ 3.4.3 4.4.15 1.0.0.Final - 2.13.2.20220328 + 2.13.3 1.3.5 2.0.2 1.0 @@ -59,14 +59,15 @@ 31.1-jre 1.0.1 1.2.6 - 1.0.9 + 1.0.10 1.1.0.Final 1.7.36 - 22.0.0.2 + 22.1.0 2.6.0 1.11.0 7.4.2 0.0.9 + 0.1.1 bom @@ -252,6 +253,22 @@ clean install + + quick-build-docs + + + quicklyDocs + + + + true + true + true + + + clean install + + quick-build-ci @@ -446,6 +463,7 @@ io.quarkus.jakarta-versions io.quarkus.maven.javax.versions + io.quarkus.smallrye diff --git a/independent-projects/bootstrap/runner/pom.xml b/independent-projects/bootstrap/runner/pom.xml index c3a85a452cb6c..83406882a2a93 100644 --- a/independent-projects/bootstrap/runner/pom.xml +++ b/independent-projects/bootstrap/runner/pom.xml @@ -50,6 +50,10 @@ org.jboss.logging jboss-logging + + io.github.crac + org-crac + org.junit.jupiter junit-jupiter diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/logging/LateBoundMDCProvider.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/logging/LateBoundMDCProvider.java new file mode 100644 index 0000000000000..13d218efe94ae --- /dev/null +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/logging/LateBoundMDCProvider.java @@ -0,0 +1,101 @@ +package io.quarkus.bootstrap.logging; + +import java.util.Collections; +import java.util.Map; +import org.jboss.logmanager.MDCProvider; + +/** + * Class enabling Quarkus to instantiate a {@link MDCProvider} + * and set a delegate during runtime initialization. + * + * While no delegate is set it serves as a NOP MDC. + * + * LateBoundMDCProvider is an implementation of the MDC Provider SPI + * it will only be used/discovered if a provider configuration file + * {@code META-INF/services/org.jboss.logmanager.MDCProvider } is created. + */ +@SuppressWarnings({ "unused" }) +public class LateBoundMDCProvider implements MDCProvider { + private static volatile MDCProvider delegate; + + /** + * Set the actual {@link MDCProvider} to use as the delegate. + * + * @param delegate Properly constructed {@link MDCProvider}. + */ + public static void setMDCProviderDelegate(MDCProvider delegate) { + LateBoundMDCProvider.delegate = delegate; + } + + @Override + public String get(String key) { + if (delegate == null) { + return null; + } + return delegate.get(key); + } + + @Override + public Object getObject(String key) { + if (delegate == null) { + return null; + } + return delegate.getObject(key); + } + + @Override + public String put(String key, String value) { + if (delegate == null) { + return null; + } + return delegate.put(key, value); + } + + @Override + public Object putObject(String key, Object value) { + if (delegate == null) { + return null; + } + return delegate.putObject(key, value); + } + + @Override + public String remove(String key) { + if (delegate == null) { + return null; + } + return delegate.remove(key); + } + + @Override + public Object removeObject(String key) { + if (delegate == null) { + return null; + } + return delegate.removeObject(key); + } + + @Override + public Map copy() { + if (delegate == null) { + return Collections.emptyMap(); + } + return delegate.copy(); + } + + @Override + public Map copyObject() { + if (delegate == null) { + return Collections.emptyMap(); + } + return delegate.copyObject(); + } + + @Override + public void clear() { + if (delegate == null) { + return; + } + delegate.clear(); + } +} diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java index 18b95b0cdbd50..1fced46e0a7ea 100644 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java @@ -7,6 +7,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.crac.Context; +import org.crac.Resource; /** * Classloader used with the fast-jar package type. @@ -41,6 +43,8 @@ public final class RunnerClassLoader extends ClassLoader { //Protected by synchronization on the above field, as they are related. private boolean postBootPhase = false; + private final CracResource resource; + RunnerClassLoader(ClassLoader parent, Map resourceDirectoryMap, Set parentFirstPackages, Set nonExistentResources, List fullyIndexedDirectories, Map directlyIndexedResourcesIndexMap) { @@ -50,6 +54,9 @@ public final class RunnerClassLoader extends ClassLoader { this.nonExistentResources = nonExistentResources; this.fullyIndexedDirectories = fullyIndexedDirectories; this.directlyIndexedResourcesIndexMap = directlyIndexedResourcesIndexMap; + + resource = new CracResource(); + org.crac.Core.getGlobalContext().register(resource); } @Override @@ -290,4 +297,22 @@ public void resetInternalCaches() { this.postBootPhase = true; } } + + class CracResource implements Resource { + @Override + public void beforeCheckpoint(Context ctx) { + synchronized (currentlyBufferedResources) { + for (int i = 0; i < currentlyBufferedResources.length; ++i) { + if (currentlyBufferedResources[i] != null) { + currentlyBufferedResources[i].resetInternalCaches(); + currentlyBufferedResources[i] = null; + } + } + } + } + + @Override + public void afterRestore(Context ctx) { + } + } } diff --git a/independent-projects/enforcer-rules/pom.xml b/independent-projects/enforcer-rules/pom.xml index bb76149502b98..8a1d2949cef6b 100644 --- a/independent-projects/enforcer-rules/pom.xml +++ b/independent-projects/enforcer-rules/pom.xml @@ -162,6 +162,23 @@ clean install + + quick-build-docs + + + quicklyDocs + + + + true + true + true + true + + + clean install + + quick-build-ci diff --git a/independent-projects/ide-config/pom.xml b/independent-projects/ide-config/pom.xml index 4c3d790831a40..4ca5954ad8d37 100644 --- a/independent-projects/ide-config/pom.xml +++ b/independent-projects/ide-config/pom.xml @@ -85,6 +85,22 @@ clean install + + quick-build-docs + + + quicklyDocs + + + + true + true + true + + + clean install + + quick-build-ci diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Engine.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Engine.java index 4ba6fd6598733..d784f85814494 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Engine.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Engine.java @@ -11,7 +11,7 @@ * * @see EngineBuilder */ -public interface Engine { +public interface Engine extends ErrorInitializer { /** * diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java index 13d187f5508bb..c5aee97fa51af 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java @@ -42,7 +42,7 @@ class EngineImpl implements Engine { this.valueResolvers = sort(builder.valueResolvers); this.namespaceResolvers = ImmutableList. builder() .addAll(builder.namespaceResolvers).add(new TemplateImpl.DataNamespaceResolver()).build(); - this.evaluator = new EvaluatorImpl(this.valueResolvers, this.namespaceResolvers, builder.strictRendering); + this.evaluator = new EvaluatorImpl(this.valueResolvers, this.namespaceResolvers, builder.strictRendering, this); this.templates = new ConcurrentHashMap<>(); this.locators = sort(builder.locators); this.resultMappers = sort(builder.resultMappers); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ErrorCode.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ErrorCode.java new file mode 100644 index 0000000000000..212bedea2a2fd --- /dev/null +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ErrorCode.java @@ -0,0 +1,18 @@ +package io.quarkus.qute; + +/** + * Represents a unique error code. + * + * @see TemplateException + */ +public interface ErrorCode { + + /** + * Implementations are encouraged to use a prefix for a group of related problems, i.e. the parser error codes start with + * {@code PARSER_}. + * + * @return the unique name + */ + String getName(); + +} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ErrorInitializer.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ErrorInitializer.java new file mode 100644 index 0000000000000..9949a5c69cc7b --- /dev/null +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ErrorInitializer.java @@ -0,0 +1,15 @@ +package io.quarkus.qute; + +public interface ErrorInitializer { + + /** + * + * @param message + * @return a new initialized {@link TemplateException} builder instance + */ + default TemplateException.Builder error(String message) { + return TemplateException.builder() + .message("Rendering error{#if origin.hasNonGeneratedTemplateId??} in{origin}{/if}: " + message); + } + +} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvalSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvalSectionHelper.java index 403eb4e429477..90da6cc22e8c4 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvalSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvalSectionHelper.java @@ -37,14 +37,16 @@ public CompletionStage resolve(SectionResolutionContext context) { template = (TemplateImpl) engine.parse(templateStr); } catch (TemplateException e) { Origin origin = parameters.get(TEMPLATE).getOrigin(); - StringBuilder builder = new StringBuilder("Parser error in the evaluated template"); - if (!origin.getTemplateId().equals(origin.getTemplateGeneratedId())) { - builder.append(" in template [").append(origin.getTemplateId()).append("]"); - } - builder.append(" on line ").append(origin.getLine()).append(":\n\t") - .append(e.getMessage()); - throw new TemplateException(parameters.get(TEMPLATE).getOrigin(), - builder.toString()); + throw TemplateException.builder() + .message( + "Parser error in the evaluated template: {templateId} line {line}:\\n\\t{originalMessage}") + .code(Code.ERROR_IN_EVALUATED_TEMPLATE) + .argument("templateId", + origin.hasNonGeneratedTemplateId() ? " template [" + origin.getTemplateId() + "]" + : "") + .argument("line", origin.getLine()) + .argument("originalMessage", e.getMessage()) + .build(); } template.root .resolve(context.resolutionContext().createChild(Mapper.wrap(evaluatedParams), null)) @@ -96,4 +98,17 @@ public Scope initializeBlock(Scope outerScope, BlockInfo block) { } + enum Code implements ErrorCode { + + ERROR_IN_EVALUATED_TEMPLATE, + + ; + + @Override + public String getName() { + return "EVAL_" + name(); + } + + } + } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatorImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatorImpl.java index c3f8c030d4982..352ef80030357 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatorImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EvaluatorImpl.java @@ -22,8 +22,10 @@ class EvaluatorImpl implements Evaluator { private final List resolvers; private final Map> namespaceResolvers; private final boolean strictRendering; + private final ErrorInitializer initializer; - EvaluatorImpl(List valueResolvers, List namespaceResolvers, boolean strictRendering) { + EvaluatorImpl(List valueResolvers, List namespaceResolvers, boolean strictRendering, + ErrorInitializer errorInitializer) { this.resolvers = valueResolvers; Map> namespaceResolversMap = new HashMap<>(); for (NamespaceResolver namespaceResolver : namespaceResolvers) { @@ -46,6 +48,7 @@ class EvaluatorImpl implements Evaluator { } this.namespaceResolvers = namespaceResolversMap; this.strictRendering = strictRendering; + this.initializer = errorInitializer; } @Override @@ -55,10 +58,13 @@ public CompletionStage evaluate(Expression expression, ResolutionContext parts = expression.getParts().iterator(); List matching = namespaceResolvers.get(expression.getNamespace()); if (matching == null) { - return CompletedStage.failure(new TemplateException(expression.getOrigin(), - String.format("No namespace resolver found for [%s] in expression {%s} in template %s on line %s", - expression.getNamespace(), expression.toOriginalString(), - expression.getOrigin().getTemplateId(), expression.getOrigin().getLine()))); + return CompletedStage.failure( + initializer.error("No namespace resolver found for [{namespace}] in expression \\{{expression}\\}") + .code(Code.NAMESPACE_RESOLVER_NOT_FOUND) + .argument("namespace", expression.getNamespace()) + .argument("expression", expression.toOriginalString()) + .origin(expression.getOrigin()) + .build()); } EvalContext context = new EvalContextImpl(false, null, resolutionContext, parts.next()); if (matching.size() == 1) { @@ -222,16 +228,33 @@ private static CompletionStage toCompletionStage(Object result) { return CompletedStage.of(result); } - private static TemplateException propertyNotFound(Object result, Expression expression) { + private TemplateException propertyNotFound(Object result, Expression expression) { String propertyMessage; if (result instanceof NotFound) { propertyMessage = ((NotFound) result).asMessage(); } else { propertyMessage = "Property not found"; } - return new TemplateException(expression.getOrigin(), - String.format("%s in expression {%s} in template %s on line %s", propertyMessage, expression.toOriginalString(), - expression.getOrigin().getTemplateId(), expression.getOrigin().getLine())); + return initializer.error("{prop} in expression \\{{expression}\\}") + .code(Code.PROPERTY_NOT_FOUND) + .origin(expression.getOrigin()) + .arguments(Map.of("prop", propertyMessage, "expression", expression.toOriginalString())) + .build(); + } + + enum Code implements ErrorCode { + + PROPERTY_NOT_FOUND, + + NAMESPACE_RESOLVER_NOT_FOUND, + + ; + + @Override + public String getName() { + return "EVALUATOR_" + name(); + } + } static class EvalContextImpl implements EvalContext { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionNode.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionNode.java index 7770ab74061dd..a10303e2a043d 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionNode.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ExpressionNode.java @@ -15,20 +15,18 @@ class ExpressionNode implements TemplateNode, Function resolve(ResolutionContext context) { if (traceLevel) { - LOG.tracef("Resolve {%s} started: %s", expression.toOriginalString(), expression.getOrigin()); + LOG.tracef("Resolve {%s} started:%s", expression.toOriginalString(), expression.getOrigin()); } return context.evaluate(expression).thenCompose(this); } @@ -36,7 +34,7 @@ public CompletionStage resolve(ResolutionContext context) { @Override public CompletionStage apply(Object result) { if (traceLevel) { - LOG.tracef("Resolve {%s} completed: %s", expression.toOriginalString(), expression.getOrigin()); + LOG.tracef("Resolve {%s} completed:%s", expression.toOriginalString(), expression.getOrigin()); } if (result instanceof ResultNode) { return CompletedStage.of((ResultNode) result); @@ -48,7 +46,7 @@ public CompletionStage apply(Object result) { } public Origin getOrigin() { - return origin; + return expression.getOrigin(); } @Override diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expressions.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expressions.java index 0c91d55aa7b54..a7554e5f1a2fa 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expressions.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expressions.java @@ -37,14 +37,17 @@ public static List parseVirtualMethodParams(String value, Origin origin, String params = value.substring(start + 1, value.length() - 1); return splitParts(params, PARAMS_SPLIT_CONFIG); } - throw Parser.parserError("invalid virtual method in {" + exprValue + "}", origin); + throw Parser.error(ParserError.INVALID_VIRTUAL_METHOD, "invalid virtual method in \\{{exprValue}}", origin) + .argument("exprValue", exprValue).build(); } public static String parseBracketContent(String value, Origin origin, String exprValue) { if (value.endsWith(SQUARE_RIGHT_BRACKET)) { return value.substring(1, value.length() - 1); } - throw Parser.parserError("invalid bracket notation expression in {" + exprValue + "}", origin); + throw Parser.error(ParserError.INVALID_BRACKET_EXPRESSION, + "invalid bracket notation expression in \\{{exprValue}}", origin) + .argument("exprValue", exprValue).build(); } public static String buildVirtualMethodSignature(String name, List params) { @@ -196,6 +199,24 @@ public boolean isInfixNotationSupported() { }; + static final SplitConfig PARAM_DECLARATION_SPLIT_CONFIG = new SplitConfig() { + + @Override + public boolean isSeparator(char candidate) { + return ' ' == candidate; + } + + public boolean isInfixNotationSupported() { + return false; + } + + @Override + public boolean isLiteralSeparator(char candidate) { + return SplitConfig.super.isLiteralSeparator(candidate) || candidate == '<' || candidate == '>'; + } + + }; + private static final SplitConfig TYPE_INFO_SPLIT_CONFIG = new DefaultSplitConfig() { @Override @@ -223,7 +244,7 @@ public boolean shouldAppendSeparator(char candidate) { } - interface SplitConfig { + public interface SplitConfig { boolean isSeparator(char candidate); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java index a799e2ddcc716..790ff8b4c5948 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java @@ -2,7 +2,6 @@ import static io.quarkus.qute.Booleans.isFalsy; -import io.quarkus.qute.SectionHelperFactory.ParserDelegate; import io.quarkus.qute.SectionHelperFactory.SectionInitContext; import java.math.BigDecimal; import java.math.BigInteger; @@ -27,9 +26,9 @@ public class IfSectionHelper implements SectionHelper { IfSectionHelper(SectionInitContext context) { List conditionBlocks = new ArrayList<>(); - for (SectionBlock part : context.getBlocks()) { - if (SectionHelperFactory.MAIN_BLOCK_NAME.equals(part.label) || ELSE.equals(part.label)) { - conditionBlocks.add(new ConditionBlock(part, context)); + for (SectionBlock block : context.getBlocks()) { + if (SectionHelperFactory.MAIN_BLOCK_NAME.equals(block.label) || ELSE.equals(block.label)) { + conditionBlocks.add(new ConditionBlock(block, context)); } } if (conditionBlocks.size() == 1) { @@ -204,7 +203,7 @@ static class ConditionBlock { public ConditionBlock(SectionBlock block, SectionInitContext context) { this.section = block; - List params = parseParams(new ArrayList<>(block.parameters.values()), context); + List params = parseParams(new ArrayList<>(block.parameters.values()), block); if (!params.isEmpty() && !SectionHelperFactory.MAIN_BLOCK_NAME.equals(block.label)) { params = params.subList(1, params.size()); } @@ -501,9 +500,9 @@ static BigDecimal getDecimal(Object value) { } - static List parseParams(List params, ParserDelegate parserDelegate) { + static List parseParams(List params, B block) { - replaceOperatorsAndCompositeParams(params, parserDelegate); + replaceOperatorsAndCompositeParams(params, block); int highestPrecedence = getHighestPrecedence(params); if (!isGroupingNeeded(params)) { @@ -558,16 +557,26 @@ static List parseParams(List params, ParserDelegate parserDelega ret.addAll(params.subList(lastGroupdIdx + 1, params.size())); } } - return parseParams(ret, parserDelegate); + return parseParams(ret, block); } private static boolean isGroupingNeeded(List params) { - // No operators or all of the same precedence - return params.stream().filter(p -> (p instanceof Operator)).map(p -> ((Operator) p).getPrecedence()).distinct() - .count() > 1; + Integer lastPrecedence = null; + for (Object param : params) { + if (param instanceof Operator) { + Operator op = (Operator) param; + if (lastPrecedence == null) { + lastPrecedence = op.getPrecedence(); + } else if (!lastPrecedence.equals(op.getPrecedence())) { + return true; + } + } + } + return false; } - private static void replaceOperatorsAndCompositeParams(List params, ParserDelegate parserDelegate) { + private static void replaceOperatorsAndCompositeParams(List params, + B block) { for (ListIterator iterator = params.listIterator(); iterator.hasNext();) { Object param = iterator.next(); if (param instanceof String) { @@ -575,8 +584,12 @@ private static void replaceOperatorsAndCompositeParams(List params, Pars Operator operator = Operator.from(stringParam); if (operator != null) { if (operator.isBinary() && !iterator.hasNext()) { - throw parserDelegate.createParserError( - "binary operator [" + operator + "] set but the second operand not present for {#if} section"); + throw block.error( + "binary operator [{operator}] set but the second operand not present for \\{#if\\} section") + .argument("operator", operator) + .code(Code.BINARY_OPERATOR_MISSING_SECOND_OPERAND) + .origin(block.getOrigin()) + .build(); } iterator.set(operator); } else { @@ -585,13 +598,13 @@ private static void replaceOperatorsAndCompositeParams(List params, Pars iterator.set(Operator.NOT); stringParam = stringParam.substring(1); if (stringParam.charAt(0) == Parser.START_COMPOSITE_PARAM) { - iterator.add(processCompositeParam(stringParam, parserDelegate)); + iterator.add(processCompositeParam(stringParam, block)); } else { iterator.add(stringParam); } } else { if (stringParam.charAt(0) == Parser.START_COMPOSITE_PARAM) { - iterator.set(processCompositeParam(stringParam, parserDelegate)); + iterator.set(processCompositeParam(stringParam, block)); } } } @@ -612,15 +625,16 @@ private static int getHighestPrecedence(List params) { return highestPrecedence; } - static List processCompositeParam(String stringParam, ParserDelegate parserDelegate) { + static List processCompositeParam(String stringParam, B block) { // Composite params if (!stringParam.endsWith("" + Parser.END_COMPOSITE_PARAM)) { throw new TemplateException("Invalid composite parameter found: " + stringParam); } List split = new ArrayList<>(); - Parser.splitSectionParams(stringParam.substring(1, stringParam.length() - 1), TemplateException::new) + Parser.splitSectionParams(stringParam.substring(1, stringParam.length() - 1), + block) .forEachRemaining(split::add); - return parseParams(split, parserDelegate); + return parseParams(split, block); } @SuppressWarnings("unchecked") @@ -668,4 +682,20 @@ static Condition createCondition(Object param, SectionBlock block, Operator oper return condition; } + enum Code implements ErrorCode { + + /** + * {#if foo >}{/} + */ + BINARY_OPERATOR_MISSING_SECOND_OPERAND, + + ; + + @Override + public String getName() { + return "IF_" + name(); + } + + } + } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java index df817372b64fd..43f40aa8ad009 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java @@ -2,7 +2,6 @@ import static io.quarkus.qute.Futures.evaluateParams; -import io.quarkus.qute.TemplateNode.Origin; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -17,33 +16,51 @@ public class IncludeSectionHelper implements SectionHelper { static final String DEFAULT_NAME = "$default$"; private static final String TEMPLATE = "template"; - private final Supplier